mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-09 06:01:39 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9a43abb7f | |||
| 3649f76045 | |||
| bdd8ad1413 | |||
| 46aefa8307 | |||
| 6343b62778 | |||
| 6ca3acbeff | |||
| 16f5d4b0e2 | |||
| 350f789737 | |||
| 54e89daa46 | |||
| 770343b1c0 | |||
| 4cf13a8e5a | |||
| b73f34454f | |||
| 754f239241 | |||
| 8e845235d2 | |||
| 04ffccdeac | |||
| 447b68e306 | |||
| 7471141eba | |||
| a1fba2218d | |||
| 22c7fa1b08 | |||
| 9028b18792 | |||
| a5bf0f2bfb | |||
| 2e33760575 | |||
| 3b8788d5d2 | |||
| 00a9ef1c06 | |||
| fd0f458978 | |||
| 3ff2797b11 | |||
| f0e4f53f03 | |||
| 2b486e3399 | |||
| 29b5df8722 | |||
| ae378bbdb7 | |||
| 0e0c7826ab | |||
| fa5e0faaf7 | |||
| b07a5c780c | |||
| 13e240ebdb | |||
| a701b610c5 | |||
| 17af0e54d9 | |||
| efb5ee0bf0 | |||
| 7a681c7c43 | |||
| 7e87912177 | |||
| e76881d64a | |||
| ad98dca8c2 | |||
| 1fc7c033cb | |||
| 8d4a313657 | |||
| 3a7259250b | |||
| 9292f16cd7 | |||
| 95473f4c24 | |||
| 6d606d9277 | |||
| d1baaaab48 | |||
| 921a83825a | |||
| 4e3b3d133d | |||
| a9376b3b04 | |||
| f7025af97e | |||
| a3544ba01f | |||
| c6acbf5440 | |||
| ba5beac7fc | |||
| 4b8cf7fb25 | |||
| dc966804e6 | |||
| 98c1a466fd | |||
| bcccb4373f | |||
| 049e2f6287 | |||
| b619eae9ef | |||
| 4b673692f6 | |||
| 9633a37421 | |||
| d090f5d769 | |||
| 51d7a82aa3 | |||
| d2c183baeb | |||
| b63eaa81d4 | |||
| 94db17b53a | |||
| ab5202677b | |||
| 469c1b2ed7 | |||
| 1b5e1e9886 | |||
| d8d7f863e5 | |||
| e8fee00df6 | |||
| 266616de7b |
@@ -44,7 +44,7 @@ runs:
|
||||
|
||||
- name: Login to builtin registry
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||||
with:
|
||||
registry: ${{ env.BUILTIN_REGISTRY }}
|
||||
username: ${{ inputs.registry_user }}
|
||||
@@ -52,7 +52,7 @@ runs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
||||
with:
|
||||
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
|
||||
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
|
||||
@@ -61,7 +61,7 @@ runs:
|
||||
- name: Extract metadata (tags) for Docker
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
||||
with:
|
||||
flavor: |
|
||||
latest=auto
|
||||
|
||||
@@ -67,7 +67,7 @@ runs:
|
||||
uses: ./.forgejo/actions/rust-toolchain
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
||||
with:
|
||||
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
|
||||
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
|
||||
@@ -75,11 +75,11 @@ runs:
|
||||
|
||||
- name: Set up QEMU
|
||||
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4
|
||||
|
||||
- name: Login to builtin registry
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||||
with:
|
||||
registry: ${{ env.BUILTIN_REGISTRY }}
|
||||
username: ${{ inputs.registry_user }}
|
||||
@@ -87,7 +87,7 @@ runs:
|
||||
|
||||
- name: Extract metadata (labels, annotations) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
||||
with:
|
||||
images: ${{ inputs.images }}
|
||||
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
|
||||
|
||||
@@ -33,7 +33,7 @@ runs:
|
||||
echo "version=$(rustup --version)" >> $GITHUB_OUTPUT
|
||||
- name: Cache rustup toolchains
|
||||
if: steps.rustup-version.outputs.version == ''
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
~/.rustup
|
||||
|
||||
@@ -57,7 +57,7 @@ runs:
|
||||
|
||||
- name: Check for LLVM cache
|
||||
id: cache
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
/usr/bin/clang-*
|
||||
@@ -120,7 +120,7 @@ runs:
|
||||
|
||||
- name: Install additional packages
|
||||
if: inputs.extra-packages != ''
|
||||
uses: https://github.com/awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # latest
|
||||
uses: https://github.com/awalsh128/cache-apt-pkgs-action@2c09a5e66da6c8016428a2172bd76e5e4f14bb17 # latest
|
||||
with:
|
||||
packages: ${{ inputs.extra-packages }}
|
||||
version: 1.0
|
||||
|
||||
@@ -65,7 +65,7 @@ runs:
|
||||
|
||||
- name: Cache toolchain binaries
|
||||
id: toolchain-cache
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
.cargo/bin
|
||||
@@ -76,7 +76,7 @@ runs:
|
||||
|
||||
- name: Cache Cargo registry and git
|
||||
id: registry-cache
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
.cargo/registry/index
|
||||
|
||||
@@ -31,7 +31,7 @@ runs:
|
||||
|
||||
- name: Restore binary cache
|
||||
id: binary-cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
/usr/share/rust/.cargo/bin
|
||||
@@ -71,13 +71,13 @@ runs:
|
||||
|
||||
- name: Install timelord-cli and git-warp-time
|
||||
if: steps.check-binaries.outputs.need-install == 'true'
|
||||
uses: https://github.com/taiki-e/install-action@50414676f9f5d50a65992c6dd2ed02641263226c # v2
|
||||
uses: https://github.com/taiki-e/install-action@9e1e5806d4a4822de933115878265be9aaa786d9 # v2
|
||||
with:
|
||||
tool: git-warp-time,timelord-cli@3.0.1
|
||||
|
||||
- name: Save binary cache
|
||||
if: steps.check-binaries.outputs.need-install == 'true'
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
/usr/share/rust/.cargo/bin
|
||||
@@ -87,7 +87,7 @@ runs:
|
||||
|
||||
- name: Restore timelord cache with fallbacks
|
||||
id: timelord-restore
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ${{ env.TIMELORD_CACHE_PATH }}
|
||||
key: ${{ env.TIMELORD_KEY }}
|
||||
@@ -114,7 +114,7 @@ runs:
|
||||
timelord sync --source-dir ${{ env.TIMELORD_PATH }} --cache-dir ${{ env.TIMELORD_CACHE_PATH }}
|
||||
|
||||
- name: Save updated timelord cache immediately
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ${{ env.TIMELORD_CACHE_PATH }}
|
||||
key: ${{ env.TIMELORD_KEY }}
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Cache Cargo registry
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
|
||||
- name: Cache DNF packages
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
/var/cache/dnf
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
dnf-fedora${{ steps.fedora.outputs.version }}-
|
||||
|
||||
- name: Cache Cargo registry
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
cargo-fedora${{ steps.fedora.outputs.version }}-
|
||||
|
||||
- name: Cache Rust build dependencies
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
~/rpmbuild/BUILD/*/target/release/deps
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
node-version: 22
|
||||
|
||||
- name: Cache npm dependencies
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: continuwuity-rspress-${{ steps.runner-env.outputs.slug }}-${{ steps.runner-env.outputs.arch }}-node-${{ steps.runner-env.outputs.node_version }}-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
# repositories: continuwuity
|
||||
|
||||
- name: Install regsync
|
||||
uses: https://github.com/regclient/actions/regsync-installer@39bf30ced4be9d147faaabbde2682e9725fb8bd1 # main
|
||||
uses: https://github.com/regclient/actions/regsync-installer@4b4db1dcc7dad75ad67a788a380f75a20cc8a040 # main
|
||||
|
||||
- name: Check what images need mirroring
|
||||
run: |
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for file changes
|
||||
uses: https://github.com/dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4
|
||||
uses: https://github.com/dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push Docker image by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
||||
with:
|
||||
context: .
|
||||
file: "docker/Dockerfile"
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push max-perf Docker image by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
||||
with:
|
||||
context: .
|
||||
file: "docker/Dockerfile"
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
name: Renovate
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/renovatebot/renovate:43.252.1@sha256:121eb04ef758537019fb58f587aa53c99e5fda4e703993ce2ca01bd4b3926bd4
|
||||
image: ghcr.io/renovatebot/renovate:43.234.0@sha256:bff111bfe347c559c615b658b28721eba5b7bb32a7b7901ea321336767209fe1
|
||||
options: --tmpfs /tmp:exec
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
run: /usr/local/renovate/node -e 'console.log(`node heap limit = ${require("v8").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'
|
||||
|
||||
- name: Restore renovate repo cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
/tmp/renovate/cache/renovate/repository
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
renovate-repo-cache-
|
||||
|
||||
- name: Restore renovate package cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
/tmp/renovate/cache/renovate/renovate-cache-sqlite
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
renovate-package-cache-
|
||||
|
||||
- name: Restore renovate OSV cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
/tmp/osv
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
|
||||
- name: Save renovate package cache
|
||||
if: always()
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
/tmp/renovate/cache/renovate/renovate-cache-sqlite
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
|
||||
- name: Save renovate OSV cache
|
||||
if: always()
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
/tmp/osv
|
||||
|
||||
Generated
+343
-801
File diff suppressed because it is too large
Load Diff
+2
-18
@@ -45,7 +45,7 @@ version = "1.0.6"
|
||||
version = "1.0.0"
|
||||
|
||||
[workspace.dependencies.cargo_toml]
|
||||
version = "1.0"
|
||||
version = "0.22"
|
||||
default-features = false
|
||||
features = ["features"]
|
||||
|
||||
@@ -141,12 +141,6 @@ features = [
|
||||
version = "0.23.25"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.aws-lc-sys]
|
||||
version = "0.41.0"
|
||||
|
||||
[workspace.dependencies.aws-lc-rs]
|
||||
version = "1.17.0"
|
||||
|
||||
[workspace.dependencies.reqwest]
|
||||
version = "0.13.2"
|
||||
default-features = false
|
||||
@@ -170,7 +164,7 @@ features = ["raw_value"]
|
||||
|
||||
# Used for appservice registration files
|
||||
[workspace.dependencies.serde-saphyr]
|
||||
version = "0.0.29"
|
||||
version = "0.0.27"
|
||||
|
||||
# Used to load forbidden room/user regex from config
|
||||
[workspace.dependencies.serde_regex]
|
||||
@@ -362,7 +356,6 @@ features = [
|
||||
"ring-compat",
|
||||
"compat-upload-signatures",
|
||||
"compat-optional-txn-pdus",
|
||||
"compat-get-3pids",
|
||||
"unstable-msc2666",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc2870",
|
||||
@@ -409,9 +402,6 @@ default-features = false
|
||||
version = "0.11.0"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.openidconnect]
|
||||
version = "4.0.1"
|
||||
|
||||
# optional opentelemetry, performance measurements, flamegraphs, etc for performance measurements and monitoring
|
||||
[workspace.dependencies.opentelemetry]
|
||||
version = "0.32.0"
|
||||
@@ -569,12 +559,6 @@ features = ["std"]
|
||||
[workspace.dependencies.nonzero_ext]
|
||||
version = "0.3.0"
|
||||
|
||||
[workspace.dependencies.resolvematrix]
|
||||
version = "0.1.2"
|
||||
|
||||
[workspace.dependencies.serde_urlencoded]
|
||||
version = "0.7.1"
|
||||
|
||||
#
|
||||
# Patches
|
||||
#
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Appservice device management as outlined in MSC4190 (part of Matrix 1.17) is now fully supported. Contributed by @ginger.
|
||||
@@ -1 +0,0 @@
|
||||
Improved invite and join reliability in clients using legacy sync. Contributed by @ginger
|
||||
@@ -1 +0,0 @@
|
||||
Users may now be forbidden from deactivating their own accounts with the new `allow_deactivation` config option. Contributed by @ginger.
|
||||
@@ -1 +0,0 @@
|
||||
Added support for authenticating clients using the new OAuth 2.0 login API. Contributed by @ginger.
|
||||
@@ -1 +0,0 @@
|
||||
Rewrite the resolver service to use [resolvematrix](https://forgejo.ellis.link/continuwuation/resolvematrix) for server resolution. Rewrite by @s1lv3r, crate by @Jade
|
||||
@@ -1 +0,0 @@
|
||||
Updated deployment docs to account for new RPM package availability across more distros. Contributed by @julian45.
|
||||
@@ -1 +0,0 @@
|
||||
Added support for linking an external identity provider with OIDC. Contributed by @ginger.
|
||||
+9
-135
@@ -521,15 +521,17 @@
|
||||
#
|
||||
#recaptcha_private_site_key =
|
||||
|
||||
# Controls whether users are allowed to deactivate their own accounts
|
||||
# through the account management panel or their Matrix clients. Server
|
||||
# admins can always deactivate users using the relevant admin commands.
|
||||
# Policy documents, such as terms and conditions or a privacy policy,
|
||||
# which users must agree to when registering an account.
|
||||
#
|
||||
# Note that, in some jurisdictions, you may be legally required to honor
|
||||
# users who request to deactivate their accounts if you set this option
|
||||
# to `false`.
|
||||
# Example:
|
||||
# ```ignore
|
||||
# [global.registration_terms.privacy_policy]
|
||||
# en = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
||||
# es = { name = "Política de Privacidad", url = "https://homeserver.example/es/privacy_policy.html" }
|
||||
# ```
|
||||
#
|
||||
#allow_deactivation = true
|
||||
#registration_terms = {}
|
||||
|
||||
# Controls whether encrypted rooms and events are allowed.
|
||||
#
|
||||
@@ -1434,11 +1436,6 @@
|
||||
#
|
||||
#send_messages_from_ignored_users_to_client = false
|
||||
|
||||
# Send "org.matrix.dummy_event" events to the client. This is a debugging
|
||||
# option.
|
||||
#
|
||||
#send_dummy_events_to_clients = false
|
||||
|
||||
# Vector list of IPv4 and IPv6 CIDR ranges / subnets *in quotes* that you
|
||||
# do not want continuwuity to send outbound requests to. Defaults to
|
||||
# RFC1918, unroutable, loopback, multicast, and testnet addresses for
|
||||
@@ -1998,126 +1995,3 @@
|
||||
# `require_email_for_registration`.
|
||||
#
|
||||
#require_email_for_token_registration = false
|
||||
|
||||
#[global.registration_terms]
|
||||
|
||||
# The language code to provide to clients along with the policy documents.
|
||||
#
|
||||
#language = "en"
|
||||
|
||||
# Policy documents, such as terms and conditions or a privacy policy,
|
||||
# which users must agree to when registering an account.
|
||||
#
|
||||
# Example:
|
||||
# ```ignore
|
||||
# [global.registration_terms.documents]
|
||||
# privacy_policy = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
||||
# ```
|
||||
#
|
||||
#documents =
|
||||
|
||||
#[global.oauth]
|
||||
|
||||
# The compatibility mode to use for OAuth.
|
||||
#
|
||||
# - "disabled": OAuth will be unavailable. Users will only be able to log
|
||||
# in using legacy authentication.
|
||||
# - "hybrid": OAuth and legacy authentication will both be available. Some
|
||||
# clients may only use one or the other.
|
||||
# - "exclusive": Only OAuth will be available. Clients which require
|
||||
# legacy authentication will be unable to log in.
|
||||
#
|
||||
#compatibility_mode = "hybrid"
|
||||
|
||||
#[global.oauth.oidc]
|
||||
# Uncommenting this section will enable Continuwuity's support for
|
||||
# authenticating users using an OpenID Connect-compatible identity provider.
|
||||
# This is referred to as "delegated authentication".
|
||||
#
|
||||
# IMPORTANT NOTE: When delegated authentication is active, Continuwuity will behave as if
|
||||
# the `global.oauth.compatibility_mode` setting is set to `exclusive`.
|
||||
# Matrix clients which do not support OAuth login (also referred to as "next-gen auth") will NOT be able
|
||||
# to log in while delegated authentication is active.
|
||||
|
||||
|
||||
# The OIDC issuer URL. Continuwuity will use OpenID Connect Discovery to
|
||||
# automatically fetch the identity provider's metadata from this URL.
|
||||
# Generally you should set this to the base domain your identity provider
|
||||
# runs on.
|
||||
#
|
||||
#discovery_url =
|
||||
|
||||
# The OAuth client ID for Continuwuity to use when communicating with the
|
||||
# identity provider.
|
||||
#
|
||||
#client_id =
|
||||
|
||||
# The OAuth client secret for Continuwuity to use when communicating with
|
||||
# the identity provider.
|
||||
#
|
||||
#client_secret =
|
||||
|
||||
# A path to a file which Continuwuity will read the client secret from.
|
||||
# If this option is set, it will override `client_secret`.
|
||||
#
|
||||
# The server will fail to start if the file cannot be read.
|
||||
#
|
||||
#client_secret_file =
|
||||
|
||||
# Additional scopes Continuwuity should request from the IDP. This may be
|
||||
# necessary to access certain claims. Continuwuity always requests the
|
||||
# `openid` scope.
|
||||
#
|
||||
#additional_scopes = []
|
||||
|
||||
# Whether the user should be prompted to choose a localpart
|
||||
# when signing in for the first time. If this is `false`, Continuwuity
|
||||
# will attempt to use the value of the `preferred_username_claim`
|
||||
# (see below) as the user's localpart. Authentication will
|
||||
# fail if this claim is missing or is not a valid localpart.
|
||||
#
|
||||
#prompt_for_localpart = true
|
||||
|
||||
# The claim to use for the user's localpart, if `prompt_for_localpart` is
|
||||
# false.
|
||||
#
|
||||
#preferred_username_claim = "preferred_username"
|
||||
|
||||
# The claim which will be used to set the user's email address,
|
||||
# either on initial registration or on every login depending on
|
||||
# the value of `profile_key_import_mode`. Continuwuity assumes that
|
||||
# the IDP has taken care of verifying that the user controls the email
|
||||
# address it provides.
|
||||
#
|
||||
# This option does nothing if SMTP is not configured.
|
||||
#
|
||||
# If this option is set, and `profile_key_import_mode` is `on_login`,
|
||||
# users will not be able to change their email addresses themselves.
|
||||
#
|
||||
#email_claim = "email"
|
||||
|
||||
# Defines how claims returned from the IDP should be mapped to a user's
|
||||
# profile data. The profile field named in each key will be set from the
|
||||
# claim named in the corresponding value when the user first registers,
|
||||
# and possibly on subsequent logins as well, depending on the value of
|
||||
# `profile_key_import_mode` (see below).
|
||||
#
|
||||
# Per-room overrides to the user's display name or avatar will be
|
||||
# preserved by the import process.
|
||||
#
|
||||
# SECURITY NOTE: If the `avatar_url` field is set, Continuwuity will
|
||||
# perform a HTTP GET to the URL in the mapped claim and use the returned
|
||||
# file as the user's profile picture. Make sure your users are not able
|
||||
# to set the value of the mapped claim to an arbitrary URL.
|
||||
#
|
||||
#profile_key_map = { displayname = "name" }
|
||||
|
||||
# When profile keys should be imported from the IDP's claims.
|
||||
#
|
||||
# - "on_registration": Listed keys will be imported once, when the user
|
||||
# logs in for the first time and their shadow account is created.
|
||||
# - "on_login": Listed keys will be imported every time the user logs in.
|
||||
# Additionally, users will not be able to manually edit any listed keys
|
||||
# through their Matrix client.
|
||||
#
|
||||
#profile_key_import_mode = "on_registration"
|
||||
|
||||
+2
-2
@@ -36,8 +36,8 @@
|
||||
},
|
||||
{
|
||||
"type": "dir",
|
||||
"name": "guides",
|
||||
"label": "Guides"
|
||||
"name": "advanced",
|
||||
"label": "Advanced"
|
||||
},
|
||||
"security",
|
||||
{
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
[
|
||||
{
|
||||
"text": "Documentation",
|
||||
"text": "Guide",
|
||||
"link": "/introduction",
|
||||
"activeMatch": "^/(introduction|configuration|deploying|calls|appservices|maintenance|troubleshooting|guides)"
|
||||
"activeMatch": "^/(introduction|configuration|deploying|calls|appservices|maintenance|troubleshooting|advanced)"
|
||||
},
|
||||
{
|
||||
"text": "Development",
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
"type": "file",
|
||||
"name": "performance",
|
||||
"label": "Performance tuning"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "oidc",
|
||||
"label": "Delegated authentication"
|
||||
}
|
||||
|
||||
]
|
||||
@@ -58,13 +58,6 @@ # Defaults to members of the admin room if unset
|
||||
|
||||
To enable full discovery, you will need to reverse proxy these paths from the base domain back to Continuwuity.
|
||||
|
||||
:::warning
|
||||
If you choose to serve the well-known files directly through your reverse proxy, and you are
|
||||
using delegation, you must still set the `global.well_known.client` configuration option to
|
||||
the domain you're delegating to. Continuwuity needs to know the domain it runs on for OAuth-compatible
|
||||
clients to work correctly.
|
||||
:::
|
||||
|
||||
## Reverse proxying well-known files to Continuwuity
|
||||
|
||||
<details>
|
||||
@@ -16,8 +16,8 @@
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "rpm",
|
||||
"label": "RPM"
|
||||
"name": "fedora",
|
||||
"label": "Fedora"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
|
||||
@@ -44,7 +44,7 @@ ### Prerequisites
|
||||
- Alternatively, if you want both client and federation traffic on `:443`, you can configure `CONTINUWUITY_WELL_KNOWN` following some of the [examples](#choose-your-reverse-proxy) below.
|
||||
|
||||
:::tip Split-domain setups
|
||||
For more setups with `.well-known` delegation and split-domain deployments, consult the [Delegation/Split-domain](../guides/delegation) page.
|
||||
For more setups with `.well-known` delegation and split-domain deployments, consult the [Delegation/Split-domain](../advanced/delegation) page.
|
||||
:::
|
||||
|
||||
## Docker Compose
|
||||
@@ -79,7 +79,7 @@ ### Choose Your Reverse Proxy
|
||||
nameserver 1.1.1.1
|
||||
```
|
||||
|
||||
Consult the [**DNS tuning guide (recommended)**](../guides/dns.mdx) for full solutions to this issue.
|
||||
Consult the [**DNS tuning guide (recommended)**](../advanced/dns.mdx) for full solutions to this issue.
|
||||
:::
|
||||
|
||||
#### Caddy (using Caddyfile)
|
||||
@@ -269,7 +269,7 @@ ### Accessing the Server's Console
|
||||
|
||||
## Next steps
|
||||
|
||||
- For smooth federation, set up a caching resolver according to the [**DNS tuning guide**](../guides/dns.mdx) (recommended)
|
||||
- For smooth federation, set up a caching resolver according to the [**DNS tuning guide**](../advanced/dns.mdx) (recommended)
|
||||
- To set up Audio/Video communication, see the [**Calls**](../calls.mdx) page.
|
||||
- Consult the [Maintenance](../maintenance.mdx) page for guidance on maintaining your homeserver.
|
||||
- If you want to set up an appservice, take a look at the [**Appservice Guide**](../appservices.mdx).
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# RPM Installation Guide
|
||||
|
||||
Continuwuity is available as RPM packages for Fedora and compatible distributions.
|
||||
We do not currently have infrastructure to build RPMs for RHEL and compatible distributions, but this is a work in progress.
|
||||
|
||||
The RPM packaging files are maintained in the `fedora/` directory:
|
||||
- `continuwuity.spec.rpkg` - RPM spec file using rpkg macros for building from git
|
||||
- `continuwuity.service` - Systemd service file for the server
|
||||
- `RPM-GPG-KEY-continuwuity.asc` - GPG public key for verifying signed packages
|
||||
|
||||
RPM packages built by CI are signed with our GPG key (RSA, ID: `6595 E8DB 9191 D39A 46D6 A514 4BA7 F590 DF0B AA1D`). # spellchecker:disable-line
|
||||
|
||||
```bash
|
||||
# Import the signing key
|
||||
sudo rpm --import https://forgejo.ellis.link/api/packages/continuwuation/rpm/repository.key
|
||||
|
||||
# Verify a downloaded package
|
||||
rpm --checksig continuwuity-*.rpm
|
||||
```
|
||||
|
||||
## Installation methods
|
||||
|
||||
**Stable releases** (recommended)
|
||||
|
||||
```bash
|
||||
# Add the repository and install
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable.repo
|
||||
sudo dnf install continuwuity
|
||||
```
|
||||
|
||||
**Development builds** from main branch
|
||||
|
||||
```bash
|
||||
# Add the dev repository and install
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/dev.repo
|
||||
sudo dnf install continuwuity
|
||||
```
|
||||
|
||||
**Feature branch builds** (example: `tom/new-feature`)
|
||||
|
||||
```bash
|
||||
# Branch names are sanitized (slashes become hyphens, lowercase only)
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/tom-new-feature.repo
|
||||
sudo dnf install continuwuity
|
||||
```
|
||||
|
||||
**Manual repository configuration** (alternative method)
|
||||
|
||||
```bash
|
||||
cat << 'EOF' | sudo tee /etc/yum.repos.d/continuwuity.repo
|
||||
[continuwuity]
|
||||
name=Continuwuity - Matrix homeserver
|
||||
baseurl=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgkey=https://forgejo.ellis.link/api/packages/continuwuation/rpm/repository.key
|
||||
EOF
|
||||
|
||||
sudo dnf install continuwuity
|
||||
```
|
||||
|
||||
## Package management
|
||||
|
||||
**Automatic updates** with DNF Automatic
|
||||
|
||||
```bash
|
||||
# Install and configure
|
||||
sudo dnf install dnf-automatic
|
||||
sudo nano /etc/dnf/automatic.conf # Set: apply_updates = yes
|
||||
sudo systemctl enable --now dnf-automatic.timer
|
||||
```
|
||||
|
||||
**Manual updates**
|
||||
|
||||
```bash
|
||||
# Check for updates
|
||||
sudo dnf check-update continuwuity
|
||||
|
||||
# Update to latest version
|
||||
sudo dnf update continuwuity
|
||||
```
|
||||
|
||||
**Switching channels** (stable/dev/feature branches)
|
||||
|
||||
```bash
|
||||
# List enabled repositories
|
||||
dnf repolist | grep continuwuation
|
||||
|
||||
# Disable current repository
|
||||
sudo dnf config-manager --set-disabled continuwuation-stable # or -dev, or branch name
|
||||
|
||||
# Enable desired repository
|
||||
sudo dnf config-manager --set-enabled continuwuation-dev # or -stable, or branch name
|
||||
|
||||
# Update to the new channel's version
|
||||
sudo dnf update continuwuity
|
||||
```
|
||||
|
||||
**Verifying installation**
|
||||
|
||||
```bash
|
||||
# Check installed version
|
||||
rpm -q continuwuity
|
||||
|
||||
# View package information
|
||||
rpm -qi continuwuity
|
||||
|
||||
# List installed files
|
||||
rpm -ql continuwuity
|
||||
|
||||
# Verify package integrity
|
||||
rpm -V continuwuity
|
||||
```
|
||||
|
||||
## Service management and removal
|
||||
|
||||
**Systemd service commands**
|
||||
|
||||
```bash
|
||||
# Start the service
|
||||
sudo systemctl start conduwuit
|
||||
|
||||
# Enable on boot
|
||||
sudo systemctl enable conduwuit
|
||||
|
||||
# Check status
|
||||
sudo systemctl status conduwuit
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u conduwuit -f
|
||||
```
|
||||
|
||||
**Uninstallation**
|
||||
|
||||
```bash
|
||||
# Stop and disable the service
|
||||
sudo systemctl stop conduwuit
|
||||
sudo systemctl disable conduwuit
|
||||
|
||||
# Remove the package
|
||||
sudo dnf remove continuwuity
|
||||
|
||||
# Remove the repository (optional)
|
||||
sudo rm /etc/yum.repos.d/continuwuation-*.repo
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**GPG key errors**: Temporarily disable GPG checking
|
||||
|
||||
```bash
|
||||
sudo dnf --nogpgcheck install continuwuity
|
||||
```
|
||||
|
||||
**Repository metadata issues**: Clear and rebuild cache
|
||||
|
||||
```bash
|
||||
sudo dnf clean all
|
||||
sudo dnf makecache
|
||||
```
|
||||
|
||||
**Finding specific versions**
|
||||
|
||||
```bash
|
||||
# List all available versions
|
||||
dnf --showduplicates list continuwuity
|
||||
|
||||
# Install a specific version
|
||||
sudo dnf install continuwuity-<version>
|
||||
```
|
||||
|
||||
## Building locally
|
||||
|
||||
Build the RPM locally using rpkg:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
sudo dnf install rpkg rpm-build cargo-rpm-macros systemd-rpm-macros
|
||||
|
||||
# Clone the repository
|
||||
git clone https://forgejo.ellis.link/continuwuation/continuwuity.git
|
||||
cd continuwuity
|
||||
|
||||
# Build SRPM
|
||||
rpkg srpm
|
||||
|
||||
# Build RPM
|
||||
rpmbuild --rebuild *.src.rpm
|
||||
```
|
||||
@@ -170,7 +170,7 @@ ## Exposing ports in the firewall or the router
|
||||
are: `ufw allow 8448/tcp` and `ufw allow 443/tcp`.
|
||||
|
||||
:::tip Alternative port/domain setups
|
||||
If you would like to use only port 443, a different port, or a subdomain for the homeserver, you will need to set up `.well-known` delegation. Consult the `[global.well_known]` section of the config file, and the [**Delegation/Split-domain**](../guides/delegation) page to learn more about these kinds of deployments.
|
||||
If you would like to use only port 443, a different port, or a subdomain for the homeserver, you will need to set up `.well-known` delegation. Consult the `[global.well_known]` section of the config file, and the [**Delegation/Split-domain**](../advanced/delegation) page to learn more about these kinds of deployments.
|
||||
:::
|
||||
|
||||
## Setting up the Reverse Proxy
|
||||
@@ -300,8 +300,7 @@ # For client-server endpoints
|
||||
|
||||
## What's next?
|
||||
|
||||
- For smooth federation, set up a caching resolver according to the [**DNS tuning guide**](../guides/dns.mdx) (recommended)
|
||||
- To configure OIDC login with an identity provider, see the [**delegated authentication guide**](../guides/oidc.mdx).
|
||||
- For smooth federation, set up a caching resolver according to the [**DNS tuning guide**](../advanced/dns.mdx) (recommended)
|
||||
- For Audio/Video call functionality see the [**Calls**](../calls.md) page.
|
||||
- Consult the [Maintenance](../maintenance.mdx) page for guidance on maintaining your homeserver.
|
||||
- If you want to set up an appservice, take a look at the [**Appservice Guide**](../appservices.md).
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import { Tab, Tabs} from '@rspress/core/theme';
|
||||
|
||||
# RPM Installation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Continuwuity versions v0.5.10+ are available as RPM packages for the following distributions:
|
||||
|
||||
<Tabs groupId="distro">
|
||||
<Tab label="Fedora">
|
||||
Available for upstream-supported Fedora versions (including compatible distributions, such as Ultramarine Linux) and Rawhide through Terra.
|
||||
</Tab>
|
||||
<Tab label="EL">
|
||||
Available for Enterprise Linux (RHEL, AlmaLinux, Rocky Linux, etc.) **10+** through Terra.
|
||||
|
||||
:::warning Oracle Linux support
|
||||
Due to upstream limitations, Terra is only usable on Oracle Linux if you use [upstream EPEL](https://docs.fedoraproject.org/en-US/epel/getting-started/)
|
||||
rather than Oracle's rebuilds of EPEL. Oracle tends to lag behind on new EL versions, both major and minor—for example, as of the time of writing, 10.2 has been
|
||||
available for over a month through other ELs, but Oracle has not yet released corresponding updates—so you may encounter further compatibility issues with EPEL.
|
||||
**For this reason, it is recommended that you use another EL distribution if at all possible.**
|
||||
:::
|
||||
|
||||
</Tab>
|
||||
<Tab label="SUSE">
|
||||
Available for openSUSE Tumbleweed and [supported Leap versions](https://en.opensuse.org/Lifetime) through the openSUSE Build Service.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Installation methods
|
||||
|
||||
### Stable releases (recommended)
|
||||
|
||||
<Tabs groupId="distro">
|
||||
<Tab label="Fedora">
|
||||
1. Follow [Terra's directions for adding the Terra repo on your distribution](https://docs.terrapkg.com/usage/installing/#fedora-and-derivatives).
|
||||
2. Install the `continuwuity` package.
|
||||
</Tab>
|
||||
<Tab label="EL">
|
||||
1. Follow [Terra's directions for adding the Terra repo on your distribution](https://docs.terrapkg.com/usage/installing/#enterprise-linux). Be sure to acquire/enable relevant dependencies!
|
||||
2. Install the `continuwuity` package.
|
||||
</Tab>
|
||||
<Tab label="SUSE">
|
||||
openSUSE packages are available through an openSUSE Build Service project namespace provided by a community member.
|
||||
|
||||
1. Navigate to [this page](https://software.opensuse.org/download.html?project=home%3Ajulian45&package=continuwuity).
|
||||
2. Click on the "Add repository and install manually" text to expand install instructions.
|
||||
3. Follow the provided instructions for your version of openSUSE.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Nightly releases
|
||||
Nightly versions are built from the latest commit on the `main` branch every 24 hours.
|
||||
|
||||
<Tabs groupId="distro">
|
||||
<Tab label="Fedora">
|
||||
1. Follow [Terra's directions for adding the Terra repo on your distribution](https://docs.terrapkg.com/usage/installing/#fedora-and-derivatives).
|
||||
2. Install the `continuwuity-nightly` package.
|
||||
</Tab>
|
||||
<Tab label="EL">
|
||||
1. Follow [Terra's directions for adding the Terra repo on your distribution](https://docs.terrapkg.com/usage/installing/#enterprise-linux). Be sure to acquire/enable relevant dependencies!
|
||||
2. Install the `continuwuity-nightly` package.
|
||||
</Tab>
|
||||
<Tab label="SUSE">
|
||||
Nightly packages for openSUSE are **not currently available**.
|
||||
|
||||
If there is an ongoing need for these, please ask in `#continuwuity:continuwuity.org` or [open an issue on Forgejo](https://forgejo.ellis.link/continuwuation/continuwuity/issues/new).
|
||||
In the meantime, you may follow the [generic deployment instructions](generic).
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Service management and removal
|
||||
|
||||
**Systemd service commands**
|
||||
|
||||
```bash
|
||||
# Start the service
|
||||
sudo systemctl start conduwuit
|
||||
|
||||
# Enable on boot
|
||||
sudo systemctl enable conduwuit
|
||||
|
||||
# Check status
|
||||
sudo systemctl status conduwuit
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u conduwuit -f
|
||||
```
|
||||
|
||||
**Uninstallation**
|
||||
|
||||
```bash
|
||||
# Stop and disable the service
|
||||
sudo systemctl stop conduwuit
|
||||
sudo systemctl disable conduwuit
|
||||
|
||||
# Remove the package
|
||||
sudo dnf remove continuwuity # replace `dnf` with `zypper` on openSUSE
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**GPG key errors**: Temporarily disable GPG checking
|
||||
|
||||
```bash
|
||||
sudo dnf --nogpgcheck install continuwuity
|
||||
```
|
||||
|
||||
**Repository metadata issues**: Clear and rebuild cache
|
||||
|
||||
```bash
|
||||
sudo dnf clean all
|
||||
sudo dnf makecache
|
||||
```
|
||||
|
||||
**Finding specific versions**
|
||||
|
||||
```bash
|
||||
# List all available versions
|
||||
dnf --showduplicates list continuwuity
|
||||
|
||||
# Install a specific version
|
||||
sudo dnf install continuwuity-<version>
|
||||
```
|
||||
|
||||
## Building locally
|
||||
|
||||
<Tabs groupId="distro">
|
||||
<Tab label="Fedora">
|
||||
:::details Directly from Continuwuity source
|
||||
|
||||
Build the RPM locally using rpkg:
|
||||
```bash
|
||||
# Install dependencies
|
||||
sudo dnf install rpkg rpm-build cargo-rpm-macros systemd-rpm-macros
|
||||
|
||||
# Clone the repository
|
||||
git clone https://forgejo.ellis.link/continuwuation/continuwuity.git
|
||||
cd continuwuity
|
||||
|
||||
# Build SRPM
|
||||
rpkg srpm
|
||||
|
||||
# Build RPM
|
||||
rpmbuild --rebuild *.src.rpm
|
||||
```
|
||||
:::
|
||||
|
||||
Or, use Terra:
|
||||
1. Follow [Terra's prep directions](https://docs.terrapkg.com/contributing/getting-started/#preparation) to bootstrap your development environment.
|
||||
2. `git clone` the [Terra sources repo](https://github.com/terrapkg/packages).
|
||||
3. Follow [Terra's build instructions](https://docs.terrapkg.com/contributing/getting-started/#building), using the path `anda/misc/continuwuity/nightly/pkg`. If desiring a build of the latest stable version, replace `nightly` with `stable` instead.
|
||||
</Tab>
|
||||
<Tab label="EL">
|
||||
1. Follow [Terra's prep directions](https://docs.terrapkg.com/contributing/getting-started/#preparation) to bootstrap your development environment.
|
||||
2. `git clone` the [Terra sources repo](https://github.com/terrapkg/packages).
|
||||
3. Follow [Terra's build instructions](https://docs.terrapkg.com/contributing/getting-started/#building), using the path `anda/misc/continuwuity/nightly/pkg`. If desiring a build of the latest stable version, replace `nightly` with `stable` instead.
|
||||
</Tab>
|
||||
<Tab label="SUSE">
|
||||
1. Navigate to [this package's location in the openSUSE Build Service web UI](https://build.opensuse.org/package/show/home:julian45/continuwuity).
|
||||
2. Click on the "Checkout package" text and follow the instructions to acquire a local copy of the packaging source.
|
||||
3. In the `_service` file, adjust the revision within the `<param name="revision">` tag (approx. line 6) to your desired git repo revision.
|
||||
4. Run `osc service ra` to update your copy of the Continuwuity sources and vendored packages.
|
||||
5. Run `osc build` to locally build an openSUSE-ready RPM.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -1,56 +0,0 @@
|
||||
# Configuring delegated authentication
|
||||
|
||||
Continuwuity supports delegating user authentication to an external identity provider that implements the OpenID Connect specification, such as Authentik, kanidm, or Keycloak.
|
||||
|
||||
:::warning{title="OIDC versus OAuth, and supported clients"}
|
||||
**OIDC** is not to be confused with **OAuth**. In the context of Matrix, OAuth is the protocol that Matrix clients use to authenticate with the _homeserver_. OIDC is the protocol that the _homeserver_ uses to communicate with the _identity provider_. Continuwuity supports OAuth by default, alongside the legacy **UIAA** authentication framework.
|
||||
|
||||
When OIDC is configured, Continuwuity will disable its support for legacy authentication. **Only clients that support OAuth**, such as the Element family of clients, will be able to log in when OIDC is configured. If your client of choice shows an error when you try to log in after configuring OIDC, it likely does not support OAuth. This is an issue with your client, not Continuwuity, and should be reported to your client's developers.
|
||||
:::
|
||||
|
||||
A simple OIDC configuration is as easy as creating a new OIDC application in your identity provider's settings and supplying Continuwuity with the client ID and client secret. This guide will use kanidm as an example, but the described steps are broadly applicable to other identity providers.
|
||||
|
||||
First, create a new application for Continuwuity in your identity provider.
|
||||
```sh
|
||||
# Here, `c10y` is the client ID that kanidm will use, and `Continuwuity` is the display name.
|
||||
# Other identity providers may generate a client ID for you.
|
||||
# Use the domain that clients can reach Continuwuity at, which may not be the same as your server name
|
||||
# if you have configured well-known delegation.
|
||||
kanidm system oauth2 create c10y Continuwuity https://matrix.yourdomain.com
|
||||
```
|
||||
|
||||
Configure the redirect URL that Continuwuity uses.
|
||||
```sh
|
||||
kanidm system oauth2 add-redirect-url c10y https://matrix.yourdomain.com/_continuwuity/oidc/complete
|
||||
```
|
||||
|
||||
Allow Continuwuity to request the `openid` scope. Other identity providers may not require this step.
|
||||
```sh
|
||||
kanidm system oauth2 update-scope-map c10y idm_all_persons openid
|
||||
```
|
||||
|
||||
Find the client secret that was generated. Other identity providers may show this information in their web UI.
|
||||
```sh
|
||||
kanidm system oauth2 show-basic-secret c10y
|
||||
d1qgx352kkuvs1j70b6w293d65x68jve1f7b27fyk90gjhpr
|
||||
```
|
||||
|
||||
Configure Continuwuity with the client ID, client secret, and discovery URL. kanidm has a different discovery URL for each client, but other identity providers may have a single discovery URL at the root of their domain.
|
||||
|
||||
```toml
|
||||
[global.oauth.oidc]
|
||||
|
||||
# `/.well-known/openid-configuration` will be appended automatically
|
||||
discovery_url = "https://idm.example.com/oauth2/openid/c10y"
|
||||
|
||||
# This may be randomly generated by your identity provider. kanidm requires
|
||||
# you to set it manually when you create the application.
|
||||
client_id = "c10y"
|
||||
|
||||
# From the previous step
|
||||
client_secret = "d1qgx352kkuvs1j70b6w293d65x68jve1f7b27fyk90gjhpr"
|
||||
```
|
||||
|
||||
Finally, restart Continuwuity, and log out and back in again. Your client should prompt you to continue in your web browser and open a webpage with the Continuwuity logo that allows you to continue in your identity provider. Once you log in successfully, you will be prompted to choose a user ID -- to link your existing account, enter its user ID, and then your old password when prompted.
|
||||
|
||||
Continuwuity offers several additional configuration options to tweak its integration with your identity provider. Review the `[global.oauth.oidc]` section towards the bottom of the [reference configuration](../reference/config) for a complete list of options and documentation.
|
||||
@@ -10,13 +10,7 @@ ## `!admin debug echo`
|
||||
|
||||
## `!admin debug get-auth-chain`
|
||||
|
||||
Loads the auth_chain of a PDU, reporting how long it took
|
||||
|
||||
## `!admin debug show-auth-chain`
|
||||
|
||||
Walks & displays the auth_chain of a PDU in a mermaid graph format.
|
||||
|
||||
This is useless to basically anyone but developers, and is also probably slow and memory hungry.
|
||||
Get the auth_chain of a PDU
|
||||
|
||||
## `!admin debug parse-pdu`
|
||||
|
||||
@@ -50,12 +44,6 @@ ## `!admin debug get-room-state`
|
||||
|
||||
Of course the check is still done on the actual client API.
|
||||
|
||||
## `!admin debug get-state-at`
|
||||
|
||||
Gets all the room state events at the specified event.
|
||||
|
||||
State at event might not be available for some PDUs, such as rejected ones.
|
||||
|
||||
## `!admin debug get-signing-keys`
|
||||
|
||||
Get and display signing keys from local cache or remote server
|
||||
|
||||
@@ -14,7 +14,6 @@ ## Categories
|
||||
- [`!admin appservices`](appservices/): Commands for managing appservices
|
||||
- [`!admin users`](users/): Commands for managing local users
|
||||
- [`!admin token`](token/): Commands for managing registration tokens
|
||||
- [`!admin oidc`](oidc/): Commands for managing OIDC
|
||||
- [`!admin rooms`](rooms/): Commands for managing rooms
|
||||
- [`!admin federation`](federation/): Commands for managing federation
|
||||
- [`!admin server`](server/): Commands for managing the server
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<!-- This file is generated by `cargo xtask generate-docs`. Do not edit. -->
|
||||
# `!admin oidc`
|
||||
|
||||
Commands for managing OIDC
|
||||
|
||||
|
||||
## `!admin oidc link`
|
||||
|
||||
Link a user ID to the given subject claim
|
||||
|
||||
## `!admin oidc unlink`
|
||||
|
||||
Unlink the given subject claim from its associated user ID
|
||||
@@ -12,6 +12,10 @@ ## `!admin users reset-password`
|
||||
|
||||
Reset user password
|
||||
|
||||
## `!admin users issue-password-reset-link`
|
||||
|
||||
Issue a self-service password reset link for a user
|
||||
|
||||
## `!admin users get-email`
|
||||
|
||||
Get a user's associated email address
|
||||
@@ -92,14 +96,6 @@ ## `!admin users list-users`
|
||||
|
||||
List local users in the database
|
||||
|
||||
## `!admin users list-invited-rooms`
|
||||
|
||||
Lists all the rooms (local and remote) that the specified user is invited to
|
||||
|
||||
## `!admin users reject-all-invites`
|
||||
|
||||
Manually make a user reject all current invites
|
||||
|
||||
## `!admin users list-joined-rooms`
|
||||
|
||||
Lists all the rooms (local and remote) that the specified user is joined in
|
||||
|
||||
@@ -60,7 +60,7 @@ ### DNS server overload
|
||||
|
||||
Matrix federation is extremely heavy and sends wild amounts of DNS requests. This makes normal resolvers like the ones above unsuitable for its activity. Ultimately, the best solution/fix for this is to selfhost a high quality caching DNS resolver such as Unbound, and configure Continuwuity to use it.
|
||||
|
||||
Follow the [**DNS tuning guide**](./guides/dns) for details on setting it up.
|
||||
Follow the [**DNS tuning guide**](./advanced/dns) for details on setting it up.
|
||||
|
||||
### Intermittent federation failures to a specific server
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
{
|
||||
# basic nix shell containing all things necessary to build continuwuity in all flavors manually (on x86_64-linux)
|
||||
devShells.default =
|
||||
((inputs.crane.mkLib pkgs).overrideToolchain (pkgs: self'.packages.stable-toolchain)).devShell
|
||||
(inputs.crane.mkLib pkgs).overrideToolchain (pkgs: self'.packages.stable-toolchain).devShell
|
||||
{
|
||||
packages = [
|
||||
self'.packages.rocksdb
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
target:
|
||||
target.fromToolchainName {
|
||||
name = (lib.importTOML "${inputs.self}/rust-toolchain.toml").toolchain.channel;
|
||||
sha256 = "sha256-h+t2xTBz5yt2YIO+1VMIIGlCU7gyp2LYOFvaV1nwOXU=";
|
||||
sha256 = "sha256-mvUGEOHYJpn3ikC5hckneuGixaC+yGrkMM/liDIDgoU=";
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
Generated
+36
-36
@@ -383,18 +383,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/core": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.16.tgz",
|
||||
"integrity": "sha512-jJcYNNBKY/VLR8oxLqdd5uvm6bHahXeF3wSaoAe2U1hxWWwoP9k4rBTDx9X3JkUWcgnthu7UgtMiHeLs+2fhFg==",
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.14.tgz",
|
||||
"integrity": "sha512-k59i08zwBGgHrjHw8CK1m4CeTrKPvZRmV54bxubQl6AdDdmhJK6WrNg3UthwWmd38scKtqF40ATXDE8RMiNcNA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@rsbuild/core": "^2.0.15",
|
||||
"@rsbuild/core": "^2.0.9",
|
||||
"@rsbuild/plugin-react": "~2.0.1",
|
||||
"@rspress/shared": "2.0.16",
|
||||
"@shikijs/rehype": "^4.2.0",
|
||||
"@rspress/shared": "2.0.14",
|
||||
"@shikijs/rehype": "^4.0.2",
|
||||
"@types/unist": "^3.0.3",
|
||||
"@unhead/react": "^2.1.15",
|
||||
"body-scroll-lock": "4.0.0-beta.0",
|
||||
@@ -407,22 +407,22 @@
|
||||
"mdast-util-mdxjs-esm": "^2.0.1",
|
||||
"medium-zoom": "1.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"react-lazy-with-preload": "^2.2.1",
|
||||
"react-reconciler": "0.33.0",
|
||||
"react-render-to-markdown": "19.1.0",
|
||||
"react-router-dom": "^7.18.1",
|
||||
"react-router-dom": "^7.15.1",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-cjk-friendly": "^2.3.1",
|
||||
"remark-cjk-friendly-gfm-strikethrough": "^2.3.1",
|
||||
"remark-cjk-friendly": "^2.0.1",
|
||||
"remark-cjk-friendly-gfm-strikethrough": "^2.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-mdx": "^3.1.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"scroll-into-view-if-needed": "^3.1.0",
|
||||
"shiki": "^4.2.0",
|
||||
"shiki": "^4.0.2",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-remove": "^4.0.0",
|
||||
"unist-util-visit": "^5.1.0",
|
||||
@@ -436,9 +436,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/plugin-client-redirects": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.16.tgz",
|
||||
"integrity": "sha512-FEjZb+3lxpkEESdt0uWa4dRQU7d/Okn5SyX7CDaMHdHuqg4XdOidAQ/95ZzgXomN7YVPv40eVe/0is3oWnAjew==",
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.14.tgz",
|
||||
"integrity": "sha512-/WpbWUiepQglpPeplxCnELe2c7VdBUxPiICPAVnS1ZxAFdYkIpW0C+Vbk1t08kZqx8EAZGu+s6Zy43zyQpjdxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -449,9 +449,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/plugin-sitemap": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.16.tgz",
|
||||
"integrity": "sha512-zRPvKAGF8EexblJvrkhhHtD2Kqlbaw6XFjbIMM07gs0SIUKNg1o3T2I/uo5cvWtk0pGfYyyAJj94HoqLUgUsEw==",
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.14.tgz",
|
||||
"integrity": "sha512-Gpone22PvXGfGRSyi/WM8IXgsvKhNspXqHjtPD3g62jX8SJL3kpj2YZ2V28WEkg672fICauUYXrpre74Rddcsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -462,14 +462,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/shared": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.16.tgz",
|
||||
"integrity": "sha512-FjBSfGtgrlR1bRJ0EQLyNo2qXUXxzb2QE3NPRIICf8TKpP413gRCMyMRtzhbIqD4Gn7k+em82VAkWTAAQjQLTw==",
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.14.tgz",
|
||||
"integrity": "sha512-sCe9tAo+s9tR4DmFSjMyHOxQvhzTSYXkkMUfVEo5w+uMCNXXGAIC6D0xAVDMHq1jIFF9ix47VxzlCo+CYNS14g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rsbuild/core": "^2.0.15",
|
||||
"@shikijs/rehype": "^4.2.0",
|
||||
"@rsbuild/core": "^2.0.9",
|
||||
"@shikijs/rehype": "^4.0.2",
|
||||
"unified": "^11.0.5"
|
||||
}
|
||||
},
|
||||
@@ -2869,9 +2869,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.18.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
|
||||
"integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
|
||||
"integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2892,13 +2892,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.18.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
|
||||
"integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz",
|
||||
"integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.18.1"
|
||||
"react-router": "7.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -3058,9 +3058,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/remark-cjk-friendly": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-cjk-friendly/-/remark-cjk-friendly-2.3.1.tgz",
|
||||
"integrity": "sha512-f+pKZRxCRwNEGFBKNRAZAqU91GIK1SAo3ZyFHWRUgC9zcxRR0BXKd6YwqgSsxtW0rNpUDtONj7H5nje2WL3fcA==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-cjk-friendly/-/remark-cjk-friendly-2.1.0.tgz",
|
||||
"integrity": "sha512-ZWGDfTJNLEZ1gap+pd33K13ZhRAWgVDqxKA7JIlBs5IDu+qvbiWl/pEbeuxzRrWyrrkeFFoTnvNw00iW9mBcow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3081,9 +3081,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/remark-cjk-friendly-gfm-strikethrough": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.3.1.tgz",
|
||||
"integrity": "sha512-JE3TGgouk/sy92SemNMEUhO5mNP4on04cmzOV3s3R5Dbk160ewmpM4tgPiinKKvoJ5UW2fTu7FOYsjVbusSA9w==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.1.0.tgz",
|
||||
"integrity": "sha512-3Kyq2hjY7V7eU8MbVbWW6QQLN81pjJcIvKHvPxr8hZZmcq/9wqm3MJ3iUG34Ch9QTM4WHN+a1JVAVC1fSi5mig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
[toolchain]
|
||||
profile = "minimal"
|
||||
channel = "1.96.1"
|
||||
channel = "1.96.0"
|
||||
components = [
|
||||
# For rust-analyzer
|
||||
"rust-src",
|
||||
|
||||
@@ -92,7 +92,6 @@ serde-saphyr.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
resolvematrix.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+11
-26
@@ -1,5 +1,5 @@
|
||||
use clap::Parser;
|
||||
use conduwuit::{Err, Result};
|
||||
use conduwuit::Result;
|
||||
|
||||
use crate::{
|
||||
appservice::{self, AppserviceCommand},
|
||||
@@ -8,7 +8,6 @@
|
||||
debug::{self, DebugCommand},
|
||||
federation::{self, FederationCommand},
|
||||
media::{self, MediaCommand},
|
||||
oidc::{self, OidcCommand},
|
||||
query::{self, QueryCommand},
|
||||
room::{self, RoomCommand},
|
||||
server::{self, ServerCommand},
|
||||
@@ -17,50 +16,46 @@
|
||||
};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = conduwuit_core::BRANDING, version = conduwuit_core::version())]
|
||||
#[command(name = conduwuit_core::name(), version = conduwuit_core::version())]
|
||||
pub enum AdminCommand {
|
||||
/// Commands for managing appservices
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing appservices
|
||||
Appservices(AppserviceCommand),
|
||||
|
||||
/// Commands for managing local users
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing local users
|
||||
Users(UserCommand),
|
||||
|
||||
/// Commands for managing registration tokens
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing registration tokens
|
||||
Token(TokenCommand),
|
||||
|
||||
/// Commands for managing OIDC
|
||||
#[command(subcommand)]
|
||||
Oidc(OidcCommand),
|
||||
|
||||
/// Commands for managing rooms
|
||||
#[command(subcommand)]
|
||||
Rooms(RoomCommand),
|
||||
|
||||
/// Commands for managing federation
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing federation
|
||||
Federation(FederationCommand),
|
||||
|
||||
/// Commands for managing the server
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing the server
|
||||
Server(ServerCommand),
|
||||
|
||||
/// Commands for managing media
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing media
|
||||
Media(MediaCommand),
|
||||
|
||||
/// Commands for checking integrity
|
||||
#[command(subcommand)]
|
||||
/// Commands for checking integrity
|
||||
Check(CheckCommand),
|
||||
|
||||
/// Commands for debugging things
|
||||
#[command(subcommand)]
|
||||
/// Commands for debugging things
|
||||
Debug(DebugCommand),
|
||||
|
||||
/// Low-level queries for database getters and iterators
|
||||
#[command(subcommand)]
|
||||
/// Low-level queries for database getters and iterators
|
||||
Query(QueryCommand),
|
||||
}
|
||||
|
||||
@@ -85,16 +80,6 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
|
||||
context.bail_restricted()?;
|
||||
token::process(command, context).await
|
||||
},
|
||||
| Oidc(command) => {
|
||||
// OIDC commands are all restricted
|
||||
context.bail_restricted()?;
|
||||
|
||||
if !context.services.oidc.enabled() {
|
||||
return Err!("OIDC is not configured");
|
||||
}
|
||||
|
||||
oidc::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,
|
||||
|
||||
@@ -6,12 +6,7 @@
|
||||
impl Context<'_> {
|
||||
pub(super) async fn check_all_users(&self) -> Result {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let users = self
|
||||
.services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let users = self.services.users.stream().collect::<Vec<_>>().await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
let total = users.len();
|
||||
|
||||
@@ -20,10 +20,6 @@
|
||||
};
|
||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||
use lettre::message::Mailbox;
|
||||
use resolvematrix::{
|
||||
resolution::ResolvedDestination,
|
||||
server::{MatrixResolver, MatrixResolverBuilder},
|
||||
};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
|
||||
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId, UInt,
|
||||
@@ -616,7 +612,7 @@ pub(super) async fn force_device_list_updates(&self) -> Result {
|
||||
// Force E2EE device list updates for all users
|
||||
self.services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.stream()
|
||||
.for_each(async |user_id| self.services.users.mark_device_key_update(&user_id).await)
|
||||
.await;
|
||||
|
||||
@@ -1011,23 +1007,13 @@ pub(super) async fn resolve_true_destination(
|
||||
);
|
||||
}
|
||||
|
||||
let resolver: &MatrixResolver = if no_cache {
|
||||
&MatrixResolverBuilder::new()
|
||||
.dangerous_tls_accept_invalid_certs(self.services.server.config.allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure)
|
||||
.http_client(self.services.client.default.clone())
|
||||
.build()?
|
||||
} else {
|
||||
&self.services.resolver.resolver
|
||||
};
|
||||
let actual = self
|
||||
.services
|
||||
.resolver
|
||||
.resolve_actual_dest(&server_name, !no_cache)
|
||||
.await?;
|
||||
|
||||
let actual = resolver.resolve_server(server_name.as_str()).await?;
|
||||
|
||||
let destination = match actual.destination {
|
||||
| ResolvedDestination::Literal(addr) => addr.to_string(),
|
||||
| ResolvedDestination::Named(host, port) => format!("{host}:{port}"),
|
||||
};
|
||||
|
||||
let msg = format!("Destination: {}\nHostname URI (SNI): {}", destination, actual.host);
|
||||
let msg = format!("Destination: {}\nHostname URI: {}", actual.dest, actual.host);
|
||||
self.write_str(&msg).await
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
pub(crate) mod debug;
|
||||
pub(crate) mod federation;
|
||||
pub(crate) mod media;
|
||||
pub(crate) mod oidc;
|
||||
pub(crate) mod query;
|
||||
pub(crate) mod room;
|
||||
pub(crate) mod server;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
use conduwuit::Result;
|
||||
|
||||
use crate::utils::parse_active_local_user_id;
|
||||
|
||||
impl crate::Context<'_> {
|
||||
pub(super) async fn oidc_link(&self, user_id: String, subject: String) -> Result {
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
self.services.oidc.link_user(&user_id, &subject);
|
||||
|
||||
self.write_str(&format!("Subject `{subject}` linked to account `{user_id}`."))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn oidc_unlink(&self, subject: String) -> Result {
|
||||
self.services.oidc.unlink_user(&subject);
|
||||
|
||||
self.write_str(&format!("Subject `{subject}` unlinked."))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
mod commands;
|
||||
|
||||
use clap::Subcommand;
|
||||
use conduwuit::Result;
|
||||
use conduwuit_macros::admin_command_dispatch;
|
||||
|
||||
#[admin_command_dispatch]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum OidcCommand {
|
||||
/// Link a user ID to the given subject claim.
|
||||
#[clap(name = "link")]
|
||||
OidcLink {
|
||||
user_id: String,
|
||||
subject: String,
|
||||
},
|
||||
|
||||
/// Unlink the given subject claim from its associated user ID.
|
||||
#[clap(name = "unlink")]
|
||||
OidcUnlink {
|
||||
subject: String,
|
||||
},
|
||||
}
|
||||
@@ -46,7 +46,7 @@ async fn destinations_cache(&self, server_name: Option<OwnedServerName>) -> Resu
|
||||
writeln!(self, "| Server Name | Destination | Hostname | Expires |").await?;
|
||||
writeln!(self, "| ----------- | ----------- | -------- | ------- |").await?;
|
||||
|
||||
let mut destinations = self.services.resolver.dns.cache.destinations().boxed();
|
||||
let mut destinations = self.services.resolver.cache.destinations().boxed();
|
||||
|
||||
while let Some((name, CachedDest { dest, host, expire })) = destinations.next().await {
|
||||
if let Some(server_name) = server_name.as_ref() {
|
||||
@@ -69,7 +69,7 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
|
||||
writeln!(self, "| Server Name | IP | Port | Expires | Overriding |").await?;
|
||||
writeln!(self, "| ----------- | --- | ----:| ------- | ---------- |").await?;
|
||||
|
||||
let mut overrides = self.services.resolver.dns.cache.overrides().boxed();
|
||||
let mut overrides = self.services.resolver.cache.overrides().boxed();
|
||||
|
||||
while let Some((name, CachedOverride { ips, port, expire, overriding })) =
|
||||
overrides.next().await
|
||||
@@ -92,16 +92,11 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
|
||||
|
||||
async fn flush_cache(&self, name: Option<OwnedServerName>, all: bool) -> Result {
|
||||
if all {
|
||||
self.services.resolver.resolver.clear_cache();
|
||||
self.services.resolver.dns.cache.clear().await;
|
||||
self.services.resolver.cache.clear().await;
|
||||
writeln!(self, "Resolver caches cleared!").await
|
||||
} else if let Some(name) = name {
|
||||
self.services
|
||||
.resolver
|
||||
.resolver
|
||||
.remove_cache_entry(name.as_str());
|
||||
self.services.resolver.dns.cache.del_destination(&name);
|
||||
self.services.resolver.dns.cache.del_override(&name);
|
||||
self.services.resolver.cache.del_destination(&name);
|
||||
self.services.resolver.cache.del_override(&name);
|
||||
self.write_str(&format!("Cleared {name} from resolver caches!"))
|
||||
.await
|
||||
} else {
|
||||
|
||||
@@ -191,13 +191,8 @@ async fn get_latest_backup(&self, user_id: OwnedUserId) -> Result {
|
||||
|
||||
async fn iter_users(&self) -> Result {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result: Vec<OwnedUserId> = self
|
||||
.services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
let result: Vec<OwnedUserId> =
|
||||
self.services.users.stream().map(Into::into).collect().await;
|
||||
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
@@ -207,7 +202,7 @@ async fn iter_users(&self) -> Result {
|
||||
|
||||
async fn iter_users2(&self) -> Result {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result: Vec<_> = self.services.users.stream_local_users().collect().await;
|
||||
let result: Vec<_> = self.services.users.stream().collect().await;
|
||||
let result: Vec<_> = result
|
||||
.into_iter()
|
||||
.map(|user_id| String::from_utf8_lossy(user_id.as_bytes()).into_owned())
|
||||
|
||||
@@ -31,37 +31,14 @@ pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
|
||||
.issue_token(self.sender_or_service_user().into(), expires);
|
||||
|
||||
self.write_str(&format!(
|
||||
"New registration token issued: `{token}` . {}.",
|
||||
"New registration token issued: `{token}`. {}.",
|
||||
if let Some(expires) = info.expires {
|
||||
format!("{expires}")
|
||||
} else {
|
||||
"Never expires".to_owned()
|
||||
}
|
||||
))
|
||||
.await?;
|
||||
|
||||
if self
|
||||
.services
|
||||
.config
|
||||
.oauth
|
||||
.compatibility_mode()
|
||||
.oauth_available()
|
||||
{
|
||||
self.write_str(&format!(
|
||||
"\nInvite link using this token: {}",
|
||||
self.services
|
||||
.config
|
||||
.get_client_domain()
|
||||
.join(&format!(
|
||||
"{}/account/register/?flow=trusted&token={token}",
|
||||
conduwuit::ROUTE_PREFIX
|
||||
))
|
||||
.unwrap()
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn revoke_token(&self, token: String) -> Result {
|
||||
|
||||
+208
-59
@@ -1,10 +1,13 @@
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
fmt::Write as _,
|
||||
};
|
||||
|
||||
use api::client::{
|
||||
full_user_deactivate, leave_room, recreate_push_rules_and_return, remote_leave_room,
|
||||
};
|
||||
use conduwuit::{
|
||||
Err, Result, debug_warn, info,
|
||||
Err, Result, debug_warn, error, info,
|
||||
matrix::{Event, pdu::PartialPdu},
|
||||
utils::{self, ReadyExt},
|
||||
warn,
|
||||
@@ -20,7 +23,7 @@
|
||||
tag::{TagEvent, TagEventContent, TagInfo},
|
||||
},
|
||||
};
|
||||
use service::users::{AccountStatus, HashedPassword};
|
||||
use service::users::HashedPassword;
|
||||
|
||||
use crate::{
|
||||
get_room_info,
|
||||
@@ -48,22 +51,134 @@ pub(super) async fn list_users(&self) -> Result {
|
||||
}
|
||||
|
||||
pub(super) async fn create_user(&self, username: String, password: Option<String>) -> Result {
|
||||
let user_id = self
|
||||
.services
|
||||
.users
|
||||
.determine_registration_user_id(Some(username), None, None)
|
||||
.await?;
|
||||
// Validate user id
|
||||
let user_id = parse_local_user_id(self.services, &username)?;
|
||||
|
||||
let password =
|
||||
&password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
if self.services.config.emergency_password.is_none() {
|
||||
return Err!("Username {user_id} contains disallowed characters or spaces: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
if self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} already exists");
|
||||
}
|
||||
|
||||
let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
|
||||
|
||||
// Create user
|
||||
self.services
|
||||
.users
|
||||
.create(&user_id, Some(HashedPassword::new(&password)?))?;
|
||||
|
||||
// Default to pretty displayname
|
||||
let mut displayname = user_id.localpart().to_owned();
|
||||
|
||||
// If `new_user_displayname_suffix` is set, registration will push whatever
|
||||
// content is set to the user's display name with a space before it
|
||||
if !self
|
||||
.services
|
||||
.server
|
||||
.config
|
||||
.new_user_displayname_suffix
|
||||
.is_empty()
|
||||
{
|
||||
write!(displayname, " {}", self.services.server.config.new_user_displayname_suffix)?;
|
||||
}
|
||||
|
||||
self.services
|
||||
.users
|
||||
.create_local_account(&user_id, Some(HashedPassword::new(password)?), None)
|
||||
.set_displayname(&user_id, Some(displayname));
|
||||
|
||||
// Initial account data
|
||||
self.services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
&user_id,
|
||||
ruma::events::GlobalAccountDataEventType::PushRules
|
||||
.to_string()
|
||||
.into(),
|
||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent::new(
|
||||
ruma::events::push_rules::PushRulesEventContent::new(
|
||||
ruma::push::Ruleset::server_default(&user_id),
|
||||
),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!("Created user {user_id} with password `{password}`"))
|
||||
.await
|
||||
if !self.services.server.config.auto_join_rooms.is_empty() {
|
||||
for room in &self.services.server.config.auto_join_rooms {
|
||||
let Ok(room_id) = self.services.rooms.alias.resolve(room).await else {
|
||||
error!(
|
||||
%user_id,
|
||||
"Failed to resolve room alias to room ID when attempting to auto join {room}, skipping"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
if !self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(self.services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Skipping room {room} to automatically join as we have never joined \
|
||||
before."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_server_name) = room.server_name() {
|
||||
match self
|
||||
.services
|
||||
.rooms
|
||||
.membership
|
||||
.join_room(
|
||||
&user_id,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[
|
||||
self.services.globals.server_name().to_owned(),
|
||||
room_server_name.to_owned(),
|
||||
],
|
||||
)
|
||||
.await
|
||||
{
|
||||
| Ok(_response) => {
|
||||
info!("Automatically joined room {room} for user {user_id}");
|
||||
},
|
||||
| Err(e) => {
|
||||
// don't return this error so we don't fail registrations
|
||||
error!(
|
||||
"Failed to automatically join room {room} for user {user_id}: \
|
||||
{e}"
|
||||
);
|
||||
self.services
|
||||
.admin
|
||||
.send_text(&format!(
|
||||
"Failed to automatically join room {room} for user \
|
||||
{user_id}: {e}"
|
||||
))
|
||||
.await;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we dont add a device since we're not the user, just the creator
|
||||
|
||||
// Make the first user to register an administrator and disable first-run mode.
|
||||
self.services.firstrun.empower_first_user(&user_id).await?;
|
||||
|
||||
self.write_str(&format!(
|
||||
"Created user with user_id: {user_id} and password: `{password}`"
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) -> Result {
|
||||
@@ -103,12 +218,15 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
|
||||
|
||||
pub(super) async fn suspend(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
|
||||
if user_id == self.services.globals.server_user {
|
||||
return Err!("Not allowed to suspend 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 suspended.");
|
||||
}
|
||||
@@ -124,12 +242,15 @@ pub(super) async fn suspend(&self, user_id: String) -> Result {
|
||||
|
||||
pub(super) async fn unsuspend(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
|
||||
if user_id == self.services.globals.server_user {
|
||||
return Err!("Not allowed to unsuspend the server service account.",);
|
||||
}
|
||||
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} does not exist.");
|
||||
}
|
||||
self.services.users.unsuspend_account(&user_id).await;
|
||||
|
||||
self.write_str(&format!("User {user_id} has been unsuspended."))
|
||||
@@ -141,7 +262,6 @@ pub(super) async fn reset_password(
|
||||
logout: bool,
|
||||
username: String,
|
||||
password: Option<String>,
|
||||
convert_to_local_account: bool,
|
||||
) -> Result {
|
||||
let user_id = parse_local_user_id(self.services, &username)?;
|
||||
|
||||
@@ -154,37 +274,15 @@ pub(super) async fn reset_password(
|
||||
|
||||
let new_password =
|
||||
password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
|
||||
let new_password_hash = HashedPassword::new(&new_password)?;
|
||||
|
||||
if convert_to_local_account {
|
||||
self.services
|
||||
.users
|
||||
.convert_to_local_account(&user_id, new_password_hash)
|
||||
.await?;
|
||||
} else {
|
||||
match self.services.users.status(&user_id).await {
|
||||
| AccountStatus::Active if !self.services.users.is_shadow(&user_id).await => {
|
||||
self.services
|
||||
.users
|
||||
.set_password(&user_id, new_password_hash)
|
||||
.await?;
|
||||
},
|
||||
| AccountStatus::NotFound => {
|
||||
return Err!("The provided user does not exist.");
|
||||
},
|
||||
| _ => {
|
||||
return Err!(
|
||||
"The provided user is a shadow or deactivated account. To convert it to \
|
||||
a local account, pass the --convert-to-local-account flag."
|
||||
);
|
||||
},
|
||||
}
|
||||
self.services
|
||||
.users
|
||||
.set_password(&user_id, Some(HashedPassword::new(&new_password)?));
|
||||
|
||||
self.write_str(&format!(
|
||||
"Successfully reset the password for user {user_id}: `{new_password}`"
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
self.write_str(&format!(
|
||||
"Successfully reset the password for user {user_id}: `{new_password}`"
|
||||
))
|
||||
.await?;
|
||||
|
||||
if logout {
|
||||
self.services
|
||||
@@ -203,6 +301,30 @@ pub(super) async fn reset_password(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn issue_password_reset_link(&self, username: String) -> Result {
|
||||
use conduwuit_service::password_reset::{PASSWORD_RESET_PATH, RESET_TOKEN_QUERY_PARAM};
|
||||
|
||||
self.bail_restricted()?;
|
||||
|
||||
let mut reset_url = self
|
||||
.services
|
||||
.config
|
||||
.get_client_domain()
|
||||
.join(PASSWORD_RESET_PATH)
|
||||
.unwrap();
|
||||
|
||||
let user_id = parse_local_user_id(self.services, &username)?;
|
||||
let token = self.services.password_reset.issue_token(user_id).await?;
|
||||
reset_url
|
||||
.query_pairs_mut()
|
||||
.append_pair(RESET_TOKEN_QUERY_PARAM, &token.token);
|
||||
|
||||
self.write_str(&format!("Password reset link issued for {username}: {reset_url}"))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result {
|
||||
if self.body.len() < 2
|
||||
|| !self.body[0].trim().starts_with("```")
|
||||
@@ -936,16 +1058,21 @@ pub(super) async fn force_leave_remote_room(
|
||||
|
||||
pub(super) async fn lock(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
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())
|
||||
@@ -957,8 +1084,11 @@ pub(super) async fn lock(&self, user_id: String) -> Result {
|
||||
|
||||
pub(super) async fn unlock(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
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."))
|
||||
@@ -967,16 +1097,21 @@ pub(super) async fn unlock(&self, user_id: String) -> Result {
|
||||
|
||||
pub(super) async fn logout(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
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)
|
||||
@@ -993,12 +1128,18 @@ pub(super) async fn logout(&self, user_id: String) -> Result {
|
||||
|
||||
pub(super) async fn disable_login(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
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.");
|
||||
}
|
||||
@@ -1012,8 +1153,14 @@ pub(super) async fn disable_login(&self, user_id: String) -> Result {
|
||||
|
||||
pub(super) async fn enable_login(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
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
|
||||
@@ -1121,8 +1268,10 @@ pub(super) async fn change_email(&self, user_id: String, email: Option<String>)
|
||||
}
|
||||
|
||||
pub(super) async fn reset_push_rules(&self, user_id: String) -> Result {
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
if !self.services.users.is_active(&user_id).await {
|
||||
return Err!("User is not active.");
|
||||
}
|
||||
recreate_push_rules_and_return(self.services, &user_id).await?;
|
||||
self.write_str("Reset user's push rules to the server default.")
|
||||
.await
|
||||
|
||||
@@ -27,9 +27,12 @@ pub enum UserCommand {
|
||||
username: String,
|
||||
/// New password for the user, if unspecified one is generated
|
||||
password: Option<String>,
|
||||
},
|
||||
|
||||
#[arg(long)]
|
||||
convert_to_local_account: bool,
|
||||
/// Issue a self-service password reset link for a user.
|
||||
IssuePasswordResetLink {
|
||||
/// Username of the user who may use the link
|
||||
username: String,
|
||||
},
|
||||
|
||||
/// Get a user's associated email address.
|
||||
|
||||
+8
-1
@@ -54,7 +54,14 @@ pub(crate) async fn parse_active_local_user_id(
|
||||
user_id: &str,
|
||||
) -> Result<OwnedUserId> {
|
||||
let user_id = parse_local_user_id(services, user_id)?;
|
||||
services.users.status(&user_id).await.ensure_active()?;
|
||||
|
||||
if !services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id:?} does not exist on this server.");
|
||||
}
|
||||
|
||||
if services.users.is_deactivated(&user_id).await? {
|
||||
return Err!("User {user_id:?} is deactivated.");
|
||||
}
|
||||
|
||||
Ok(user_id)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
},
|
||||
};
|
||||
use service::{mailer::messages, uiaa::UiaaInitiator, users::HashedPassword};
|
||||
use service::{mailer::messages, uiaa::Identity, users::HashedPassword};
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||
use crate::{Ruma, router::ClientIdentity};
|
||||
@@ -49,16 +49,39 @@ pub(crate) async fn get_register_available_route(
|
||||
ClientIp(client): ClientIp,
|
||||
body: Ruma<get_username_availability::v3::Request>,
|
||||
) -> Result<get_username_availability::v3::Response> {
|
||||
let _ = services
|
||||
.users
|
||||
.determine_registration_user_id(
|
||||
Some(body.username.clone()),
|
||||
None,
|
||||
body.identity
|
||||
.as_ref()
|
||||
.and_then(ClientIdentity::appservice_info),
|
||||
)
|
||||
.await?;
|
||||
// Validate user id
|
||||
let user_id =
|
||||
match UserId::parse_with_server_name(&body.username, services.globals.server_name()) {
|
||||
| Ok(user_id) => {
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {} contains disallowed characters or spaces: {e}",
|
||||
body.username
|
||||
))));
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {} is not valid: {e}",
|
||||
body.username
|
||||
))));
|
||||
},
|
||||
};
|
||||
|
||||
// Check if username is creative enough
|
||||
if services.users.exists(&user_id).await {
|
||||
return Err!(Request(UserInUse("User ID is not available.")));
|
||||
}
|
||||
|
||||
if let Some(ClientIdentity::Appservice { appservice_info, .. }) = &body.identity
|
||||
&& !appservice_info.is_user_match(&user_id)
|
||||
{
|
||||
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
|
||||
} else if services.appservice.is_exclusive_user_id(&user_id).await {
|
||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||
}
|
||||
|
||||
Ok(get_username_availability::v3::Response::new(true))
|
||||
}
|
||||
@@ -86,7 +109,12 @@ pub(crate) async fn change_password_route(
|
||||
ClientIp(client): ClientIp,
|
||||
body: Ruma<change_password::v3::Request>,
|
||||
) -> Result<change_password::v3::Response> {
|
||||
let identity = if let Some(identity) = body.identity.as_ref() {
|
||||
let identity = if let Some(user_id) = body
|
||||
.identity
|
||||
.as_ref()
|
||||
.map(ClientIdentity::expect_sender_user)
|
||||
.transpose()?
|
||||
{
|
||||
// A signed-in user is trying to change their password, prompt them for their
|
||||
// existing one
|
||||
|
||||
@@ -96,10 +124,7 @@ pub(crate) async fn change_password_route(
|
||||
&body.auth,
|
||||
vec![AuthFlow::new(vec![AuthType::Password])],
|
||||
Box::default(),
|
||||
Some(UiaaInitiator::new(
|
||||
identity.expect_sender_user()?,
|
||||
identity.sender_device(),
|
||||
)),
|
||||
Some(Identity::from_user_id(user_id)),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
@@ -128,8 +153,7 @@ pub(crate) async fn change_password_route(
|
||||
|
||||
services
|
||||
.users
|
||||
.set_password(&sender_user, HashedPassword::new(&body.new_password)?)
|
||||
.await?;
|
||||
.set_password(&sender_user, Some(HashedPassword::new(&body.new_password)?));
|
||||
|
||||
if body.logout_devices {
|
||||
// Logout all devices except the current one
|
||||
@@ -256,24 +280,16 @@ pub(crate) async fn deactivate_route(
|
||||
) -> Result<deactivate::v3::Response> {
|
||||
// Authentication for this endpoint is technically optional,
|
||||
// but we require the user to be logged in
|
||||
let identity = body
|
||||
let sender_user = body
|
||||
.identity
|
||||
.as_ref()
|
||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
||||
|
||||
let sender_user = identity.expect_sender_user()?;
|
||||
|
||||
if !services.config.allow_deactivation {
|
||||
return Err!(Request(Forbidden(
|
||||
"You may not deactivate your own account. Contact your server's administrator for \
|
||||
assistance."
|
||||
)));
|
||||
}
|
||||
.map(ClientIdentity::expect_sender_user)
|
||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))??;
|
||||
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, sender_user, identity.sender_device(), None)
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
|
||||
// Remove profile pictures and display name
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, fmt::Write};
|
||||
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::ClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, debug_info, info,
|
||||
Err, Result, debug_info, error, info,
|
||||
utils::{self},
|
||||
warn,
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
use futures::StreamExt;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use lettre::{Address, message::Mailbox};
|
||||
use ruma::{
|
||||
OwnedUserId, UserId,
|
||||
api::client::{
|
||||
account::{
|
||||
register::{self, LoginType, RegistrationKind},
|
||||
@@ -18,6 +20,11 @@
|
||||
uiaa::{AuthFlow, AuthType},
|
||||
},
|
||||
assign,
|
||||
events::{
|
||||
GlobalAccountDataEventType, push_rules::PushRulesEvent,
|
||||
room::message::RoomMessageEventContent,
|
||||
},
|
||||
push,
|
||||
};
|
||||
use serde_json::value::RawValue;
|
||||
use service::{mailer::messages, users::HashedPassword};
|
||||
@@ -25,6 +32,8 @@
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||
use crate::Ruma;
|
||||
|
||||
const RANDOM_USER_ID_LENGTH: usize = 10;
|
||||
|
||||
/// # `POST /_matrix/client/v3/register`
|
||||
///
|
||||
/// Register an account on this homeserver.
|
||||
@@ -43,6 +52,8 @@ pub(crate) async fn register_route(
|
||||
return Err!(Request(GuestAccessForbidden("Guests may not register on this server.")));
|
||||
}
|
||||
|
||||
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||
|
||||
// Allow registration if it's enabled in the config file or if this is the first
|
||||
// run (so the first user account can be created)
|
||||
let allow_registration =
|
||||
@@ -60,59 +71,99 @@ pub(crate) async fn register_route(
|
||||
)));
|
||||
}
|
||||
|
||||
let user_id = if body.body.login_type == Some(LoginType::ApplicationService) {
|
||||
let Some(appservice_info) = &body.identity else {
|
||||
return Err!(Request(Forbidden(
|
||||
"Only appservices can use the appservice login type."
|
||||
)));
|
||||
};
|
||||
|
||||
let user_id = services
|
||||
.users
|
||||
.determine_registration_user_id(body.username.clone(), None, Some(appservice_info))
|
||||
.await?;
|
||||
|
||||
services.users.create_shadow_account(&user_id).await?;
|
||||
|
||||
user_id
|
||||
let identity = if body.identity.is_some() {
|
||||
// Appservices can skip auth
|
||||
None
|
||||
} else {
|
||||
// Perform UIAA to determine the user's identity
|
||||
let (flows, params) = create_registration_uiaa_session(&services).await?;
|
||||
|
||||
let identity = services
|
||||
.uiaa
|
||||
.authenticate(&body.auth, flows, params, None)
|
||||
.await?;
|
||||
|
||||
let password = if let Some(password) = &body.password {
|
||||
HashedPassword::new(password)?
|
||||
} else {
|
||||
return Err!(Request(InvalidParam("A password must be provided.")));
|
||||
};
|
||||
|
||||
let user_id = services
|
||||
.users
|
||||
.determine_registration_user_id(body.username.clone(), identity.email.as_ref(), None)
|
||||
.await?;
|
||||
|
||||
services
|
||||
.users
|
||||
.create_local_account(&user_id, Some(password), identity.email)
|
||||
.await?;
|
||||
|
||||
user_id
|
||||
Some(
|
||||
services
|
||||
.uiaa
|
||||
.authenticate(&body.auth, flows, params, None)
|
||||
.await?,
|
||||
)
|
||||
};
|
||||
|
||||
let (token, device) = if !body.inhibit_login {
|
||||
// If UIAA is disabled, we can't create a device. In that case only appservices
|
||||
// can reach this point in the first place, so we return an error for them.
|
||||
if !services.config.oauth.compatibility_mode().uiaa_available() {
|
||||
return Err!(Request(AppserviceLoginUnsupported(
|
||||
"User-interactive appservice registration is not available on this server."
|
||||
)));
|
||||
// If the user didn't supply a username but did supply an email, use
|
||||
// the email's user as their initial localpart to avoid falling back to
|
||||
// a randomly generated localpart
|
||||
let supplied_username = body.username.clone().or_else(|| {
|
||||
if let Some(identity) = &identity
|
||||
&& let Some(email) = &identity.email
|
||||
{
|
||||
Some(email.user().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// Generate new device id if the user didn't specify one
|
||||
let user_id =
|
||||
determine_registration_user_id(&services, supplied_username, emergency_mode_enabled)
|
||||
.await?;
|
||||
|
||||
if body.body.login_type == Some(LoginType::ApplicationService) {
|
||||
// For appservice logins, make sure that the user ID is in the appservice's
|
||||
// namespace
|
||||
|
||||
match body.identity {
|
||||
| Some(ref info) =>
|
||||
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
|
||||
return Err!(Request(Exclusive(
|
||||
"Username is not in an appservice namespace."
|
||||
)));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(MissingToken("Missing appservice token.")));
|
||||
},
|
||||
}
|
||||
} else if services.appservice.is_exclusive_user_id(&user_id).await && !emergency_mode_enabled
|
||||
{
|
||||
// For non-appservice logins, ban user IDs which are in an appservice's
|
||||
// namespace (unless emergency mode is enabled)
|
||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||
}
|
||||
|
||||
let password = if body.identity.is_some() {
|
||||
None
|
||||
} else if let Some(password) = body.password.as_deref() {
|
||||
Some(HashedPassword::new(password)?)
|
||||
} else {
|
||||
return Err!(Request(InvalidParam("A password must be provided")));
|
||||
};
|
||||
|
||||
// Create user
|
||||
services.users.create(&user_id, password)?;
|
||||
|
||||
// Set an initial display name
|
||||
let mut displayname = user_id.localpart().to_owned();
|
||||
|
||||
// Apply the new user displayname suffix, if it's set
|
||||
if !services.globals.new_user_displayname_suffix().is_empty() && body.identity.is_none() {
|
||||
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_displayname(&user_id, Some(displayname.clone()));
|
||||
|
||||
// Initial account data
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
&user_id,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(PushRulesEvent::new(
|
||||
push::Ruleset::server_default(&user_id).into(),
|
||||
))
|
||||
.expect("should be able to serialize push rules"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Generate new device id if the user didn't specify one
|
||||
let (token, device) = if !body.inhibit_login {
|
||||
let device_id = body
|
||||
.device_id
|
||||
.clone()
|
||||
@@ -128,7 +179,6 @@ pub(crate) async fn register_route(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&new_token,
|
||||
None,
|
||||
body.initial_device_display_name.clone(),
|
||||
Some(client.to_string()),
|
||||
)
|
||||
@@ -139,7 +189,118 @@ pub(crate) async fn register_route(
|
||||
(None, None)
|
||||
};
|
||||
|
||||
debug_info!(%user_id, ?device, "New account created via legacy registration");
|
||||
debug_info!(%user_id, ?device, "User account was created");
|
||||
|
||||
// If the user registered with an email, associate it with their account.
|
||||
if let Some(identity) = identity
|
||||
&& let Some(email) = identity.email
|
||||
{
|
||||
// This may fail if the email is already in use, but we already check for that
|
||||
// in `/requestToken`, so ignoring the error is acceptable here in the rare case
|
||||
// that an email is sniped by another user between the `/requestToken` request
|
||||
// and the `/register` request.
|
||||
let _ = services
|
||||
.threepid
|
||||
.associate_localpart_email(user_id.localpart(), &email)
|
||||
.await;
|
||||
}
|
||||
|
||||
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
|
||||
|
||||
if body.identity.is_none() {
|
||||
if !device_display_name.is_empty() {
|
||||
let notice = format!(
|
||||
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
||||
display name \"{device_display_name}\""
|
||||
);
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
} else {
|
||||
let notice = format!("New user \"{user_id}\" registered on this server.");
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make the first user to register an administrator and disable first-run mode.
|
||||
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
|
||||
|
||||
// If the registering user was not the first and we're suspending users on
|
||||
// register, suspend them.
|
||||
if !was_first_user && services.config.suspend_on_register {
|
||||
// Note that we can still do auto joins for suspended users
|
||||
services
|
||||
.users
|
||||
.suspend_account(&user_id, &services.globals.server_user)
|
||||
.await;
|
||||
// And send an @room notice to the admin room, to prompt admins to review the
|
||||
// new user and ideally unsuspend them if deemed appropriate.
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} has been suspended as they are not the first user on this \
|
||||
server. Please review and unsuspend them if appropriate."
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
if body.identity.is_none() && !services.server.config.auto_join_rooms.is_empty() {
|
||||
for room in &services.server.config.auto_join_rooms {
|
||||
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
||||
error!(
|
||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||
{room}, skipping"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Skipping room {room} to automatically join as we have never joined before."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_server_name) = room.server_name() {
|
||||
match services
|
||||
.rooms
|
||||
.membership
|
||||
.join_room(
|
||||
&user_id,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Err(e) => {
|
||||
// don't return this error so we don't fail registrations
|
||||
error!(
|
||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
||||
);
|
||||
},
|
||||
| _ => {
|
||||
info!("Automatically joined room {room} for user {user_id}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(assign!(register::v3::Response::new(user_id), {
|
||||
access_token: token,
|
||||
@@ -211,21 +372,21 @@ async fn create_registration_uiaa_session(
|
||||
|
||||
// Require all users to agree to the terms and conditions, if configured
|
||||
let terms = &services.config.registration_terms;
|
||||
if !terms.documents.is_empty() {
|
||||
let mut terms_map = HashMap::new();
|
||||
if !terms.is_empty() {
|
||||
let mut terms =
|
||||
serde_json::to_value(terms.clone()).expect("failed to serialize terms");
|
||||
|
||||
for (id, document) in &terms.documents {
|
||||
terms_map.insert(id.to_owned(), serde_json::json!({
|
||||
terms.language.clone(): serde_json::to_value(document).expect("should be able to serialize document")
|
||||
}));
|
||||
// Insert a dummy `version` field
|
||||
for (_, documents) in terms.as_object_mut().unwrap() {
|
||||
let documents = documents.as_object_mut().unwrap();
|
||||
|
||||
documents.insert("version".to_owned(), "latest".into());
|
||||
}
|
||||
|
||||
terms_map.insert("version".to_owned(), "latest".into());
|
||||
|
||||
params.insert(
|
||||
AuthType::Terms.as_str().to_owned(),
|
||||
serde_json::json!({
|
||||
"policies": terms_map,
|
||||
"policies": terms,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -258,6 +419,81 @@ async fn create_registration_uiaa_session(
|
||||
Ok((flows, params))
|
||||
}
|
||||
|
||||
async fn determine_registration_user_id(
|
||||
services: &Services,
|
||||
supplied_username: Option<String>,
|
||||
emergency_mode_enabled: bool,
|
||||
) -> Result<OwnedUserId> {
|
||||
if let Some(supplied_username) = supplied_username {
|
||||
// The user gets to pick their username. Do some validation to make sure it's
|
||||
// acceptable.
|
||||
|
||||
// Don't allow registration with forbidden usernames.
|
||||
if services
|
||||
.globals
|
||||
.forbidden_usernames()
|
||||
.is_match(&supplied_username)
|
||||
&& !emergency_mode_enabled
|
||||
{
|
||||
return Err!(Request(Forbidden("Username is forbidden")));
|
||||
}
|
||||
|
||||
// Create and validate the user ID
|
||||
let user_id = match UserId::parse_with_server_name(
|
||||
&supplied_username,
|
||||
services.globals.server_name(),
|
||||
) {
|
||||
| Ok(user_id) => {
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
// Unless we are in emergency mode, we should follow synapse's behaviour on
|
||||
// not allowing things like spaces and UTF-8 characters in usernames
|
||||
if !emergency_mode_enabled {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {supplied_username} contains disallowed characters or \
|
||||
spaces: {e}"
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow registration with user IDs that aren't local
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
return Err!(Request(InvalidUsername(
|
||||
"Username {supplied_username} is not local to this server"
|
||||
)));
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {supplied_username} is not valid: {e}"
|
||||
))));
|
||||
},
|
||||
};
|
||||
|
||||
if services.users.exists(&user_id).await {
|
||||
return Err!(Request(UserInUse("User ID is not available.")));
|
||||
}
|
||||
|
||||
Ok(user_id)
|
||||
} else {
|
||||
// The user didn't specify a username. Generate a username for
|
||||
// them.
|
||||
|
||||
loop {
|
||||
let user_id = UserId::parse_with_server_name(
|
||||
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if !services.users.exists(&user_id).await {
|
||||
break Ok(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/register/email/requestToken`
|
||||
///
|
||||
/// Requests a validation email for the purpose of registering a new account.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
},
|
||||
thirdparty::{Medium, ThirdPartyIdentifierInit},
|
||||
};
|
||||
use service::mailer::messages;
|
||||
use service::{mailer::messages, uiaa::Identity};
|
||||
|
||||
use crate::{Ruma, router::ClientIdentity};
|
||||
|
||||
@@ -25,10 +25,6 @@ pub(crate) async fn third_party_route(
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
let mut threepids = vec![];
|
||||
|
||||
if !services.threepid.email_requirement().may_view() {
|
||||
return Ok(get_3pids::v3::Response::new(vec![]));
|
||||
}
|
||||
|
||||
if let Some(email) = services
|
||||
.threepid
|
||||
.get_email_for_localpart(sender_user.localpart())
|
||||
@@ -128,18 +124,15 @@ pub(crate) async fn add_3pid_route(
|
||||
.uiaa
|
||||
.authenticate_password(
|
||||
&body.auth,
|
||||
body.identity.expect_sender_user()?,
|
||||
body.identity.sender_device(),
|
||||
None,
|
||||
Some(Identity::from_user_id(body.identity.expect_sender_user()?)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let email = services
|
||||
.threepid
|
||||
.get_valid_session(&body.sid, &body.client_secret)
|
||||
.consume_valid_session(&body.sid, &body.client_secret)
|
||||
.await
|
||||
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?
|
||||
.consume();
|
||||
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?;
|
||||
|
||||
services
|
||||
.threepid
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::future::{join, join3};
|
||||
use ruma::api::client::admin::{is_user_locked, lock_user};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_matrix/client/v1/admin/lock/{userId}`
|
||||
///
|
||||
/// Check the account lock status of a target user
|
||||
pub(crate) async fn get_lock_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<is_user_locked::v1::Request>,
|
||||
) -> Result<is_user_locked::v1::Response> {
|
||||
let (admin, status) = join(
|
||||
services.users.is_admin(body.identity.expect_sender_user()?),
|
||||
services.users.status(&body.user_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
if !admin {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
status.ensure_active()?;
|
||||
|
||||
Ok(is_user_locked::v1::Response::new(
|
||||
services.users.is_locked(&body.user_id).await?,
|
||||
))
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/v1/admin/lock/{userId}`
|
||||
///
|
||||
/// Set the account lock status of a target user
|
||||
pub(crate) async fn put_lock_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<lock_user::v1::Request>,
|
||||
) -> Result<lock_user::v1::Response> {
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
|
||||
let (sender_admin, status, target_admin) = join3(
|
||||
services.users.is_admin(sender_user),
|
||||
services.users.status(&body.user_id),
|
||||
services.users.is_admin(&body.user_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
if !sender_admin {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
status.ensure_active()?;
|
||||
|
||||
if body.user_id == *sender_user {
|
||||
return Err!(Request(Forbidden("You cannot lock yourself")));
|
||||
}
|
||||
|
||||
if target_admin {
|
||||
return Err!(Request(Forbidden("You cannot lock another server administrator")));
|
||||
}
|
||||
|
||||
if services.users.is_locked(&body.user_id).await? == body.locked {
|
||||
// No change
|
||||
return Ok(lock_user::v1::Response::new(body.locked));
|
||||
}
|
||||
|
||||
let action = if body.locked {
|
||||
services
|
||||
.users
|
||||
.suspend_account(&body.user_id, sender_user)
|
||||
.await;
|
||||
"locked"
|
||||
} else {
|
||||
services.users.unsuspend_account(&body.user_id).await;
|
||||
"unlocked"
|
||||
};
|
||||
|
||||
if services.config.admin_room_notices {
|
||||
// Notify the admin room that an account has been un/suspended
|
||||
services
|
||||
.admin
|
||||
.send_text(&format!("{} has been {} by {}.", body.user_id, action, sender_user))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(lock_user::v1::Response::new(body.locked))
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
mod lock;
|
||||
mod suspend;
|
||||
|
||||
pub(crate) use self::{lock::*, suspend::*};
|
||||
pub(crate) use self::suspend::*;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::future::{join, join3};
|
||||
use ruma::api::client::admin::{is_user_suspended, suspend_user};
|
||||
use ruminuwuity::admin::{get_suspended, set_suspended};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -10,21 +10,23 @@
|
||||
/// Check the suspension status of a target user
|
||||
pub(crate) async fn get_suspended_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<is_user_suspended::v1::Request>,
|
||||
) -> Result<is_user_suspended::v1::Response> {
|
||||
let (admin, status) = join(
|
||||
body: Ruma<get_suspended::v1::Request>,
|
||||
) -> Result<get_suspended::v1::Response> {
|
||||
let (admin, active) = join(
|
||||
services.users.is_admin(body.identity.expect_sender_user()?),
|
||||
services.users.status(&body.user_id),
|
||||
services.users.is_active(&body.user_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
if !admin {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
status.ensure_active()?;
|
||||
|
||||
Ok(is_user_suspended::v1::Response::new(
|
||||
if !services.globals.user_is_local(&body.user_id) {
|
||||
return Err!(Request(InvalidParam("Can only check the suspended status of local users")));
|
||||
}
|
||||
if !active {
|
||||
return Err!(Request(NotFound("Unknown user")));
|
||||
}
|
||||
Ok(get_suspended::v1::Response::new(
|
||||
services.users.is_suspended(&body.user_id).await?,
|
||||
))
|
||||
}
|
||||
@@ -34,13 +36,13 @@ pub(crate) async fn get_suspended_status(
|
||||
/// Set the suspension status of a target user
|
||||
pub(crate) async fn put_suspended_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<suspend_user::v1::Request>,
|
||||
) -> Result<suspend_user::v1::Response> {
|
||||
body: Ruma<set_suspended::v1::Request>,
|
||||
) -> Result<set_suspended::v1::Response> {
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
|
||||
let (sender_admin, status, target_admin) = join3(
|
||||
let (sender_admin, active, target_admin) = join3(
|
||||
services.users.is_admin(sender_user),
|
||||
services.users.status(&body.user_id),
|
||||
services.users.is_active(&body.user_id),
|
||||
services.users.is_admin(&body.user_id),
|
||||
)
|
||||
.await;
|
||||
@@ -48,20 +50,21 @@ pub(crate) async fn put_suspended_status(
|
||||
if !sender_admin {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
status.ensure_active()?;
|
||||
|
||||
if !services.globals.user_is_local(&body.user_id) {
|
||||
return Err!(Request(InvalidParam("Can only set the suspended status of local users")));
|
||||
}
|
||||
if !active {
|
||||
return Err!(Request(NotFound("Unknown user")));
|
||||
}
|
||||
if body.user_id == *sender_user {
|
||||
return Err!(Request(Forbidden("You cannot suspend yourself")));
|
||||
}
|
||||
|
||||
if target_admin {
|
||||
return Err!(Request(Forbidden("You cannot suspend another server administrator")));
|
||||
}
|
||||
|
||||
if services.users.is_suspended(&body.user_id).await? == body.suspended {
|
||||
// No change
|
||||
return Ok(suspend_user::v1::Response::new(body.suspended));
|
||||
return Ok(set_suspended::v1::Response::new(body.suspended));
|
||||
}
|
||||
|
||||
let action = if body.suspended {
|
||||
@@ -83,5 +86,5 @@ pub(crate) async fn put_suspended_status(
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(suspend_user::v1::Response::new(body.suspended))
|
||||
Ok(set_suspended::v1::Response::new(body.suspended))
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
api::client::discovery::get_capabilities::{
|
||||
self,
|
||||
v3::{
|
||||
Capabilities, GetLoginTokenCapability, ProfileFieldsCapability, RoomVersionStability,
|
||||
RoomVersionsCapability, ThirdPartyIdChangesCapability,
|
||||
Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability,
|
||||
ThirdPartyIdChangesCapability,
|
||||
},
|
||||
},
|
||||
assign,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -40,20 +40,22 @@ pub(crate) async fn get_capabilities_route(
|
||||
capabilities.get_login_token =
|
||||
GetLoginTokenCapability::new(services.server.config.login_via_existing_session);
|
||||
|
||||
capabilities.forget_forced_upon_leave.enabled = true;
|
||||
// MSC4133 capability
|
||||
capabilities.set("uk.tcpip.msc4133.profile_fields", json!({"enabled": true}))?;
|
||||
|
||||
capabilities.set(
|
||||
"org.matrix.msc4267.forget_forced_upon_leave",
|
||||
json!({"enabled": services.config.forget_forced_upon_leave}),
|
||||
)?;
|
||||
|
||||
if services
|
||||
.users
|
||||
.is_admin(body.identity.expect_sender_user()?)
|
||||
.await
|
||||
{
|
||||
capabilities.account_moderation.lock = true;
|
||||
capabilities.account_moderation.suspend = true;
|
||||
// Advertise suspension API
|
||||
capabilities.set("uk.timedout.msc4323", json!({"suspend": true, "lock": false}))?;
|
||||
}
|
||||
|
||||
capabilities.profile_fields = Some(
|
||||
assign!(ProfileFieldsCapability::new(true), { disallowed: Some(services.oidc.restricted_profile_fields()) }),
|
||||
);
|
||||
|
||||
Ok(get_capabilities::v3::Response::new(capabilities))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
self, delete_device, delete_devices, get_device, get_devices, update_device,
|
||||
},
|
||||
};
|
||||
use service::uiaa::Identity;
|
||||
|
||||
use crate::{Ruma, client::DEVICE_ID_LENGTH};
|
||||
|
||||
@@ -94,7 +95,6 @@ pub(crate) async fn update_device_route(
|
||||
&device_id,
|
||||
&appservice.registration.as_token,
|
||||
None,
|
||||
None,
|
||||
Some(client.to_string()),
|
||||
)
|
||||
.await?;
|
||||
@@ -119,15 +119,14 @@ pub(crate) async fn delete_device_route(
|
||||
body: Ruma<delete_device::v3::Request>,
|
||||
) -> Result<delete_device::v3::Response> {
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
let appservice = body.identity.appservice_info();
|
||||
|
||||
// Appservices get to skip UIAA for this endpoint
|
||||
if !body.identity.is_appservice() {
|
||||
let sender_device = body.identity.expect_sender_device()?;
|
||||
|
||||
if appservice.is_none() {
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, sender_user, Some(sender_device), None)
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -156,13 +155,14 @@ pub(crate) async fn delete_devices_route(
|
||||
body: Ruma<delete_devices::v3::Request>,
|
||||
) -> Result<delete_devices::v3::Response> {
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
let appservice = body.identity.appservice_info();
|
||||
|
||||
// Appservices get to skip UIAA for this endpoint
|
||||
if let Some(sender_device) = body.identity.sender_device() {
|
||||
if appservice.is_none() {
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, sender_user, Some(sender_device), None)
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
+2
-14
@@ -26,7 +26,7 @@
|
||||
serde::Raw,
|
||||
};
|
||||
use serde_json::json;
|
||||
use service::oauth::OAuthTicket;
|
||||
use service::uiaa::Identity;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -197,7 +197,6 @@ pub(crate) async fn upload_signing_keys_route(
|
||||
if uiaa_needed_to_upload_keys(
|
||||
services,
|
||||
sender_user,
|
||||
body.identity.is_appservice(),
|
||||
body.self_signing_key.as_ref(),
|
||||
body.user_signing_key.as_ref(),
|
||||
body.master_key.as_ref(),
|
||||
@@ -206,12 +205,7 @@ pub(crate) async fn upload_signing_keys_route(
|
||||
{
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(
|
||||
&body.auth,
|
||||
sender_user,
|
||||
body.identity.sender_device(),
|
||||
Some(OAuthTicket::CrossSigningReset),
|
||||
)
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -232,16 +226,10 @@ pub(crate) async fn upload_signing_keys_route(
|
||||
async fn uiaa_needed_to_upload_keys(
|
||||
services: crate::State,
|
||||
user_id: &UserId,
|
||||
is_appservice: bool,
|
||||
self_signing_key: Option<&Raw<CrossSigningKey>>,
|
||||
user_signing_key: Option<&Raw<CrossSigningKey>>,
|
||||
master_signing_key: Option<&Raw<CrossSigningKey>>,
|
||||
) -> bool {
|
||||
if is_appservice {
|
||||
// Appservices can skip UIAA for this endpoint
|
||||
return false;
|
||||
}
|
||||
|
||||
let (self_signing_key, user_signing_key, master_signing_key) = (
|
||||
self_signing_key.map(Raw::deserialize).flat_ok(),
|
||||
user_signing_key.map(Raw::deserialize).flat_ok(),
|
||||
|
||||
@@ -170,7 +170,9 @@ pub async fn leave_room(
|
||||
locally."
|
||||
);
|
||||
|
||||
// return the existing leave state, if one exists
|
||||
// return the existing leave state, if one exists. `mark_as_left` will then
|
||||
// update the `roomuserid_leftcount` table, making the leave come down sync
|
||||
// again.
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
@@ -205,8 +207,6 @@ pub async fn leave_room(
|
||||
.update_joined_count(room_id)
|
||||
.await;
|
||||
|
||||
services.sync.wake(user_id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -291,9 +291,7 @@ pub(crate) async fn is_ignored_pdu<Pdu>(
|
||||
{
|
||||
// exclude Synapse's dummy events from bloating up response bodies. clients
|
||||
// don't need to see this.
|
||||
if !services.config.send_dummy_events_to_clients
|
||||
&& event.kind().to_string() == "org.matrix.dummy_event"
|
||||
{
|
||||
if event.kind().to_string() == "org.matrix.dummy_event" {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
pub(super) mod membership;
|
||||
pub(super) mod message;
|
||||
pub(super) mod mutual_rooms;
|
||||
pub(super) mod oauth;
|
||||
pub(super) mod openid;
|
||||
pub(super) mod presence;
|
||||
pub(super) mod profile;
|
||||
@@ -62,7 +61,6 @@
|
||||
pub use membership::{leave_all_rooms, leave_room, remote_leave_room};
|
||||
pub(super) use message::*;
|
||||
pub(super) use mutual_rooms::*;
|
||||
pub(super) use oauth::*;
|
||||
pub(super) use openid::*;
|
||||
pub(super) use presence::*;
|
||||
pub(super) use profile::*;
|
||||
@@ -75,7 +73,6 @@
|
||||
pub(super) use room::*;
|
||||
pub(super) use search::*;
|
||||
pub(super) use send::*;
|
||||
pub use session::handle_login;
|
||||
pub(super) use session::*;
|
||||
pub(super) use space::*;
|
||||
pub(super) use state::*;
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Request, State},
|
||||
middleware::{self, Next},
|
||||
response::{IntoResponse, Response},
|
||||
routing::method_routing::{get, post},
|
||||
};
|
||||
use const_str::concat;
|
||||
use http::StatusCode;
|
||||
use serde_json::json;
|
||||
pub(crate) use server_metadata::*;
|
||||
|
||||
mod register_client;
|
||||
mod server_metadata;
|
||||
mod token;
|
||||
|
||||
const BASE_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/oauth2/");
|
||||
const AUTH_CODE_PATH: &str = "grant/authorization_code";
|
||||
const JWKS_URI_PATH: &str = "client/keys.json";
|
||||
const CLIENT_REGISTER_PATH: &str = "client/register";
|
||||
const TOKEN_REVOKE_PATH: &str = "client/revoke";
|
||||
const TOKEN_PATH: &str = "grant/token";
|
||||
const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink");
|
||||
|
||||
pub(crate) fn router(state: crate::State) -> Router<crate::State> {
|
||||
Router::new()
|
||||
.nest(BASE_PATH, oauth_router())
|
||||
.route(
|
||||
"/.well-known/openid-configuration",
|
||||
get(
|
||||
// TODO(unspecced): used by old versions of the matrix-js-sdk
|
||||
async |State(services): State<crate::State>| {
|
||||
Json(authorization_server_metadata(&services).await)
|
||||
},
|
||||
),
|
||||
)
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state,
|
||||
async |State(state): State<crate::State>, request: Request, next: Next| -> Response {
|
||||
if state.config.oauth.compatibility_mode().oauth_available() {
|
||||
next.run(request).await
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, "OAuth is unavailable on this server").into_response()
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn oauth_router() -> Router<crate::State> {
|
||||
Router::new()
|
||||
.route(concat!("/", CLIENT_REGISTER_PATH), post(register_client::register_client_route))
|
||||
// TODO(unspecced): used by old versions of the matrix-js-sdk
|
||||
.route(concat!("/", JWKS_URI_PATH), get(async || Json(json!({"keys": []}))))
|
||||
.route(concat!("/", TOKEN_PATH), post(token::token_route))
|
||||
.route(concat!("/", TOKEN_REVOKE_PATH), post(token::revoke_token_route))
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
use serde::Serialize;
|
||||
use service::oauth::client_metadata::ClientMetadata;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RegisteredClient {
|
||||
client_id: String,
|
||||
#[serde(flatten)]
|
||||
metadata: ClientMetadata,
|
||||
}
|
||||
|
||||
pub(crate) async fn register_client_route(
|
||||
State(services): State<crate::State>,
|
||||
Json(metadata): Json<ClientMetadata>,
|
||||
) -> Result<Response, Response> {
|
||||
let client_id = services
|
||||
.oauth
|
||||
.register_client(&metadata)
|
||||
.await
|
||||
.map_err(|err| (StatusCode::BAD_REQUEST, Json(err)).into_response())?;
|
||||
|
||||
Ok(Json(RegisteredClient { client_id, metadata }).into_response())
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use ruma::{
|
||||
api::client::discovery::get_authorization_server_metadata::{
|
||||
self, v1::AccountManagementAction,
|
||||
},
|
||||
serde::Raw,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use service::Services;
|
||||
|
||||
use crate::{
|
||||
Ruma,
|
||||
client::oauth::{
|
||||
ACCOUNT_MANAGEMENT_PATH, AUTH_CODE_PATH, CLIENT_REGISTER_PATH, JWKS_URI_PATH, TOKEN_PATH,
|
||||
TOKEN_REVOKE_PATH,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) async fn get_authorization_server_metadata_route(
|
||||
State(services): State<crate::State>,
|
||||
_body: Ruma<get_authorization_server_metadata::v1::Request>,
|
||||
) -> Result<get_authorization_server_metadata::v1::Response> {
|
||||
if !services.config.oauth.compatibility_mode().oauth_available() {
|
||||
return Err!(Request(Unrecognized("OAuth is unavailable on this server")));
|
||||
}
|
||||
|
||||
let metadata = Raw::new(&authorization_server_metadata(&services).await).unwrap();
|
||||
|
||||
Ok(get_authorization_server_metadata::v1::Response::new(metadata.cast_unchecked()))
|
||||
}
|
||||
|
||||
pub(crate) async fn authorization_server_metadata(services: &Services) -> Value {
|
||||
let endpoint_base = services
|
||||
.config
|
||||
.get_client_domain()
|
||||
.join(super::BASE_PATH)
|
||||
.unwrap();
|
||||
|
||||
json!({
|
||||
"account_management_uri": endpoint_base.join(ACCOUNT_MANAGEMENT_PATH).unwrap(),
|
||||
"account_management_actions_supported": [
|
||||
AccountManagementAction::AccountDeactivate,
|
||||
AccountManagementAction::CrossSigningReset,
|
||||
AccountManagementAction::DeviceDelete,
|
||||
AccountManagementAction::DeviceView,
|
||||
AccountManagementAction::DevicesList,
|
||||
AccountManagementAction::Profile,
|
||||
],
|
||||
"authorization_endpoint": endpoint_base.join(AUTH_CODE_PATH).unwrap(),
|
||||
"code_challenge_methods_supported": ["S256"],
|
||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||
"issuer": services.config.get_client_domain(),
|
||||
"jwks_uri": endpoint_base.join(JWKS_URI_PATH).unwrap(),
|
||||
"prompt_values_supported": ["create"],
|
||||
"registration_endpoint": endpoint_base.join(CLIENT_REGISTER_PATH).unwrap(),
|
||||
"response_modes_supported": ["query", "fragment"],
|
||||
"response_types_supported": ["code"],
|
||||
"revocation_endpoint": endpoint_base.join(TOKEN_REVOKE_PATH).unwrap(),
|
||||
"token_endpoint": endpoint_base.join(TOKEN_PATH).unwrap(),
|
||||
})
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use axum::{Form, Json, extract::State, response::IntoResponse};
|
||||
use http::StatusCode;
|
||||
use service::oauth::grant::{RevokeTokenRequest, TokenRequest};
|
||||
|
||||
pub(crate) async fn token_route(
|
||||
State(services): State<crate::State>,
|
||||
Form(request): Form<TokenRequest>,
|
||||
) -> impl IntoResponse {
|
||||
match services.oauth.issue_token(request).await {
|
||||
| Ok(response) => Ok(Json(response)),
|
||||
| Err(err) => Err((StatusCode::BAD_REQUEST, Json(err))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn revoke_token_route(
|
||||
State(services): State<crate::State>,
|
||||
Form(request): Form<RevokeTokenRequest>,
|
||||
) -> impl IntoResponse {
|
||||
match services.oauth.revoke_token(request.token).await {
|
||||
| Ok(()) => Ok(StatusCode::OK),
|
||||
| Err(err) => Err((StatusCode::BAD_REQUEST, Json(err))),
|
||||
}
|
||||
}
|
||||
+289
-58
@@ -1,8 +1,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use conduwuit::{Err, Result, matrix::pdu::PartialPdu, utils::to_canonical_object};
|
||||
use conduwuit_service::Services;
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
UserId,
|
||||
api::{
|
||||
@@ -12,10 +13,11 @@
|
||||
federation,
|
||||
},
|
||||
assign,
|
||||
events::room::member::MembershipState,
|
||||
presence::PresenceState,
|
||||
profile::{ProfileFieldName, ProfileFieldValue},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use service::users::ProfileFieldChange;
|
||||
use serde_json::{Value, to_value};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -63,24 +65,13 @@ pub(crate) async fn set_profile_field_route(
|
||||
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
|
||||
}
|
||||
|
||||
if services
|
||||
.oidc
|
||||
.restricted_profile_fields()
|
||||
.contains(&body.value.field_name())
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"This profile field is controlled by your identity provider."
|
||||
)));
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_profile_field(
|
||||
&body.user_id,
|
||||
ProfileFieldChange::Set(body.value.clone()),
|
||||
body.propagate_to.clone(),
|
||||
)
|
||||
.await?;
|
||||
set_profile_field(
|
||||
&services,
|
||||
&body.user_id,
|
||||
ProfileFieldChange::Set(body.value.clone()),
|
||||
body.propagate_to.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(set_profile_field::v3::Response::new())
|
||||
}
|
||||
@@ -103,24 +94,13 @@ pub(crate) async fn delete_profile_field_route(
|
||||
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
|
||||
}
|
||||
|
||||
if services
|
||||
.oidc
|
||||
.restricted_profile_fields()
|
||||
.contains(&body.field)
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"This profile field is controlled by your identity provider."
|
||||
)));
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_profile_field(
|
||||
&body.user_id,
|
||||
ProfileFieldChange::Delete(body.field.clone()),
|
||||
body.propagate_to.clone(),
|
||||
)
|
||||
.await?;
|
||||
set_profile_field(
|
||||
&services,
|
||||
&body.user_id,
|
||||
ProfileFieldChange::Delete(body.field.clone()),
|
||||
body.propagate_to.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(delete_profile_field::v3::Response::new())
|
||||
}
|
||||
@@ -130,8 +110,8 @@ async fn fetch_full_profile(
|
||||
user_id: &UserId,
|
||||
) -> Option<BTreeMap<String, Value>> {
|
||||
// If the user exists locally, fetch their local profile
|
||||
if services.users.status(user_id).await.is_found() {
|
||||
return Some(services.users.get_local_profile(user_id).await);
|
||||
if services.users.exists(user_id).await {
|
||||
return Some(get_local_profile(services, user_id).await);
|
||||
}
|
||||
|
||||
// Otherwise ask their homeserver
|
||||
@@ -155,10 +135,13 @@ async fn fetch_full_profile(
|
||||
continue;
|
||||
};
|
||||
|
||||
let _ = services
|
||||
.users
|
||||
.set_profile_field(user_id, ProfileFieldChange::Set(value), PropagateTo::None)
|
||||
.await;
|
||||
let _ = set_profile_field(
|
||||
services,
|
||||
user_id,
|
||||
ProfileFieldChange::Set(value),
|
||||
PropagateTo::None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Some(BTreeMap::from_iter(response))
|
||||
@@ -171,7 +154,7 @@ async fn fetch_profile_field(
|
||||
) -> Result<Option<ProfileFieldValue>> {
|
||||
// If the user exists locally, fetch their local profile field
|
||||
if services.globals.user_is_local(user_id) {
|
||||
return Ok(services.users.get_local_profile_field(user_id, field).await);
|
||||
return Ok(get_local_profile_field(services, user_id, field).await);
|
||||
}
|
||||
|
||||
// Otherwise ask their homeserver
|
||||
@@ -192,14 +175,13 @@ async fn fetch_profile_field(
|
||||
|
||||
if let Some(value) = response.get(field.as_str()).map(ToOwned::to_owned) {
|
||||
if let Ok(value) = ProfileFieldValue::new(field.as_str(), value) {
|
||||
let _ = services
|
||||
.users
|
||||
.set_profile_field(
|
||||
user_id,
|
||||
ProfileFieldChange::Set(value.clone()),
|
||||
PropagateTo::None,
|
||||
)
|
||||
.await;
|
||||
let _ = set_profile_field(
|
||||
services,
|
||||
user_id,
|
||||
ProfileFieldChange::Set(value.clone()),
|
||||
PropagateTo::None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Some(value))
|
||||
} else {
|
||||
@@ -208,11 +190,260 @@ async fn fetch_profile_field(
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
let _ = services
|
||||
.users
|
||||
.set_profile_field(user_id, ProfileFieldChange::Delete(field), PropagateTo::None)
|
||||
.await;
|
||||
let _ = set_profile_field(
|
||||
services,
|
||||
user_id,
|
||||
ProfileFieldChange::Delete(field),
|
||||
PropagateTo::None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_local_profile(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
) -> BTreeMap<String, Value> {
|
||||
let mut profile = BTreeMap::new();
|
||||
|
||||
// Get displayname and avatar_url independently because `all_profile_keys`
|
||||
// doesn't include them
|
||||
for field in [ProfileFieldName::AvatarUrl, ProfileFieldName::DisplayName] {
|
||||
let key = field.as_str().to_owned();
|
||||
|
||||
if let Some(value) = get_local_profile_field(services, user_id, field).await {
|
||||
profile.insert(key, value.value().into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// Insert all other profile fields
|
||||
let mut all_fields = services.users.all_profile_keys(user_id);
|
||||
|
||||
while let Some((key, value)) = all_fields.next().await {
|
||||
profile.insert(key, value);
|
||||
}
|
||||
|
||||
profile
|
||||
}
|
||||
|
||||
pub(crate) async fn get_local_profile_field(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
field: ProfileFieldName,
|
||||
) -> Option<ProfileFieldValue> {
|
||||
let value = match field.clone() {
|
||||
| ProfileFieldName::AvatarUrl => services
|
||||
.users
|
||||
.avatar_url(user_id)
|
||||
.await
|
||||
.ok()
|
||||
.map(to_value)
|
||||
.transpose()
|
||||
.expect("converting avatar url to value should succeed"),
|
||||
| ProfileFieldName::DisplayName => services
|
||||
.users
|
||||
.displayname(user_id)
|
||||
.await
|
||||
.ok()
|
||||
.map(to_value)
|
||||
.transpose()
|
||||
.expect("converting displayname to value should succeed"),
|
||||
| other => services
|
||||
.users
|
||||
.profile_key(user_id, other.as_str())
|
||||
.await
|
||||
.ok(),
|
||||
}?;
|
||||
|
||||
Some(
|
||||
ProfileFieldValue::new(field.as_str(), value)
|
||||
.expect("local profile field should be valid"),
|
||||
)
|
||||
}
|
||||
|
||||
enum ProfileFieldChange {
|
||||
Set(ProfileFieldValue),
|
||||
Delete(ProfileFieldName),
|
||||
}
|
||||
|
||||
impl ProfileFieldChange {
|
||||
fn field_name(&self) -> ProfileFieldName {
|
||||
match self {
|
||||
| &Self::Delete(ref name) => name.clone(),
|
||||
| &Self::Set(ref value) => value.field_name(),
|
||||
}
|
||||
}
|
||||
|
||||
fn value(&self) -> Option<Value> {
|
||||
if let Self::Set(value) = self {
|
||||
Some(value.value().into_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_profile_field(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
change: ProfileFieldChange,
|
||||
propagate_to: PropagateTo,
|
||||
) -> Result<()> {
|
||||
const MAX_KEY_LENGTH_BYTES: usize = 255;
|
||||
const MAX_PROFILE_LENGTH_BYTES: usize = 65536;
|
||||
|
||||
let field_name = change.field_name();
|
||||
|
||||
// TODO: The spec mentions special error codes (M_PROFILE_TOO_LARGE,
|
||||
// M_KEY_TOO_LARGE) for profile field size limits, but they're not in its list
|
||||
// of error codes and Ruma doesn't have them. Should we return those, or is
|
||||
// M_TOO_LARGE okay?
|
||||
if field_name.as_str().len() > MAX_KEY_LENGTH_BYTES {
|
||||
return Err!(Request(TooLarge(
|
||||
"Individual profile keys must not exceed {MAX_KEY_LENGTH_BYTES} bytes in length."
|
||||
)));
|
||||
}
|
||||
|
||||
// Serialize the entire profile as canonical JSON, including the new change,
|
||||
// to check if it exceeds 64 KiB
|
||||
{
|
||||
let mut full_profile = get_local_profile(services, user_id).await;
|
||||
|
||||
match &change {
|
||||
| ProfileFieldChange::Set(value) => {
|
||||
full_profile.insert(
|
||||
value.field_name().as_str().to_owned(),
|
||||
value.value().clone().into_owned(),
|
||||
);
|
||||
},
|
||||
| ProfileFieldChange::Delete(key) => {
|
||||
full_profile.remove(key.as_str());
|
||||
},
|
||||
}
|
||||
|
||||
if let Ok(canonical_profile) = to_canonical_object(full_profile) {
|
||||
if serde_json::to_string(&canonical_profile)
|
||||
.expect("should be able to serialize to string")
|
||||
.len() > MAX_PROFILE_LENGTH_BYTES
|
||||
{
|
||||
return Err!(
|
||||
"Profile data must not exceed {MAX_PROFILE_LENGTH_BYTES} bytes in length."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return Err!(Request(BadJson("Failed to canonicalize profile.")));
|
||||
}
|
||||
}
|
||||
|
||||
// If the user is local and changed their displayname or avatar_url, update it
|
||||
// in all their joined rooms. This is done before updating their profile data
|
||||
// so we can check the old value of the field if `propagate_to` is `unchanged`.
|
||||
if matches!(field_name, ProfileFieldName::AvatarUrl | ProfileFieldName::DisplayName)
|
||||
&& matches!(propagate_to, PropagateTo::All | PropagateTo::Unchanged)
|
||||
&& services.globals.user_is_local(user_id)
|
||||
{
|
||||
let current_displayname = services.users.displayname(user_id).await.ok();
|
||||
let current_avatar_url = services.users.avatar_url(user_id).await.ok();
|
||||
|
||||
let mut all_joined_rooms = services.rooms.state_cache.rooms_joined(user_id);
|
||||
|
||||
while let Some(room_id) = all_joined_rooms.next().await {
|
||||
// TODO: this clobbers any custom fields on the event content
|
||||
let mut current_membership = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_member(&room_id, user_id)
|
||||
.await
|
||||
.expect("should be able to fetch membership event for joined room");
|
||||
|
||||
assert_eq!(
|
||||
current_membership.membership,
|
||||
MembershipState::Join,
|
||||
"user should be joined"
|
||||
);
|
||||
|
||||
// If `propagate_to` is `unchanged`, and the current value of the field we're
|
||||
// updating was changed from its global value in this room, skip it.
|
||||
if matches!(propagate_to, PropagateTo::Unchanged) {
|
||||
let field_changed_from_global = match field_name {
|
||||
| ProfileFieldName::AvatarUrl =>
|
||||
current_membership.avatar_url.as_ref() != current_avatar_url.as_ref(),
|
||||
| ProfileFieldName::DisplayName =>
|
||||
current_membership.displayname.as_ref() != current_displayname.as_ref(),
|
||||
| _ => unreachable!(),
|
||||
};
|
||||
|
||||
if field_changed_from_global {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;
|
||||
|
||||
// Preserve keys in accordance with the key copying rules
|
||||
current_membership.reason = None;
|
||||
current_membership.join_authorized_via_users_server = None;
|
||||
match &change {
|
||||
| ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => {
|
||||
current_membership.avatar_url = Some(avatar_url.clone());
|
||||
},
|
||||
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
|
||||
current_membership.displayname = Some(displayname.clone());
|
||||
},
|
||||
| ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => {
|
||||
current_membership.avatar_url = None;
|
||||
},
|
||||
| ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => {
|
||||
current_membership.displayname = None;
|
||||
},
|
||||
| _ => unreachable!(),
|
||||
}
|
||||
|
||||
let _ = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PartialPdu::state(user_id.to_string(), ¤t_membership),
|
||||
user_id,
|
||||
Some(&room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if services.config.allow_local_presence {
|
||||
// Send a presence EDU to indicate the profile changed
|
||||
let _ = services
|
||||
.presence
|
||||
.ping_presence(user_id, &PresenceState::Online)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
match change {
|
||||
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
|
||||
services
|
||||
.users
|
||||
.set_displayname(user_id, Some(displayname).filter(|dn| !dn.is_empty()));
|
||||
},
|
||||
| ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => {
|
||||
services
|
||||
.users
|
||||
.set_avatar_url(user_id, Some(avatar_url).filter(|av| av.is_valid()));
|
||||
},
|
||||
| ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => {
|
||||
services.users.set_displayname(user_id, None);
|
||||
},
|
||||
| ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => {
|
||||
services.users.set_avatar_url(user_id, None);
|
||||
},
|
||||
| other =>
|
||||
services
|
||||
.users
|
||||
.set_profile_key(user_id, other.field_name().as_str(), other.value()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -107,8 +107,6 @@ pub(crate) async fn set_read_marker_route(
|
||||
.private_read_set(&body.room_id, sender_user, count);
|
||||
}
|
||||
|
||||
services.sync.wake(sender_user).await;
|
||||
|
||||
Ok(set_read_marker::v3::Response::new())
|
||||
}
|
||||
|
||||
@@ -211,7 +209,5 @@ pub(crate) async fn create_receipt_route(
|
||||
},
|
||||
}
|
||||
|
||||
services.sync.wake(sender_user).await;
|
||||
|
||||
Ok(create_receipt::v3::Response::new())
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ pub(crate) async fn report_user_route(
|
||||
|
||||
delay_response().await;
|
||||
|
||||
if !services.users.status(&body.user_id).await.is_found() {
|
||||
if !services.users.is_active_local(&body.user_id).await {
|
||||
// return 200 as to not reveal if the user exists. Recommended by spec.
|
||||
return Ok(report_user::v3::Response::new());
|
||||
}
|
||||
|
||||
@@ -337,21 +337,14 @@ pub(crate) async fn create_room_route(
|
||||
});
|
||||
|
||||
let mut power_levels_to_grant = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
|
||||
let mut creators: Vec<OwnedUserId> = vec![sender_user.to_owned()];
|
||||
|
||||
if preset == RoomPreset::TrustedPrivateChat {
|
||||
for recipient_user in invitees.iter().cloned() {
|
||||
if room_version_rules
|
||||
.authorization
|
||||
.explicitly_privilege_room_creators
|
||||
{
|
||||
creators.push(recipient_user);
|
||||
} else {
|
||||
power_levels_to_grant.insert(recipient_user, int!(100));
|
||||
}
|
||||
for recipient_user in &invitees {
|
||||
power_levels_to_grant.insert(recipient_user.clone(), int!(100));
|
||||
}
|
||||
}
|
||||
|
||||
let mut creators: Vec<OwnedUserId> = vec![sender_user.to_owned()];
|
||||
// Do we care about additional_creators?
|
||||
if room_version_rules
|
||||
.authorization
|
||||
|
||||
+24
-31
@@ -21,7 +21,7 @@
|
||||
},
|
||||
login::{
|
||||
self,
|
||||
v3::{DiscoveryInfo, HomeserverInfo, LoginInfo},
|
||||
v3::{DiscoveryInfo, HomeserverInfo},
|
||||
},
|
||||
logout, logout_all,
|
||||
},
|
||||
@@ -29,6 +29,7 @@
|
||||
},
|
||||
assign,
|
||||
};
|
||||
use service::uiaa::Identity;
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||
use crate::Ruma;
|
||||
@@ -43,12 +44,6 @@ pub(crate) async fn get_login_types_route(
|
||||
ClientIp(client): ClientIp,
|
||||
_body: Ruma<get_login_types::v3::Request>,
|
||||
) -> Result<get_login_types::v3::Response> {
|
||||
if !services.config.oauth.compatibility_mode().uiaa_available() {
|
||||
return Err!(Request(Unrecognized(
|
||||
"User-interactive authentication is not available on this server."
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(get_login_types::v3::Response::new(vec![
|
||||
get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
|
||||
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
|
||||
@@ -58,7 +53,7 @@ pub(crate) async fn get_login_types_route(
|
||||
]))
|
||||
}
|
||||
|
||||
pub async fn handle_login(
|
||||
pub(crate) async fn handle_login(
|
||||
services: &Services,
|
||||
identifier: Option<&UserIdentifier>,
|
||||
password: &str,
|
||||
@@ -88,6 +83,14 @@ pub async fn handle_login(
|
||||
UserId::parse_with_server_name(user_id_or_localpart, &services.config.server_name)
|
||||
.map_err(|_| err!(Request(InvalidUsername("User ID is malformed"))))?;
|
||||
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
|
||||
}
|
||||
|
||||
if services.users.is_deactivated(&user_id).await? {
|
||||
return Err!(Request(UserDeactivated("This account has been deactivated.")));
|
||||
}
|
||||
|
||||
if services.users.is_locked(&user_id).await? {
|
||||
return Err!(Request(UserLocked("This account has been locked.")));
|
||||
}
|
||||
@@ -120,29 +123,19 @@ pub(crate) async fn login_route(
|
||||
ClientIp(client): ClientIp,
|
||||
body: Ruma<login::v3::Request>,
|
||||
) -> Result<login::v3::Response> {
|
||||
if !services.config.oauth.compatibility_mode().uiaa_available() {
|
||||
return match body.login_info {
|
||||
| LoginInfo::ApplicationService(_) => {
|
||||
Err!(Request(AppserviceLoginUnsupported(
|
||||
"User-interactive appservice login is not available on this server."
|
||||
)))
|
||||
},
|
||||
| _ => {
|
||||
Err!(Request(Unrecognized(
|
||||
"User-interactive authentication is not available on this server."
|
||||
)))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||
|
||||
// Validate login method
|
||||
// TODO: Other login methods
|
||||
let user_id = match &body.login_info {
|
||||
#[allow(deprecated)]
|
||||
| LoginInfo::Password(login::v3::Password { identifier, password, user, .. }) =>
|
||||
handle_login(&services, identifier.as_ref(), password, user.as_ref()).await?,
|
||||
| LoginInfo::Token(login::v3::Token { token, .. }) => {
|
||||
| login::v3::LoginInfo::Password(login::v3::Password {
|
||||
identifier,
|
||||
password,
|
||||
user,
|
||||
..
|
||||
}) => handle_login(&services, identifier.as_ref(), password, user.as_ref()).await?,
|
||||
| login::v3::LoginInfo::Token(login::v3::Token { token, .. }) => {
|
||||
debug!("Got token login type");
|
||||
if !services.server.config.login_via_existing_session {
|
||||
return Err!(Request(Unknown("Token login is not enabled.")));
|
||||
@@ -150,7 +143,7 @@ pub(crate) async fn login_route(
|
||||
services.users.find_from_login_token(token).await?
|
||||
},
|
||||
#[allow(deprecated)]
|
||||
| LoginInfo::ApplicationService(login::v3::ApplicationService {
|
||||
| login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService {
|
||||
identifier,
|
||||
user,
|
||||
..
|
||||
@@ -184,6 +177,7 @@ pub(crate) async fn login_route(
|
||||
user_id
|
||||
},
|
||||
| _ => {
|
||||
debug!("/login json_body: {:?}", &body.json_body);
|
||||
return Err!(Request(Unknown(
|
||||
debug_warn!(?body.login_info, "Invalid or unsupported login type")
|
||||
)));
|
||||
@@ -213,7 +207,7 @@ pub(crate) async fn login_route(
|
||||
if device_exists {
|
||||
services
|
||||
.users
|
||||
.set_token(&user_id, &device_id, &token, None)
|
||||
.set_token(&user_id, &device_id, &token)
|
||||
.await?;
|
||||
} else {
|
||||
services
|
||||
@@ -222,7 +216,6 @@ pub(crate) async fn login_route(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&token,
|
||||
None,
|
||||
body.initial_device_display_name.clone(),
|
||||
Some(client.to_string()),
|
||||
)
|
||||
@@ -261,7 +254,7 @@ pub(crate) async fn login_token_route(
|
||||
ClientIp(client): ClientIp,
|
||||
body: Ruma<get_login_token::v1::Request>,
|
||||
) -> Result<get_login_token::v1::Response> {
|
||||
if !services.config.login_via_existing_session {
|
||||
if !services.server.config.login_via_existing_session {
|
||||
return Err!(Request(Forbidden("Login via an existing session is not enabled")));
|
||||
}
|
||||
|
||||
@@ -270,7 +263,7 @@ pub(crate) async fn login_token_route(
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, sender_user, body.identity.sender_device(), None)
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
|
||||
let login_token = utils::random_string(TOKEN_LENGTH);
|
||||
|
||||
@@ -201,6 +201,9 @@ pub(crate) async fn sync_events_route(
|
||||
.update_device_last_seen(sender_user, Some(sender_device), client_ip)
|
||||
.await;
|
||||
|
||||
// Setup watchers, so if there's no response, we can wait for them
|
||||
let watcher = services.sync.watch(sender_user, sender_device);
|
||||
|
||||
let response = build_sync_events(&services, &body).await?;
|
||||
if body.body.full_state
|
||||
|| !(response.rooms.is_empty()
|
||||
@@ -216,7 +219,7 @@ pub(crate) async fn sync_events_route(
|
||||
// Stop hanging if new info arrives
|
||||
let default = Duration::from_secs(30);
|
||||
let duration = cmp::min(body.body.timeout.unwrap_or(default), default);
|
||||
_ = tokio::time::timeout(duration, services.sync.wait_for_wake(sender_user)).await;
|
||||
_ = tokio::time::timeout(duration, watcher).await;
|
||||
|
||||
// Retry returning data
|
||||
build_sync_events(&services, &body).await
|
||||
|
||||
@@ -70,6 +70,7 @@ pub(crate) async fn sync_events_v5_route(
|
||||
ClientIp(client_ip): ClientIp,
|
||||
body: Ruma<sync_events::v5::Request>,
|
||||
) -> Result<sync_events::v5::Response> {
|
||||
debug_assert!(DEFAULT_BUMP_TYPES.is_sorted(), "DEFAULT_BUMP_TYPES is not sorted");
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
let sender_device = body.identity.expect_sender_device()?;
|
||||
|
||||
@@ -80,6 +81,9 @@ pub(crate) async fn sync_events_v5_route(
|
||||
|
||||
let mut body = body.body;
|
||||
|
||||
// Setup watchers, so if there's no response, we can wait for them
|
||||
let watcher = services.sync.watch(sender_user, sender_device);
|
||||
|
||||
let next_batch = services.globals.next_count()?;
|
||||
|
||||
let conn_id = body.conn_id.clone();
|
||||
@@ -217,7 +221,7 @@ pub(crate) async fn sync_events_v5_route(
|
||||
// Stop hanging if new info arrives
|
||||
let default = Duration::from_secs(30);
|
||||
let duration = cmp::min(body.timeout.unwrap_or(default), default);
|
||||
_ = tokio::time::timeout(duration, services.sync.wait_for_wake(sender_user)).await;
|
||||
_ = tokio::time::timeout(duration, watcher).await;
|
||||
}
|
||||
|
||||
let typing = collect_typing_events(services, sender_user, &body, &todo_rooms).await?;
|
||||
|
||||
@@ -35,8 +35,8 @@ pub(crate) async fn get_supported_versions_route(
|
||||
/// `/_matrix/federation/v1/version`
|
||||
pub(crate) async fn conduwuit_server_version() -> Result<impl IntoResponse> {
|
||||
Ok(Json(serde_json::json!({
|
||||
"name": conduwuit::BRANDING,
|
||||
"version": conduwuit::version(),
|
||||
"name": conduwuit::version::name(),
|
||||
"version": conduwuit::version::version(),
|
||||
})))
|
||||
}
|
||||
|
||||
|
||||
@@ -32,26 +32,22 @@ pub(crate) async fn search_users_route(
|
||||
.min(LIMIT_MAX);
|
||||
|
||||
let search_term = body.search_term.to_lowercase();
|
||||
let mut users = services.users.stream().broad_filter_map(async |user_id| {
|
||||
let display_name = services.users.displayname(&user_id).await.ok();
|
||||
|
||||
let mut users = services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.chain(services.users.stream_remote_users())
|
||||
.broad_filter_map(async |user_id| {
|
||||
let display_name = services.users.displayname(&user_id).await.ok();
|
||||
let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term);
|
||||
|
||||
let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term);
|
||||
let display_name_matches = display_name
|
||||
.as_deref()
|
||||
.map(str::to_lowercase)
|
||||
.is_some_and(|display_name| display_name.contains(&search_term));
|
||||
|
||||
let display_name_matches = display_name
|
||||
.as_deref()
|
||||
.map(str::to_lowercase)
|
||||
.is_some_and(|display_name| display_name.contains(&search_term));
|
||||
if !user_id_matches && !display_name_matches {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !user_id_matches && !display_name_matches {
|
||||
return None;
|
||||
}
|
||||
|
||||
let user_in_public_room = services
|
||||
let user_in_public_room =
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&user_id)
|
||||
@@ -64,22 +60,22 @@ pub(crate) async fn search_users_route(
|
||||
.await
|
||||
});
|
||||
|
||||
let user_sees_user = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.user_sees_user(sender_user, &user_id);
|
||||
let user_sees_user = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.user_sees_user(sender_user, &user_id);
|
||||
|
||||
pin_mut!(user_in_public_room, user_sees_user);
|
||||
pin_mut!(user_in_public_room, user_sees_user);
|
||||
|
||||
if user_in_public_room.or(user_sees_user).await {
|
||||
Some(assign!(search_users::v3::User::new(user_id.clone()), {
|
||||
display_name,
|
||||
avatar_url: services.users.avatar_url(&user_id).await.ok(),
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if user_in_public_room.or(user_sees_user).await {
|
||||
Some(assign!(search_users::v3::User::new(user_id.clone()), {
|
||||
display_name,
|
||||
avatar_url: services.users.avatar_url(&user_id).await.ok(),
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let results = users.by_ref().take(limit).collect().await;
|
||||
let limited = users.next().await.is_some();
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
use ruma::{
|
||||
api::client::discovery::{
|
||||
discover_homeserver::{self, HomeserverInfo},
|
||||
discover_policy_server, discover_support,
|
||||
discover_policy_server,
|
||||
discover_support::{self, Contact, ContactRole},
|
||||
},
|
||||
assign,
|
||||
};
|
||||
@@ -66,7 +67,46 @@ pub(crate) async fn well_known_support(
|
||||
.as_ref()
|
||||
.map(ToString::to_string);
|
||||
|
||||
let contacts = services.admin.get_support_contacts().await;
|
||||
let email_address = services.config.well_known.support_email.clone();
|
||||
let matrix_id = services.config.well_known.support_mxid.clone();
|
||||
let pgp_key = services.config.well_known.support_pgp_key.clone();
|
||||
|
||||
// TODO: support defining multiple contacts in the config
|
||||
let mut contacts: Vec<Contact> = vec![];
|
||||
|
||||
let role = services
|
||||
.config
|
||||
.well_known
|
||||
.support_role
|
||||
.clone()
|
||||
.unwrap_or(ContactRole::Admin);
|
||||
|
||||
// Add configured contact if at least one contact method is specified
|
||||
let configured_contact = match (matrix_id, email_address) {
|
||||
| (Some(matrix_id), email_address) =>
|
||||
Some(assign!(Contact::with_matrix_id(role, matrix_id), { email_address })),
|
||||
| (None, Some(email_address)) => Some(Contact::with_email_address(role, email_address)),
|
||||
| (None, None) => None,
|
||||
};
|
||||
|
||||
if let Some(mut configured_contact) = configured_contact {
|
||||
configured_contact.pgp_key = pgp_key;
|
||||
|
||||
contacts.push(configured_contact);
|
||||
}
|
||||
|
||||
// Try to add admin users as contacts if no contacts are configured
|
||||
if contacts.is_empty() {
|
||||
let admin_users = services.admin.get_admins().await;
|
||||
|
||||
for user_id in &admin_users {
|
||||
if *user_id == services.globals.server_user {
|
||||
continue;
|
||||
}
|
||||
|
||||
contacts.push(Contact::with_matrix_id(ContactRole::Admin, user_id.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
if contacts.is_empty() && support_page.is_none() {
|
||||
// No admin room, no configured contacts, and no support page
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#![type_length_limit = "16384"] //TODO: reduce me
|
||||
#![recursion_limit = "256"] // My Giant Async Function
|
||||
#![allow(clippy::toplevel_ref_arg)]
|
||||
|
||||
extern crate conduwuit_core as conduwuit;
|
||||
|
||||
+3
-7
@@ -10,7 +10,7 @@
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{any, get, post},
|
||||
};
|
||||
use conduwuit::err;
|
||||
use conduwuit::{Server, err};
|
||||
pub(super) use conduwuit_service::state::State;
|
||||
use http::{Uri, uri};
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
pub(super) use self::{args::Args as Ruma, auth::ClientIdentity, response::RumaResponse};
|
||||
use crate::{admin, client, server};
|
||||
|
||||
pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
let config = &state.server.config;
|
||||
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
let config = &server.config;
|
||||
let mut router = router
|
||||
.ruma_route(&client::appservice_ping)
|
||||
.ruma_route(&client::get_supported_versions_route)
|
||||
@@ -181,15 +181,11 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
.ruma_route(&client::get_room_summary)
|
||||
.ruma_route(&client::get_suspended_status)
|
||||
.ruma_route(&client::put_suspended_status)
|
||||
.ruma_route(&client::get_lock_status)
|
||||
.ruma_route(&client::put_lock_status)
|
||||
.ruma_route(&client::well_known_support)
|
||||
.ruma_route(&client::well_known_client)
|
||||
.ruma_route(&client::well_known_policy_server)
|
||||
.ruma_route(&client::get_rtc_transports)
|
||||
.ruma_route(&client::room_initial_sync_route)
|
||||
.ruma_route(&client::get_authorization_server_metadata_route)
|
||||
.merge(client::oauth::router(state))
|
||||
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
||||
.ruma_route(&admin::rooms::ban::ban_room)
|
||||
|
||||
+6
-28
@@ -1,7 +1,6 @@
|
||||
use std::any::{Any, TypeId};
|
||||
|
||||
use conduwuit::{Err, Error, Result, err};
|
||||
use http::StatusCode;
|
||||
use conduwuit::{Err, Result, err};
|
||||
use ruma::{
|
||||
DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
|
||||
api::{
|
||||
@@ -11,15 +10,12 @@
|
||||
AuthScheme, NoAccessToken, NoAuthentication,
|
||||
},
|
||||
client,
|
||||
error::{ErrorKind, UnknownTokenErrorData},
|
||||
federation::authentication::ServerSignatures,
|
||||
},
|
||||
assign,
|
||||
};
|
||||
use service::{
|
||||
Services,
|
||||
server_keys::{PubKeyMap, PubKeys},
|
||||
users::AccessTokenStatus,
|
||||
};
|
||||
|
||||
use crate::{router::args::AuthQueryParams, service::appservice::RegistrationInfo};
|
||||
@@ -60,9 +56,8 @@ pub(crate) fn sender_device(&self) -> Option<&DeviceId> {
|
||||
|
||||
pub(crate) fn expect_sender_device(&self) -> Result<&DeviceId> {
|
||||
match self {
|
||||
| Self::User { sender_device, .. }
|
||||
| Self::Appservice { sender_device: Some(sender_device), .. } => Ok(sender_device),
|
||||
| Self::Appservice { sender_device: None, .. } =>
|
||||
| Self::User { sender_device, .. } => Ok(sender_device),
|
||||
| Self::Appservice { .. } =>
|
||||
Err!(Request(Forbidden("Appservices must masquerade to use this endpoint."))),
|
||||
}
|
||||
}
|
||||
@@ -170,20 +165,7 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
if output.is_empty() {
|
||||
return Err!(Request(Unauthorized("Missing access token.")));
|
||||
}
|
||||
if let Some((sender_user, sender_device, status)) =
|
||||
services.users.find_from_token(&output).await
|
||||
{
|
||||
// If the token is expired we return a soft logout
|
||||
if matches!(status, AccessTokenStatus::Expired) {
|
||||
return Err(Error::Request(
|
||||
ErrorKind::UnknownToken(
|
||||
assign!(UnknownTokenErrorData::new(), { soft_logout: true }),
|
||||
),
|
||||
"This token has expired".into(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
));
|
||||
}
|
||||
|
||||
if let Ok((sender_user, sender_device)) = services.users.find_from_token(&output).await {
|
||||
// Locked users can only use /logout and /logout/all
|
||||
if services
|
||||
.users
|
||||
@@ -194,7 +176,7 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
if !(route == TypeId::of::<client::session::logout::v3::Request>()
|
||||
|| route == TypeId::of::<client::session::logout_all::v3::Request>())
|
||||
{
|
||||
return Err!(Request(UserLocked("Your account is locked.")));
|
||||
return Err!(Request(Unauthorized("Your account is locked.")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,11 +227,7 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
appservice_info: Box::new(appservice_info),
|
||||
})
|
||||
} else {
|
||||
Err(Error::Request(
|
||||
ErrorKind::UnknownToken(UnknownTokenErrorData::new()),
|
||||
"Invalid token".into(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
))
|
||||
Err!(Request(Unauthorized("Invalid access token.")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,8 +203,6 @@ pub(crate) async fn create_invite_route(
|
||||
.update_joined_count(&body.room_id)
|
||||
.await;
|
||||
|
||||
services.sync.wake(&recipient_user).await;
|
||||
|
||||
for appservice in services.appservice.read().await.values() {
|
||||
if appservice.is_user_match(&recipient_user) {
|
||||
let transaction_id = general_purpose::URL_SAFE_NO_PAD
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
api::federation::query::{get_profile_information, get_room_information},
|
||||
};
|
||||
|
||||
use crate::Ruma;
|
||||
use crate::{
|
||||
Ruma,
|
||||
client::{get_local_profile, get_local_profile_field},
|
||||
};
|
||||
|
||||
/// # `GET /_matrix/federation/v1/query/directory`
|
||||
///
|
||||
@@ -72,19 +75,15 @@ pub(crate) async fn get_profile_information_route(
|
||||
let response = if let Some(field) = &body.field {
|
||||
let mut response = get_profile_information::v1::Response::new();
|
||||
|
||||
if let Some(value) = services
|
||||
.users
|
||||
.get_local_profile_field(&body.user_id, field.to_owned())
|
||||
.await
|
||||
if let Some(value) =
|
||||
get_local_profile_field(&services, &body.user_id, field.to_owned()).await
|
||||
{
|
||||
response.set(value.field_name().as_str().to_owned(), value.value().into_owned());
|
||||
}
|
||||
|
||||
response
|
||||
} else {
|
||||
services
|
||||
.users
|
||||
.get_local_profile(&body.user_id)
|
||||
get_local_profile(&services, &body.user_id)
|
||||
.await
|
||||
.into_iter()
|
||||
.collect()
|
||||
|
||||
+82
-16
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
net::IpAddr,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
@@ -9,6 +9,7 @@
|
||||
use conduwuit::{
|
||||
Err, Error, Result, debug, debug_error, debug_warn, err, error,
|
||||
result::LogErr,
|
||||
state_res::lexicographical_topological_sort,
|
||||
trace,
|
||||
utils::{
|
||||
IterStream, ReadyExt, millis_since_unix_epoch,
|
||||
@@ -24,7 +25,8 @@
|
||||
use http::StatusCode;
|
||||
use itertools::Itertools;
|
||||
use ruma::{
|
||||
CanonicalJsonObject, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId,
|
||||
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId,
|
||||
OwnedUserId, RoomId, ServerName, UInt, UserId,
|
||||
api::{
|
||||
error::{ErrorKind, LimitExceededErrorData},
|
||||
federation::transactions::{
|
||||
@@ -37,12 +39,12 @@
|
||||
},
|
||||
},
|
||||
events::receipt::{ReceiptEvent, ReceiptEventContent, ReceiptType},
|
||||
int,
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
};
|
||||
use service::{
|
||||
rooms::event_handler::build_local_dag,
|
||||
transactions::{FederationTxnState, TransactionError, TxnKey, WrappedTransactionResponse},
|
||||
use service::transactions::{
|
||||
FederationTxnState, TransactionError, TxnKey, WrappedTransactionResponse,
|
||||
};
|
||||
use tokio::sync::watch::{Receiver, Sender};
|
||||
use tracing::instrument;
|
||||
@@ -274,6 +276,74 @@ async fn handle(
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Attempts to build a localised directed acyclic graph out of the given PDUs,
|
||||
/// returning them in a topologically sorted order.
|
||||
///
|
||||
/// This is used to attempt to process PDUs in an order that respects their
|
||||
/// dependencies, however it is ultimately the sender's responsibility to send
|
||||
/// them in a processable order, so this is just a best effort attempt. It does
|
||||
/// not account for power levels or other tie breaks.
|
||||
async fn build_local_dag(
|
||||
pdu_map: &HashMap<OwnedEventId, CanonicalJsonObject>,
|
||||
) -> Result<Vec<OwnedEventId>> {
|
||||
debug_assert!(pdu_map.len() >= 2, "needless call to build_local_dag with less than 2 PDUs");
|
||||
let mut dag: HashMap<OwnedEventId, HashSet<OwnedEventId>> =
|
||||
HashMap::with_capacity(pdu_map.len());
|
||||
let mut id_origin_ts: HashMap<OwnedEventId, _> = HashMap::with_capacity(pdu_map.len());
|
||||
|
||||
for (event_id, value) in pdu_map {
|
||||
// We already checked that these properties are correct in parse_incoming_pdu,
|
||||
// so it's safe to unwrap here.
|
||||
// We also filter to remove any prev_events that are not in this pdu_map, as we
|
||||
// need to have at least one event with zero out degrees for the lexico-topo
|
||||
// sort below. If there are multiple events with omitted prevs, they will be
|
||||
// ordered by timestamp, then event ID. At that point though, it's unlikely to
|
||||
// matter.
|
||||
let prev_events = value
|
||||
.get("prev_events")
|
||||
.unwrap()
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| EventId::parse(v.as_str().unwrap()).unwrap())
|
||||
.filter(|id| pdu_map.contains_key(id))
|
||||
.collect();
|
||||
|
||||
dag.insert(event_id.clone(), prev_events);
|
||||
let origin_server_ts = value
|
||||
.get("origin_server_ts")
|
||||
.and_then(ruma::CanonicalJsonValue::as_integer)
|
||||
.unwrap_or_default();
|
||||
id_origin_ts.insert(event_id.clone(), origin_server_ts);
|
||||
}
|
||||
|
||||
debug!(count = dag.len(), "Sorting incoming events with partial graph");
|
||||
lexicographical_topological_sort(&dag, &async |node_id| {
|
||||
// Note: we don't bother fetching power levels because that would massively slow
|
||||
// this function down. This is a best-effort attempt to order events correctly
|
||||
// for processing, however ultimately that should be the sender's job.
|
||||
let ts = id_origin_ts
|
||||
.get(&node_id)
|
||||
.copied()
|
||||
.unwrap_or_else(|| int!(0))
|
||||
.to_string()
|
||||
.parse::<u64>()
|
||||
.ok()
|
||||
.and_then(UInt::new)
|
||||
.unwrap_or_default();
|
||||
Ok((int!(0), MilliSecondsSinceUnixEpoch(ts)))
|
||||
})
|
||||
.await
|
||||
.inspect(|sorted| {
|
||||
debug_assert_eq!(
|
||||
sorted.len(),
|
||||
pdu_map.len(),
|
||||
"Sorted graph was not the same size as the input graph"
|
||||
);
|
||||
})
|
||||
.map_err(|e| err!("failed to resolve local graph: {e}"))
|
||||
}
|
||||
|
||||
async fn handle_room(
|
||||
services: &Services,
|
||||
_client: &IpAddr,
|
||||
@@ -289,7 +359,7 @@ async fn handle_room(
|
||||
.await;
|
||||
|
||||
let room_id = &room_id;
|
||||
let mut pdu_map: HashMap<OwnedEventId, CanonicalJsonObject> = pdus
|
||||
let pdu_map: HashMap<OwnedEventId, CanonicalJsonObject> = pdus
|
||||
.into_iter()
|
||||
.map(|(_, event_id, value)| (event_id, value))
|
||||
.collect();
|
||||
@@ -297,11 +367,7 @@ async fn handle_room(
|
||||
// failure (e.g., cycles). This is best-effort; proper ordering is the sender's
|
||||
// responsibility.
|
||||
let sorted_event_ids = if pdu_map.len() >= 2 {
|
||||
let refmap = pdu_map
|
||||
.iter()
|
||||
.map(|(event_id, obj)| (event_id.clone(), obj))
|
||||
.collect();
|
||||
build_local_dag(&refmap).await.unwrap_or_else(|e| {
|
||||
build_local_dag(&pdu_map).await.unwrap_or_else(|e| {
|
||||
debug_warn!("Failed to build local DAG for room {room_id}: {e}");
|
||||
pdu_map.keys().cloned().collect()
|
||||
})
|
||||
@@ -311,8 +377,9 @@ async fn handle_room(
|
||||
let mut results = Vec::with_capacity(sorted_event_ids.len());
|
||||
for event_id in sorted_event_ids {
|
||||
let value = pdu_map
|
||||
.remove(&event_id)
|
||||
.expect("sorted event IDs must be from the original map");
|
||||
.get(&event_id)
|
||||
.expect("sorted event IDs must be from the original map")
|
||||
.clone();
|
||||
services
|
||||
.server
|
||||
.check_running()
|
||||
@@ -320,7 +387,7 @@ async fn handle_room(
|
||||
let result = services
|
||||
.rooms
|
||||
.event_handler
|
||||
.handle_incoming_pdu(origin, room_id, &event_id, value.clone(), true)
|
||||
.handle_incoming_pdu(origin, room_id, &event_id, value, true)
|
||||
.boxed()
|
||||
.await
|
||||
.map(|_| ());
|
||||
@@ -620,9 +687,8 @@ async fn handle_edu_direct_to_device(
|
||||
.broad_filter_map(|(target_user_id, map)| async move {
|
||||
services
|
||||
.users
|
||||
.status(&target_user_id)
|
||||
.is_active_local(&target_user_id)
|
||||
.await
|
||||
.is_active()
|
||||
.then_some((target_user_id, map))
|
||||
})
|
||||
.for_each_concurrent(automatic_width(), |(target_user_id, map)| {
|
||||
|
||||
@@ -11,8 +11,8 @@ pub(crate) async fn get_server_version_route(
|
||||
) -> Result<get_server_version::v1::Response> {
|
||||
Ok(assign!(get_server_version::v1::Response::new(), {
|
||||
server: Some(assign!(get_server_version::v1::Server::new(), {
|
||||
name: Some(conduwuit::BRANDING.into()),
|
||||
version: Some(conduwuit::version().into()),
|
||||
name: Some(conduwuit::version::name().into()),
|
||||
version: Some(conduwuit::version::version().into()),
|
||||
})),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ repository.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
path = "mod.rs"
|
||||
crate-type = [
|
||||
"rlib",
|
||||
@@ -118,8 +117,6 @@ url.workspace = true
|
||||
parking_lot.workspace = true
|
||||
lock_api.workspace = true
|
||||
hyper-util.workspace = true
|
||||
resolvematrix.workspace = true
|
||||
openidconnect.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix.workspace = true
|
||||
|
||||
@@ -289,15 +289,6 @@ pub fn check(config: &Config) -> Result {
|
||||
| _ => (),
|
||||
}
|
||||
|
||||
if let Some(oidc) = &config.oauth.oidc
|
||||
&& oidc.client_secret.is_none()
|
||||
&& oidc.client_secret_file.is_none()
|
||||
{
|
||||
return Err!(
|
||||
"Either `client_secret` or `client_secret_file` must be set if OIDC is configured."
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+11
-221
@@ -17,12 +17,10 @@
|
||||
use figment::providers::{Env, Format, Toml};
|
||||
pub use figment::{Figment, value::Value as FigmentValue};
|
||||
use lettre::message::Mailbox;
|
||||
use openidconnect::{ClientId, ClientSecret, Scope};
|
||||
use regex::RegexSet;
|
||||
use ruma::{
|
||||
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
|
||||
api::client::{discovery::discover_support::ContactRole, rtc::RtcTransport},
|
||||
profile::ProfileFieldName,
|
||||
serde::Base64,
|
||||
};
|
||||
use serde::{Deserialize, Serialize, de::IgnoredAny};
|
||||
@@ -657,25 +655,19 @@ pub struct Config {
|
||||
/// even if `recaptcha_site_key` is set.
|
||||
pub recaptcha_private_site_key: Option<String>,
|
||||
|
||||
/// display: nested
|
||||
#[serde(default)]
|
||||
pub registration_terms: RegistrationTerms,
|
||||
|
||||
/// display: nested
|
||||
#[serde(default)]
|
||||
pub oauth: OauthConfig,
|
||||
|
||||
/// Controls whether users are allowed to deactivate their own accounts
|
||||
/// through the account management panel or their Matrix clients. Server
|
||||
/// admins can always deactivate users using the relevant admin commands.
|
||||
/// Policy documents, such as terms and conditions or a privacy policy,
|
||||
/// which users must agree to when registering an account.
|
||||
///
|
||||
/// Note that, in some jurisdictions, you may be legally required to honor
|
||||
/// users who request to deactivate their accounts if you set this option
|
||||
/// to `false`.
|
||||
/// Example:
|
||||
/// ```ignore
|
||||
/// [global.registration_terms.privacy_policy]
|
||||
/// en = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
||||
/// es = { name = "Política de Privacidad", url = "https://homeserver.example/es/privacy_policy.html" }
|
||||
/// ```
|
||||
///
|
||||
/// default: true
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_deactivation: bool,
|
||||
/// default: {}
|
||||
#[serde(default)]
|
||||
pub registration_terms: HashMap<String, HashMap<String, TermsDocument>>,
|
||||
|
||||
/// Controls whether encrypted rooms and events are allowed.
|
||||
#[serde(default = "true_fn")]
|
||||
@@ -1669,11 +1661,6 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub send_messages_from_ignored_users_to_client: bool,
|
||||
|
||||
/// Send "org.matrix.dummy_event" events to the client. This is a debugging
|
||||
/// option.
|
||||
#[serde(default)]
|
||||
pub send_dummy_events_to_clients: bool,
|
||||
|
||||
/// Vector list of IPv4 and IPv6 CIDR ranges / subnets *in quotes* that you
|
||||
/// do not want continuwuity to send outbound requests to. Defaults to
|
||||
/// RFC1918, unroutable, loopback, multicast, and testnet addresses for
|
||||
@@ -2374,29 +2361,6 @@ pub struct SmtpConfig {
|
||||
pub require_email_for_token_registration: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.registration_terms",
|
||||
optional = "true"
|
||||
)]
|
||||
pub struct RegistrationTerms {
|
||||
/// The language code to provide to clients along with the policy documents.
|
||||
///
|
||||
/// default: "en"
|
||||
#[serde(default = "default_terms_language")]
|
||||
pub language: String,
|
||||
/// Policy documents, such as terms and conditions or a privacy policy,
|
||||
/// which users must agree to when registering an account.
|
||||
///
|
||||
/// Example:
|
||||
/// ```ignore
|
||||
/// [global.registration_terms.documents]
|
||||
/// privacy_policy = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
||||
/// ```
|
||||
pub documents: BTreeMap<String, TermsDocument>,
|
||||
}
|
||||
|
||||
/// A policy document for use with a m.login.terms stage.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct TermsDocument {
|
||||
@@ -2404,172 +2368,6 @@ pub struct TermsDocument {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.oauth",
|
||||
optional = "true"
|
||||
)]
|
||||
pub struct OauthConfig {
|
||||
/// The compatibility mode to use for OAuth.
|
||||
///
|
||||
/// - "disabled": OAuth will be unavailable. Users will only be able to log
|
||||
/// in using legacy authentication.
|
||||
/// - "hybrid": OAuth and legacy authentication will both be available. Some
|
||||
/// clients may only use one or the other.
|
||||
/// - "exclusive": Only OAuth will be available. Clients which require
|
||||
/// legacy authentication will be unable to log in.
|
||||
///
|
||||
/// default: "hybrid"
|
||||
compatibility_mode: OAuthMode,
|
||||
|
||||
/// display: hidden
|
||||
pub oidc: Option<OidcConfig>,
|
||||
}
|
||||
|
||||
impl OauthConfig {
|
||||
#[must_use]
|
||||
pub fn compatibility_mode(&self) -> OAuthMode {
|
||||
if self.oidc.is_some() {
|
||||
OAuthMode::Exclusive
|
||||
} else {
|
||||
self.compatibility_mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OAuthMode {
|
||||
Disabled,
|
||||
#[default]
|
||||
Hybrid,
|
||||
Exclusive,
|
||||
}
|
||||
|
||||
impl OAuthMode {
|
||||
#[must_use]
|
||||
pub fn uiaa_available(&self) -> bool { matches!(self, Self::Disabled | Self::Hybrid) }
|
||||
|
||||
#[must_use]
|
||||
pub fn oauth_available(&self) -> bool { matches!(self, Self::Hybrid | Self::Exclusive) }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.oauth.oidc",
|
||||
optional = "true",
|
||||
subheader = "\
|
||||
# Uncommenting this section will enable Continuwuity's support for
|
||||
# authenticating users using an OpenID Connect-compatible identity provider.
|
||||
# This is referred to as \"delegated authentication\".
|
||||
#
|
||||
# IMPORTANT NOTE: When delegated authentication is active, Continuwuity will behave as if
|
||||
# the `global.oauth.compatibility_mode` setting is set to `exclusive`.
|
||||
# Matrix clients which do not support OAuth login (also referred to as \"next-gen auth\") will \
|
||||
NOT be able
|
||||
# to log in while delegated authentication is active."
|
||||
)]
|
||||
pub struct OidcConfig {
|
||||
/// The OIDC issuer URL. Continuwuity will use OpenID Connect Discovery to
|
||||
/// automatically fetch the identity provider's metadata from this URL.
|
||||
/// Generally you should set this to the base domain your identity provider
|
||||
/// runs on.
|
||||
pub discovery_url: Url,
|
||||
|
||||
/// The OAuth client ID for Continuwuity to use when communicating with the
|
||||
/// identity provider.
|
||||
pub client_id: ClientId,
|
||||
|
||||
/// The OAuth client secret for Continuwuity to use when communicating with
|
||||
/// the identity provider.
|
||||
pub client_secret: Option<ClientSecret>,
|
||||
|
||||
/// A path to a file which Continuwuity will read the client secret from.
|
||||
/// If this option is set, it will override `client_secret`.
|
||||
///
|
||||
/// The server will fail to start if the file cannot be read.
|
||||
pub client_secret_file: Option<PathBuf>,
|
||||
|
||||
/// Additional scopes Continuwuity should request from the IDP. This may be
|
||||
/// necessary to access certain claims. Continuwuity always requests the
|
||||
/// `openid` scope.
|
||||
///
|
||||
/// default: []
|
||||
#[serde(default)]
|
||||
pub additional_scopes: Vec<Scope>,
|
||||
|
||||
/// Whether the user should be prompted to choose a localpart
|
||||
/// when signing in for the first time. If this is `false`, Continuwuity
|
||||
/// will attempt to use the value of the `preferred_username_claim`
|
||||
/// (see below) as the user's localpart. Authentication will
|
||||
/// fail if this claim is missing or is not a valid localpart.
|
||||
///
|
||||
/// default: true
|
||||
#[serde(default = "true_fn")]
|
||||
pub prompt_for_localpart: bool,
|
||||
|
||||
/// The claim to use for the user's localpart, if `prompt_for_localpart` is
|
||||
/// false.
|
||||
///
|
||||
/// default: "preferred_username"
|
||||
#[serde(default = "default_preferred_username_claim")]
|
||||
pub preferred_username_claim: String,
|
||||
|
||||
/// The claim which will be used to set the user's email address,
|
||||
/// either on initial registration or on every login depending on
|
||||
/// the value of `profile_key_import_mode`. Continuwuity assumes that
|
||||
/// the IDP has taken care of verifying that the user controls the email
|
||||
/// address it provides.
|
||||
///
|
||||
/// This option does nothing if SMTP is not configured.
|
||||
///
|
||||
/// If this option is set, and `profile_key_import_mode` is `on_login`,
|
||||
/// users will not be able to change their email addresses themselves.
|
||||
///
|
||||
/// default: "email"
|
||||
pub email_claim: Option<String>,
|
||||
|
||||
/// Defines how claims returned from the IDP should be mapped to a user's
|
||||
/// profile data. The profile field named in each key will be set from the
|
||||
/// claim named in the corresponding value when the user first registers,
|
||||
/// and possibly on subsequent logins as well, depending on the value of
|
||||
/// `profile_key_import_mode` (see below).
|
||||
///
|
||||
/// Per-room overrides to the user's display name or avatar will be
|
||||
/// preserved by the import process.
|
||||
///
|
||||
/// SECURITY NOTE: If the `avatar_url` field is set, Continuwuity will
|
||||
/// perform a HTTP GET to the URL in the mapped claim and use the returned
|
||||
/// file as the user's profile picture. Make sure your users are not able
|
||||
/// to set the value of the mapped claim to an arbitrary URL.
|
||||
///
|
||||
/// default: { displayname = "name" }
|
||||
#[serde(default = "default_profile_key_map")]
|
||||
pub profile_key_map: HashMap<String, String>,
|
||||
|
||||
/// When profile keys should be imported from the IDP's claims.
|
||||
///
|
||||
/// - "on_registration": Listed keys will be imported once, when the user
|
||||
/// logs in for the first time and their shadow account is created.
|
||||
/// - "on_login": Listed keys will be imported every time the user logs in.
|
||||
/// Additionally, users will not be able to manually edit any listed keys
|
||||
/// through their Matrix client.
|
||||
///
|
||||
/// default: "on_registration"
|
||||
#[serde(default)]
|
||||
pub profile_key_import_mode: OidcProfileKeyImportMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OidcProfileKeyImportMode {
|
||||
#[default]
|
||||
OnRegistration,
|
||||
OnLogin,
|
||||
}
|
||||
|
||||
const DEPRECATED_KEYS: &[&str] = &[
|
||||
"cache_capacity",
|
||||
"conduit_cache_capacity_modifier",
|
||||
@@ -2952,11 +2750,3 @@ fn default_client_response_timeout() -> u64 { 120 }
|
||||
fn default_client_shutdown_timeout() -> u64 { 15 }
|
||||
|
||||
fn default_sender_shutdown_timeout() -> u64 { 5 }
|
||||
|
||||
fn default_terms_language() -> String { "en".to_owned() }
|
||||
|
||||
fn default_preferred_username_claim() -> String { "preferred_username".to_owned() }
|
||||
|
||||
fn default_profile_key_map() -> HashMap<String, String> {
|
||||
HashMap::from_iter([(ProfileFieldName::DisplayName.to_string(), "name".to_owned())])
|
||||
}
|
||||
|
||||
@@ -87,8 +87,6 @@ pub enum Error {
|
||||
YamlDe(#[from] serde_saphyr::Error),
|
||||
#[error(transparent)]
|
||||
YamlSer(#[from] serde_saphyr::ser_error::Error),
|
||||
#[error(transparent)]
|
||||
ResolveServer(#[from] resolvematrix::error::ResolveServerError),
|
||||
|
||||
// ruma/conduwuit
|
||||
#[error("Arithmetic operation failed: {0}")]
|
||||
@@ -163,7 +161,6 @@ pub fn message(&self) -> String {
|
||||
match self {
|
||||
| Self::Federation(origin, error) => format!("Answer from {origin}: {error}"),
|
||||
| Self::Ruma(error) => response::ruma_error_message(error),
|
||||
| Self::Request(_, message, _) => message.clone().into_owned(),
|
||||
| _ => format!("{self}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,11 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
|
||||
// 413
|
||||
| TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
|
||||
|
||||
// 405
|
||||
| Unrecognized => StatusCode::METHOD_NOT_ALLOWED,
|
||||
|
||||
// 404
|
||||
| Unrecognized | NotFound => StatusCode::NOT_FOUND,
|
||||
| NotFound => StatusCode::NOT_FOUND,
|
||||
|
||||
// 403
|
||||
| GuestAccessForbidden
|
||||
|
||||
@@ -7,16 +7,19 @@
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub const BRANDING: &str = "continuwuity";
|
||||
pub const ROUTE_PREFIX: &str = "/_continuwuity";
|
||||
pub const WEBSITE: &str = "https://continuwuity.org";
|
||||
pub const SEMANTIC: &str = env!("CARGO_PKG_VERSION");
|
||||
static BRANDING: &str = "continuwuity";
|
||||
static WEBSITE: &str = "https://continuwuity.org";
|
||||
static SEMANTIC: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
static VERSION: OnceLock<String> = OnceLock::new();
|
||||
static VERSION_UA: OnceLock<String> = OnceLock::new();
|
||||
static USER_AGENT: OnceLock<String> = OnceLock::new();
|
||||
static USER_AGENT_MEDIA: OnceLock<String> = OnceLock::new();
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn name() -> &'static str { BRANDING }
|
||||
|
||||
#[inline]
|
||||
pub fn version() -> &'static str { VERSION.get_or_init(init_version) }
|
||||
|
||||
@@ -29,10 +32,10 @@ pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) }
|
||||
#[inline]
|
||||
pub fn user_agent_media() -> &'static str { USER_AGENT_MEDIA.get_or_init(init_user_agent_media) }
|
||||
|
||||
fn init_user_agent() -> String { format!("{BRANDING}/{} (bot; +{WEBSITE})", version_ua()) }
|
||||
fn init_user_agent() -> String { format!("{}/{} (bot; +{WEBSITE})", name(), version_ua()) }
|
||||
|
||||
fn init_user_agent_media() -> String {
|
||||
format!("{BRANDING}/{} (embedbot; facebookexternalhit/1.1; +{WEBSITE})", version_ua())
|
||||
format!("{}/{} (embedbot; facebookexternalhit/1.1; +{WEBSITE})", name(), version_ua())
|
||||
}
|
||||
|
||||
fn init_version_ua() -> String {
|
||||
|
||||
@@ -5,10 +5,7 @@
|
||||
events::{MessageLikeEventContent, StateEventContent, TimelineEventType},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{
|
||||
json,
|
||||
value::{RawValue as RawJsonValue, to_raw_value},
|
||||
};
|
||||
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
|
||||
|
||||
use super::StateKey;
|
||||
|
||||
@@ -65,7 +62,7 @@ impl Default for PartialPdu {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
event_type: "m.room.message".into(),
|
||||
content: to_raw_value(&json!({})).unwrap(),
|
||||
content: to_raw_value("{}").unwrap(),
|
||||
unsigned: None,
|
||||
state_key: None,
|
||||
redacts: None,
|
||||
|
||||
+17
-13
@@ -21,25 +21,29 @@ pub fn versions() -> Vec<String> {
|
||||
"v1.12".to_owned(),
|
||||
"v1.13".to_owned(),
|
||||
"v1.14".to_owned(),
|
||||
"v1.15".to_owned(),
|
||||
"v1.16".to_owned(),
|
||||
"v1.17".to_owned(),
|
||||
"v1.18".to_owned(),
|
||||
]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn unstable_features() -> BTreeMap<String, bool> {
|
||||
BTreeMap::from_iter([
|
||||
// query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
|
||||
// Expected for spec v1.19
|
||||
("uk.half-shot.msc2666.query_mutual_rooms.stable".to_owned(), true),
|
||||
// Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186)
|
||||
// Expected for spec v1.19
|
||||
("org.matrix.simplified_msc3575".to_owned(), true),
|
||||
// invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155)
|
||||
("org.matrix.msc4155".to_owned(), true),
|
||||
// profile change propagation (https://github.com/matrix-org/matrix-spec-proposals/pull/4466)
|
||||
("computer.gingershaped.msc4466".to_owned(), true),
|
||||
("org.matrix.e2e_cross_signing".to_owned(), true),
|
||||
("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */
|
||||
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */
|
||||
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
|
||||
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
|
||||
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
|
||||
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
|
||||
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
|
||||
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
|
||||
("org.matrix.msc4180".to_owned(), true), /* stable flag for 3916 (https://github.com/matrix-org/matrix-spec-proposals/pull/4180) */
|
||||
("uk.tcpip.msc4133".to_owned(), true), /* Extending User Profile API with Key:Value Pairs (https://github.com/matrix-org/matrix-spec-proposals/pull/4133) */
|
||||
("us.cloke.msc4175".to_owned(), true), /* Profile field for user time zone (https://github.com/matrix-org/matrix-spec-proposals/pull/4175) */
|
||||
("org.matrix.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
|
||||
("uk.timedout.msc4323".to_owned(), true), /* agnostic suspend (https://github.com/matrix-org/matrix-spec-proposals/pull/4323) */
|
||||
("org.matrix.msc4155".to_owned(), true), /* invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155) */
|
||||
("computer.gingershaped.msc4466".to_owned(), true), /* profile change propagation (https://github.com/matrix-org/matrix-spec-proposals/pull/4466) */
|
||||
("org.matrix.msc4380.stable".to_owned(), true),
|
||||
])
|
||||
}
|
||||
|
||||
+4
-1
@@ -34,7 +34,10 @@ macro_rules! mod_dtor {
|
||||
pub use conduwuit_build_metadata as build_metadata;
|
||||
pub use config::Config;
|
||||
pub use error::Error;
|
||||
pub use info::version::*;
|
||||
pub use info::{
|
||||
version,
|
||||
version::{name, version},
|
||||
};
|
||||
pub use matrix::{Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, pdu, state_res};
|
||||
pub use parking_lot::{Mutex as SyncMutex, RwLock as SyncRwLock};
|
||||
pub use server::Server;
|
||||
|
||||
+10
-19
@@ -1,10 +1,7 @@
|
||||
pub mod exponential_backoff;
|
||||
pub mod jitter;
|
||||
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub use jitter::jitter;
|
||||
|
||||
use crate::{Result, err};
|
||||
|
||||
#[inline]
|
||||
@@ -64,23 +61,17 @@ pub fn format(ts: SystemTime, str: &str) -> String {
|
||||
pub fn pretty(d: Duration) -> String {
|
||||
use Unit::*;
|
||||
|
||||
let fmt = |w, u| {
|
||||
if w == 1 {
|
||||
format!("{w} {u}")
|
||||
} else {
|
||||
format!("{w} {u}s")
|
||||
}
|
||||
};
|
||||
let gen64 = |w, u| fmt(w, u);
|
||||
let gen128 = |w, u| gen64(u64::try_from(w).expect("u128 to u64"), u);
|
||||
let fmt = |w, f, u| format!("{w}.{f} {u}");
|
||||
let gen64 = |w, f, u| fmt(w, (f * 100.0) as u32, u);
|
||||
let gen128 = |w, f, u| gen64(u64::try_from(w).expect("u128 to u64"), f, u);
|
||||
match whole_and_frac(d) {
|
||||
| (Days(whole), _) => gen64(whole, "day"),
|
||||
| (Hours(whole), _) => gen64(whole, "hour"),
|
||||
| (Mins(whole), _) => gen64(whole, "minute"),
|
||||
| (Secs(whole), _) => gen64(whole, "second"),
|
||||
| (Millis(whole), _) => gen128(whole, "millisecond"),
|
||||
| (Micros(whole), _) => gen128(whole, "microsecond"),
|
||||
| (Nanos(whole), _) => gen128(whole, "nanosecond"),
|
||||
| (Days(whole), frac) => gen64(whole, frac, "days"),
|
||||
| (Hours(whole), frac) => gen64(whole, frac, "hours"),
|
||||
| (Mins(whole), frac) => gen64(whole, frac, "minutes"),
|
||||
| (Secs(whole), frac) => gen64(whole, frac, "seconds"),
|
||||
| (Millis(whole), frac) => gen128(whole, frac, "milliseconds"),
|
||||
| (Micros(whole), frac) => gen128(whole, frac, "microseconds"),
|
||||
| (Nanos(whole), frac) => gen128(whole, frac, "nanoseconds"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user