Compare commits

..

49 Commits

Author SHA1 Message Date
Ginger 46e2607aeb fix: Use a LRU cache for storing pending auth code grants 2026-06-16 12:50:43 -04:00
Ginger 90758944ca fix: Spacing 2026-06-16 12:50:43 -04:00
Ginger e3aa13d2f6 chore: Clippy fixes 2026-06-16 12:50:43 -04:00
Ginger 5a0e9303ee fix: Accessibility improvements 2026-06-16 12:50:43 -04:00
Ginger 32b018a676 fix: More CSS tweaks 2026-06-16 12:50:43 -04:00
Ginger 8a652cdd2f fix: Set correct default for registration terms config section 2026-06-16 12:50:43 -04:00
Ginger d0e885fb1c fix: Check for existing device when creating oauth session 2026-06-16 12:50:43 -04:00
Ginger e95e653379 fix: Use RFC-compliant error responses for OAuth endpoints 2026-06-16 12:50:43 -04:00
Ginger 3057a44de4 fix: Force trusted flow UI for first-run registration 2026-06-16 12:50:43 -04:00
Ginger 37f26dc430 fix: Panic when trying to check an unknown UIAA stage type 2026-06-16 12:50:43 -04:00
Ginger 1442cfcb4f fix: Use the right error code for CAPTCHA errors 2026-06-16 12:50:43 -04:00
Ginger 3f263714c3 fix: Formatting 2026-06-16 12:50:43 -04:00
Ginger 30684724b5 refactor: Update logic for checking if a username is available 2026-06-16 12:50:43 -04:00
Ginger df8bcdd92d fix: CSS adjustments 2026-06-16 12:50:43 -04:00
Ginger 560c959ee7 fix: Adjust error codes to comply with MSC4190 2026-06-16 12:50:43 -04:00
Ginger ee4b2f5b8d feat: Mark spec version 1.15 as supported 2026-06-16 12:50:43 -04:00
Ginger 1a766d52d4 feat: Add a page with some information about the server 2026-06-16 12:50:43 -04:00
Ginger 0c34700a96 fix: Correct config file example section name 2026-06-16 12:50:43 -04:00
Ginger 9ef401f13e chore: My Giant Future 2026-06-16 12:50:43 -04:00
Ginger bfe45facad feat: Improve account panel UI for locked and suspended accounts 2026-06-16 12:50:43 -04:00
Ginger 866742ecc5 fix: Include query parameters in link back to login on register page 2026-06-16 12:50:43 -04:00
Ginger 7a7e3b4697 fix: CSS tweaks 2026-06-16 12:50:43 -04:00
Ginger 27267eec69 feat: Improve registration UI in first-run mode 2026-06-16 12:50:43 -04:00
Ginger 6f3985d837 fix: Minor wording improvements 2026-06-16 12:50:43 -04:00
Ginger d42836dabf fix: Set default for allow_deactivation 2026-06-16 12:50:43 -04:00
Ginger 05dc9d08e1 fix: Fix registration terms example in config 2026-06-16 12:50:43 -04:00
Ginger 0daf876e9b feat: Implement support for prompt=create in the authorization code flow 2026-06-16 12:50:43 -04:00
Ginger 77ae59fde7 fix: Don't let logged-in users access the registration page 2026-06-16 12:50:43 -04:00
Ginger b865f0d56e feat: Allow self-service deactivation to be disabled 2026-06-16 12:50:43 -04:00
Ginger 9e9eb19331 refactor: Use more consistent terminology for email validation pages 2026-06-16 12:50:43 -04:00
Ginger 4f333ed173 feat: Add support for registering accounts with the web UI 2026-06-16 12:50:43 -04:00
Ginger 1840549332 refactor: Change template context to allow using a CSP nonce 2026-06-16 12:50:43 -04:00
Ginger ca50d52469 fix: Minor CSS improvements 2026-06-16 12:50:43 -04:00
Ginger 80493f3e36 fix: Remove errant whitespace in device details 2026-06-16 12:50:43 -04:00
Ginger e39c3e3aee chore: News fragment 2026-06-16 12:50:43 -04:00
Ginger 25144ecceb feat: Allow configuring the OAuth compatibility mode 2026-06-16 12:50:43 -04:00
Ginger c2c841b8c7 fix: Use button styling for account management link on index page 2026-06-16 12:50:43 -04:00
Ginger 9f10657406 fix: Use the right text color on input elements 2026-06-16 12:50:43 -04:00
Ginger a8c49a3034 feat: Add support for account management deeplinks 2026-06-16 12:50:43 -04:00
Ginger 35f3b44e46 fix: Return the correct error code for expired access tokens 2026-06-16 12:50:43 -04:00
Ginger 2ffbead621 feat: Add a page for viewing a device's details 2026-06-16 12:50:43 -04:00
Ginger 772553ec79 fix: Use SameSite=Lax for session cookie 2026-06-16 12:50:43 -04:00
Ginger 732dd623ee feat: Allow devices to be removed from the account panel 2026-06-16 12:50:43 -04:00
Ginger 0974481066 feat: Implement oauth token revocation 2026-06-16 12:50:43 -04:00
Ginger 5ac3ef7372 chore: Clippy fixes 2026-06-16 12:50:43 -04:00
Ginger 53a0ca9ae5 feat: Implement oauth auth code and refresh token flows 2026-06-16 12:50:43 -04:00
Ginger b5956368bc chore: Clippy fixes 2026-06-16 12:50:43 -04:00
Ginger e143da969a feat: Implement a web-based account management dashboard 2026-06-16 12:50:43 -04:00
Ginger 7631250278 feat: Implement oauth service and client registration 2026-06-16 12:50:43 -04:00
253 changed files with 17674 additions and 21072 deletions
+1 -1
View File
@@ -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-*
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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@9bcaee1dcae34154180f412e2fa69355a7cda9f6 # v2
uses: https://github.com/taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # 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 }}
+17 -6
View File
@@ -10,7 +10,7 @@ on:
- "v*.*.*"
workflow_dispatch:
schedule:
- cron: '30 0 * * 1'
- cron: '30 0 * * *'
jobs:
build:
@@ -41,15 +41,26 @@ jobs:
# else
# echo "No workaround needed for llvm-project#153385"
# fi
- name: Pick compatible clang version
id: clang-version
run: |
# both latest need to use clang-23, but oldstable and previous can just use clang
if [[ "${{ matrix.container }}" == "ubuntu-latest" ]]; then
echo "Using clang-23 package for ${{ matrix.container }}"
echo "version=clang-23" >> $GITHUB_OUTPUT
else
echo "Using default clang package for ${{ matrix.container }}"
echo "version=clang" >> $GITHUB_OUTPUT
fi
- name: Checkout repository with full history
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
ref: ${{ github.ref_name }}
- name: Cache Cargo registry
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
~/.cargo/registry
@@ -82,10 +93,10 @@ jobs:
# VERSION is the package version, COMPONENT is used in
# apt's repository config like a git repo branch
VERSION=$BASE_VERSION
if [[ ${{ forge.ref_name }} =~ ^v+[0-9]+\.+[0-9]+\.+[0-9]+$ ]]; then
if [[ ${{ forge.ref_name }} =~ ^v+[0-9]\.+[0-9]\.+[0-9]$ ]]; then
# Use the "stable" component for tagged semver releases
COMPONENT="stable"
elif [[ ${{ forge.ref_name }} =~ ^v+[0-9]+\.+[0-9]+\.+[0-9]+ ]]; then
elif [[ ${{ forge.ref_name }} =~ ^v+[0-9]\.+[0-9]\.+[0-9] ]]; then
# Use the "unstable" component for tagged semver pre-releases
COMPONENT="unstable"
else
@@ -119,7 +130,7 @@ jobs:
run: |
apt-get update -y
# Build dependencies for rocksdb
apt-get install -y liburing-dev clang
apt-get install -y liburing-dev ${{ steps.clang-version.outputs.version }}
- name: Run cargo-deb
id: cargo-deb
+5 -5
View File
@@ -16,7 +16,7 @@ on:
# - '.forgejo/workflows/build-fedora.yml'
workflow_dispatch:
schedule:
- cron: '30 0 * * 2'
- cron: '30 0 * * *'
jobs:
build:
@@ -30,14 +30,14 @@ jobs:
echo "Fedora version: $VERSION"
- name: Checkout repository with full history
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
ref: ${{ github.ref_name }}
- 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
-71
View File
@@ -1,71 +0,0 @@
name: Build / Static via Nix
concurrency:
group: "build-nix-${{ forge.ref }}"
cancel-in-progress: true
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
schedule:
- cron: '30 0 * * 3'
jobs:
build:
name: "Build ${{ matrix.filename }} Binary"
runs-on: ubuntu-latest
strategy:
matrix:
include:
- package: default-static-x86_64
filename: conduwuit-linux-static-amd64
- package: default-static-aarch64
filename: conduwuit-linux-static-arm64
- package: max-perf-static-aarch64
filename: conduwuit-linux-static-arm64-maxperf
- package: max-perf-haswell-static-x86_64
filename: conduwuit-haswell-linux-static-amd64-maxperf
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
- name: Install Lix
uses: https://github.com/samueldr/lix-gha-installer-action@a0fee77b2a98bb7c5c0ed7ae6d6ad4903dbdad0d
with:
extra_nix_config: experimental-features = nix-command flakes flake-self-attrs
- name: Build static binary
run: |
nix build .#${{ matrix.package }}
install -D result/bin/conduwuit /tmp/binaries/${{ matrix.filename }}
- name: Upload binary artifact
uses: forgejo/upload-artifact@v4
with:
name: ${{ matrix.filename }}
path: /tmp/binaries/${{ matrix.filename }}
release-binaries:
name: "Release Binaries"
runs-on: ubuntu-latest
needs:
- build
permissions:
contents: write
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download binary artifacts
uses: forgejo/download-artifact@v4
with:
pattern: conduwuit*
path: binaries
merge-multiple: true
- name: Create Release and Upload
uses: https://github.com/softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
with:
draft: true
files: binaries/*
+11 -7
View File
@@ -14,19 +14,23 @@ jobs:
name: Check changelog is added
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
persist-credentials: false
sparse-checkout: .
- name: Check for changelog entry
id: check_files
run: |
AUTH=$(echo -n "x-access-token:${{ secrets.GITHUB_TOKEN }}" | base64 -w 0)
git config --global http.${{ github.server_url }}/.extraheader "Authorization: basic $AUTH"
git clone "${{ github.event.repository.clone_url }}" repo.git --bare
git -C repo.git fetch origin pull/${{ github.event.pull_request.number }}/head
git fetch origin ${GITHUB_BASE_REF}
# Check for Added (A) or Modified (M) files in changelog.d
CHANGELOG_CHANGES=$(git -C repo.git diff --name-status ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- changelog.d/)
CHANGELOG_CHANGES=$(git diff --name-status origin/${GITHUB_BASE_REF}...HEAD -- changelog.d/)
SRC_CHANGES=$(git -C repo.git diff --name-status ${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }} -- src/)
SRC_CHANGES=$(git diff --name-status origin/${GITHUB_BASE_REF}...HEAD -- src/)
echo "Changes in changelog.d/:"
echo "$CHANGELOG_CHANGES"
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Sync repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
fetch-depth: 0
@@ -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') }}
+2 -2
View File
@@ -41,7 +41,7 @@ jobs:
DOCKER_MIRROR_TOKEN: ${{ secrets.DOCKER_MIRROR_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
@@ -55,7 +55,7 @@ jobs:
# repositories: continuwuity
- name: Install regsync
uses: https://github.com/regclient/actions/regsync-installer@9a2d4216180dbb3e2dccfa60d2dd4afd98e42ec5 # main
uses: https://github.com/regclient/actions/regsync-installer@14f9d37db17b5dc41fefd1ffdd1af4b9e2490560 # main
- name: Check what images need mirroring
run: |
+3 -3
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
@@ -48,7 +48,7 @@ jobs:
rust: ${{ steps.filter.outputs.rust }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
@@ -70,7 +70,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
+5 -5
View File
@@ -46,7 +46,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Prepare Docker build environment
@@ -100,7 +100,7 @@ jobs:
needs: build-release
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Create multi-platform manifest
@@ -133,7 +133,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Prepare max-perf Docker build environment
@@ -187,7 +187,7 @@ jobs:
needs: build-maxperf
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Create max-perf manifest
@@ -216,7 +216,7 @@ jobs:
path: binaries
merge-multiple: true
- name: Create Release and Upload
uses: https://github.com/softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
uses: https://github.com/softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with:
draft: true
files: binaries/*
+7 -7
View File
@@ -43,11 +43,11 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:43.246.1@sha256:5965c08f8ca5baff8dc9bf3a32c44ca71fef843ad94880e9696d46e1d722b0fa
image: ghcr.io/renovatebot/renovate:43.222.1@sha256:b9af3f59f3f4d92b2c41e9f4ca3ffe92400503f20158d0bd67d07a3fdbe781d2
options: --tmpfs /tmp:exec
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
show-progress: false
@@ -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
+3 -3
View File
@@ -14,7 +14,7 @@ jobs:
update-flake-hashes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: true
token: ${{ secrets.FORGEJO_TOKEN }}
@@ -27,7 +27,7 @@ jobs:
- name: Get new toolchain hash
run: |
# Set the current sha256 to an empty hash to make `nix build` calculate a new one
awk '/fromToolchainName *\{/{found=1; print; next} found && /sha256 =/{sub(/sha256 = .*/, "sha256 = lib.fakeSha256;"); found=0} 1' nix/rust.nix > temp.nix
awk '/fromToolchainFile *\{/{found=1; print; next} found && /sha256 =/{sub(/sha256 = .*/, "sha256 = lib.fakeSha256;"); found=0} 1' nix/rust.nix > temp.nix
mv temp.nix nix/rust.nix
# Build continuwuity and filter for the new hash
@@ -39,7 +39,7 @@ jobs:
sed -i "s|lib.fakeSha256|\"$new_hash\"|" nix/rust.nix
echo "New hash:"
awk -F'"' '/fromToolchainName/{found=1; next} found && /sha256 =/{print $2; found=0}' nix/rust.nix
awk -F'"' '/fromToolchainFile/{found=1; next} found && /sha256 =/{print $2; found=0}' nix/rust.nix
echo "Expected new hash:"
cat new_toolchain_hash.txt
Generated
+147 -725
View File
File diff suppressed because it is too large Load Diff
+5 -9
View File
@@ -12,7 +12,7 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "26.6.0-alpha.1"
version = "0.5.9"
[workspace.metadata.crane]
name = "conduwuit"
@@ -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"]
@@ -124,7 +124,7 @@ default-features = false
features = ["util"]
[workspace.dependencies.tower-http]
version = "0.7.0"
version = "0.6.8"
default-features = false
features = [
"add-extension",
@@ -164,7 +164,7 @@ features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde-saphyr]
version = "0.0.28"
version = "0.0.26"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
@@ -316,7 +316,7 @@ default-features = false
# Used to make working with iterators easier, was already a transitive depdendency
[workspace.dependencies.itertools]
version = "0.15.0"
version = "0.14.0"
# to parse user-friendly time durations in admin commands
#TODO: overlaps chrono?
@@ -356,7 +356,6 @@ features = [
"ring-compat",
"compat-upload-signatures",
"compat-optional-txn-pdus",
"compat-get-3pids",
"unstable-msc2666",
"unstable-msc2867",
"unstable-msc2870",
@@ -403,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"
-1
View File
@@ -23,7 +23,6 @@ ### Responsible Disclosure
1. **Contact members of the team directly** over E2EE private message.
- [@jade:ellis.link](https://matrix.to/#/@jade:ellis.link)
- [@nex:nexy7574.co.uk](https://matrix.to/#/@nex:nexy7574.co.uk)
- [@ginger:gingershaped.computer](https://matrix.to/#/@ginger:gingershaped.computer)
2. **Email the security team** at [security@continuwuity.org](mailto:security@continuwuity.org). This is not E2EE, so don't include sensitive details.
3. **Do not disclose the vulnerability publicly** until it has been addressed
4. **Provide detailed information** about the vulnerability, including:
-1
View File
@@ -1 +0,0 @@
Appservice device management as outlined in MSC4190 (part of Matrix 1.17) is now fully supported. Contributed by @ginger.
-1
View File
@@ -1 +0,0 @@
Added support for Matrix 1.16's `state_after` feature, allowing clients which understand it to sync room state changes more reliably. Contributed by @ginger.
-2
View File
@@ -1,2 +0,0 @@
Improved the performance and reliability of fetching missing events, improving network partition recovery. Contributed
by @nex.
-1
View File
@@ -1 +0,0 @@
Added static builds using Nix, allowing for Continuwuity on musl. During this, we also introduced a `max-perf-haswell` package, separating it from `max-perf`, so you may want to swap to this if you are on NixOS. Contributed by @Henry-Hiles (QuadRadical).
-1
View File
@@ -1 +0,0 @@
Added support for MSC4380 invite blocking, which has become part of the Matrix specification in v1.18. Contributed by @nex.
-1
View File
@@ -1 +0,0 @@
Added `!admin debug get-state-at` command
-1
View File
@@ -1 +0,0 @@
Added support for linking an external identity provider with OIDC. Contributed by @ginger.
-1
View File
@@ -1 +0,0 @@
Adjusted legacy sync logic to no longer use the `roomsynctoken_shortstatehash` database column. Once this change has been confirmed to be stable and reliable, a future update will remove it entirely, significantly decreasing database sizes. Contributed by @ginger.
+1 -107
View File
@@ -297,7 +297,7 @@
# This item is undocumented. Please contribute documentation for it.
#
#max_fetch_prev_events = 1024
#max_fetch_prev_events = 192
# How many incoming federation transactions the server is willing to be
# processing at any given time before it becomes overloaded and starts
@@ -643,14 +643,6 @@
#
#default_room_acl_deny =
# The number of forward extremities to tolerate in a room before
# attempting to manually squash them with a "dummy event". Setting this
# above 20 will hinder its efficacy, and setting it below 5 will cause
# more dummy events to be sent than necessary (which increases federation
# traffic).
#
#dummy_event_threshold = 10
# Enable OpenTelemetry OTLP tracing export. This replaces the deprecated
# Jaeger exporter. Traces will be sent via OTLP to a collector (such as
# Jaeger) that supports the OpenTelemetry Protocol.
@@ -1434,11 +1426,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
@@ -2028,96 +2015,3 @@
# 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"
+1 -1
View File
@@ -50,7 +50,7 @@ EOF
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.20.1
ENV BINSTALL_VERSION=1.20.0
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
+1 -1
View File
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.20.1
ENV BINSTALL_VERSION=1.20.0
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
+2 -8
View File
@@ -47,15 +47,9 @@ #### Performance-optimised builds
### Nix
If you wish to generate a static binary, you can do so using Nix: `nix build git+https://forgejo.ellis.link/continuwuation/continuwuity#packageName`, where `packageName` is one of:
Theres a Nix package defined in our flake, available for Linux and MacOS. Add continuwuity as an input to your flake, and use `inputs.continuwuity.packages.${system}.default` to get a working Continuwuity package.
- `default-static-x86_64`
- `default-static-aarch64`
- `max-perf-static-x86_64`
- `max-perf-haswell-static-x86_64`
- `max-perf-static-aarch64`
`max-perf` takes longer to build, but has more runtime optimizations. Haswell builds are optimized for modern CPUs.
If you simply wish to generate a binary using Nix, you can run `nix build git+https://forgejo.ellis.link/continuwuation/continuwuity` to generate a binary in `result/bin/conduwuit`.
### Compiling
+1 -8
View File
@@ -47,16 +47,9 @@ ### Available options
- `extraEnvironment`: Extra environment variables to pass to the Continuwuity server
- `package`: The Continuwuity package to use, defaults to `pkgs.matrix-continuwuity`
- You may want to override this to be from our flake, for faster updates and unstable versions:
```nix
package = inputs.continuwuity.packages.${pkgs.stdenv.hostPlatform.system}.packageName;
package = inputs.continuwuity.packages.${pkgs.stdenv.hostPlatform.system}.default;
```
Where `packageName` is one of:
- `default`
- `max-perf`: Takes longer to build, but has more runtime optimizations
- `max-perf-haswell`: Optimized for modern CPUs, don't use if your CPU is not Haswell or later.
- `admin.enable`: Whether to add the `conduwuit` binary to `PATH` for administration (enabled by default)
- `settings`: The Continuwuity configuration
@@ -6,10 +6,10 @@
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
},
{
"id": 14,
"id": 13,
"mention_room": true,
"date": "2026-06-20",
"message": "[v0.5.10](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.10) has been released. It is a security release, so we suggest you update as soon as possible. Don't forget to also join [our announcements room](https://matrix.to/#/!jIdNjSM5X-V5JVx2h2kAhUZIIQ08GyzPL55NFZAH1vM/%24K1ISNKIqfNiZzsNVCaTt2E7ZtNeP6Dsy6sbz9l3rO0A?via=ellis.link&via=gingershaped.computer&via=matrix.org)."
"date": "2026-05-08",
"message": "[v0.5.9](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.9) has been released, fixing a few low-severity federation-related vulnerabilities. It is recommended you read the changelog and update as soon as possible. There are no new features or other changes in this release, only related bugfixes. Deployments tracking the main branch should also update to the latest commit."
}
]
}
+1 -13
View File
@@ -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
-1
View File
@@ -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
-13
View File
@@ -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
+4 -8
View File
@@ -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
+1 -1
View File
@@ -10,7 +10,7 @@ ## Continuwuity issues
### Slow joins to rooms
Some slowness is to be expected if you're the first person on your homeserver to join a room (which will
Some slowness is to be expected if you're the first person on your homserver to join a room (which will
always be the case for single-user homeservers). In this situation, your homeserver has to verify the signatures of
all of the state events sent by other servers before your join. To make this process as fast as possible, make sure you have
multiple fast, trusted servers listed in `trusted_servers` in your configuration, and ensure
+14
View File
@@ -0,0 +1,14 @@
{ inputs, ... }:
{
perSystem =
{
pkgs,
self',
...
}:
{
_module.args.craneLib = (inputs.crane.mkLib pkgs).overrideToolchain (
pkgs: self'.packages.stable-toolchain
);
};
}
+1
View File
@@ -1,6 +1,7 @@
{
imports = [
./rust.nix
./crane.nix
./packages
./devshell.nix
./fmt.nix
+28 -29
View File
@@ -1,6 +1,7 @@
{ inputs, ... }: {
{
perSystem =
{
craneLib,
self',
lib,
pkgs,
@@ -8,36 +9,34 @@
}:
{
# 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
{
packages = [
self'.packages.rocksdb
pkgs.nodejs
pkgs.pkg-config
devShells.default = craneLib.devShell {
packages = [
self'.packages.rocksdb
pkgs.nodejs
pkgs.pkg-config
]
++ lib.optionals pkgs.stdenv.isLinux [
pkgs.liburing
pkgs.rust-jemalloc-sys-unprefixed
];
env = {
LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.llvmPackages.libclang.lib ];
LD_LIBRARY_PATH = lib.makeLibraryPath (
[
pkgs.stdenv.cc.cc.lib
]
++ lib.optionals pkgs.stdenv.isLinux [
pkgs.liburing
pkgs.rust-jemalloc-sys-unprefixed
];
env = {
LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.llvmPackages.libclang.lib ];
LD_LIBRARY_PATH = lib.makeLibraryPath (
[
pkgs.stdenv.cc.cc.lib
]
++ lib.optionals pkgs.stdenv.isLinux [
pkgs.liburing
pkgs.jemalloc
]
);
}
// lib.optionalAttrs pkgs.stdenv.isLinux {
PKG_CONFIG_PATH = lib.makeSearchPath "lib/pkgconfig" [
pkgs.liburing.dev
];
};
};
pkgs.jemalloc
]
);
}
// lib.optionalAttrs pkgs.stdenv.isLinux {
PKG_CONFIG_PATH = lib.makeSearchPath "lib/pkgconfig" [
pkgs.liburing.dev
];
};
};
};
}
+5 -13
View File
@@ -2,14 +2,14 @@
lib,
self,
stdenv,
rocksdb,
liburing,
craneLib,
pkg-config,
liburing,
rustPlatform,
cargoExtraArgs ? "",
rustflags ? "",
target_cpu ? null,
rocksdb,
profile ? "release",
}:
let
@@ -28,26 +28,18 @@ let
};
attrs = {
__structuredAttrs = true;
strictDeps = true;
inherit src;
nativeBuildInputs = [
pkg-config
rustPlatform.bindgenHook
];
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [ liburing ];
env = {
ROCKSDB_INCLUDE_DIR = "${rocksdb}/include";
ROCKSDB_LIB_DIR = "${rocksdb}/lib";
CARGO_PROFILE = profile;
RUSTFLAGS = rustflags;
}
// (lib.optionalAttrs (rocksdb != null) {
ROCKSDB_INCLUDE_DIR = "${rocksdb}/include";
ROCKSDB_LIB_DIR = "${rocksdb}/lib";
})
// (lib.optionalAttrs (target_cpu != null) {
TARGET_CPU = target_cpu;
});
@@ -59,7 +51,7 @@ craneLib.buildPackage (
cargoArtifacts = craneLib.buildDepsOnly attrs;
# Needed to make continuwuity link to rocksdb
postFixup = lib.optionalString (stdenv.hostPlatform.isLinux && rocksdb != null) ''
postFixup = lib.optionalString stdenv.hostPlatform.isLinux ''
old_rpath="$(patchelf --print-rpath $out/bin/conduwuit)"
extra_rpath="${
lib.makeLibraryPath [
+21 -74
View File
@@ -1,5 +1,4 @@
{
inputs,
self,
...
}:
@@ -7,84 +6,32 @@
perSystem =
{
self',
lib,
pkgs,
inputs',
system,
craneLib,
mkToolchain,
...
}:
{
packages =
let
mkPackages =
pkgs:
let
fnx = inputs'.fenix.packages;
packages = {
rocksdb = pkgs.callPackage ./rocksdb.nix { };
default = pkgs.callPackage ./continuwuity.nix {
inherit self craneLib;
inherit (self'.packages) rocksdb;
# extra features via `cargoExtraArgs`
cargoExtraArgs = "-F http3";
# extra RUSTFLAGS via `rustflags`
# the stuff below is required for http3
rustflags = "--cfg reqwest_unstable";
};
# users may also override this with other cargo profiles to build for other feature sets
# for features configuration see `default` package which enables http3 by default
isStatic = pkgs.stdenv.hostPlatform.isMusl;
craneLib = (inputs.crane.mkLib pkgs).overrideToolchain (
_:
if isStatic then
fnx.combine [
self'.packages.stable-toolchain
(mkToolchain fnx.targets.${pkgs.stdenv.hostPlatform.config}).rust-std
]
else
self'.packages.stable-toolchain
);
default = pkgs.callPackage ./continuwuity.nix {
inherit self craneLib;
liburing = (if isStatic then pkgs.pkgsStatic else pkgs).liburing;
rocksdb = if isStatic then null else self'.packages.rocksdb;
# extra features via `cargoExtraArgs`
cargoExtraArgs = "-F http3";
# extra RUSTFLAGS via `rustflags`
# the stuff below is required for http3
rustflags = "--cfg reqwest_unstable";
};
# users may also override this with other cargo profiles to build for other feature sets
# for features configuration see `default` package which enables http3 by default
max-perf = default.override {
# compiles slower but with more thorough optimizations
profile = "release-max-perf";
};
max-perf-haswell = max-perf.override {
# compiles explicitly for haswell arch cpus
target_cpu = "haswell";
};
in
{
inherit default max-perf max-perf-haswell;
};
in
{
rocksdb = pkgs.callPackage ./rocksdb.nix { };
}
// (mkPackages pkgs)
// (lib.mapAttrs' (name: value: lib.nameValuePair "${name}-static-x86_64" value) (
mkPackages (
import inputs.nixpkgs {
localSystem = system;
crossSystem = "x86_64-unknown-linux-musl";
}
)
))
// (lib.mapAttrs' (name: value: lib.nameValuePair "${name}-static-aarch64" value) (
mkPackages (
import inputs.nixpkgs {
localSystem = system;
crossSystem = "aarch64-unknown-linux-musl";
}
)
));
# example: different compilation profile and different target_cpu
max-perf-haswell = self'.packages.default.override {
# compiles explicitly for haswell arch cpus
target_cpu = "haswell";
# compiles slower but with more thorough optimizations
profile = "release-max-perf";
};
};
};
}
+9 -13
View File
@@ -2,26 +2,22 @@
{
perSystem =
{
system,
lib,
inputs',
pkgs,
...
}:
let
mkToolchain =
target:
target.fromToolchainName {
name = (lib.importTOML "${inputs.self}/rust-toolchain.toml").toolchain.channel;
sha256 = "sha256-h+t2xTBz5yt2YIO+1VMIIGlCU7gyp2LYOFvaV1nwOXU=";
};
in
{
_module.args = { inherit mkToolchain; };
packages =
let
fnx = inputs'.fenix.packages;
stable-toolchain = (mkToolchain fnx).toolchain;
fnx = inputs.fenix.packages.${system};
stable-toolchain = fnx.fromToolchainFile {
file = inputs.self + "/rust-toolchain.toml";
# See also `rust-toolchain.toml`
sha256 = "sha256-mvUGEOHYJpn3ikC5hckneuGixaC+yGrkMM/liDIDgoU=";
};
in
{
inherit stable-toolchain;
+25 -25
View File
@@ -383,18 +383,18 @@
}
},
"node_modules/@rspress/core": {
"version": "2.0.15",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.15.tgz",
"integrity": "sha512-epLmUXYscNRw/GtQZx2oknoBE9wKbCrUGEOrQEDI4Qq8X32GdM4d7itzuHsliY7q3IbffKx8rMVbvlmygEocTQ==",
"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.15",
"@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.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.1.0",
"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.15",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.15.tgz",
"integrity": "sha512-bPf/KIHH7Y6huLTtK6JXwRfxM7zKjksoxm46+IBsF1wisw0doKkEKR9HwJydxWnykyKBbA2cuZOaoT4h174Z1w==",
"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.15",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.15.tgz",
"integrity": "sha512-z1hbyGP79ZXdSGJxiWw7ZjmX8qW0q9nXMDxr14cVEg/wdj7ToVzGtZHw0wvTPE0YiKG3BMiGkVNfE1rdOaPXiQ==",
"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.15",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.15.tgz",
"integrity": "sha512-o8aYwEzNuTmWnmKe91ntPv+34u3RbtAe+rcK9XC5MANOlgncwOaCs3bUa8/B1/llwyLoNgrpi+VB9bEiU11ZRQ==",
"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"
}
},
@@ -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": {
+1 -1
View File
@@ -10,7 +10,7 @@
[toolchain]
profile = "minimal"
channel = "1.96.1"
channel = "1.96.0"
components = [
# For rust-analyzer
"rust-src",
+10 -25
View File
@@ -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},
@@ -19,48 +18,44 @@
#[derive(Debug, Parser)]
#[command(name = conduwuit_core::BRANDING, 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,
+68 -64
View File
@@ -1,76 +1,80 @@
use conduwuit::{Err, Result, checked};
use futures::{FutureExt, StreamExt, TryFutureExt};
impl crate::Context<'_> {
pub(super) async fn register(&self) -> Result {
let body = &self.body;
let body_len = self.body.len();
if body_len < 2
|| !body[0].trim().starts_with("```")
|| body.last().unwrap_or(&"").trim() != "```"
{
return Err!("Expected code block in command body. Add --help for details.");
}
use crate::admin_command;
let range = 1..checked!(body_len - 1)?;
let appservice_config_body = body[range].join("\n");
let parsed_config = serde_saphyr::from_str(&appservice_config_body);
match parsed_config {
| Err(e) => return Err!("Could not parse appservice config as YAML: {e}"),
| Ok(registration) => match self
.services
.appservice
.register_appservice(&registration, &appservice_config_body)
.await
.map(|()| registration.id)
{
| Err(e) => return Err!("Failed to register appservice: {e}"),
| Ok(id) => write!(self, "Appservice registered with ID: {id}"),
},
}
.await
#[admin_command]
pub(super) async fn register(&self) -> Result {
let body = &self.body;
let body_len = self.body.len();
if body_len < 2
|| !body[0].trim().starts_with("```")
|| body.last().unwrap_or(&"").trim() != "```"
{
return Err!("Expected code block in command body. Add --help for details.");
}
pub(super) async fn unregister(&self, appservice_identifier: String) -> Result {
match self
let range = 1..checked!(body_len - 1)?;
let appservice_config_body = body[range].join("\n");
let parsed_config = serde_saphyr::from_str(&appservice_config_body);
match parsed_config {
| Err(e) => return Err!("Could not parse appservice config as YAML: {e}"),
| Ok(registration) => match self
.services
.appservice
.unregister_appservice(&appservice_identifier)
.register_appservice(&registration, &appservice_config_body)
.await
.map(|()| registration.id)
{
| Err(e) => return Err!("Failed to unregister appservice: {e}"),
| Ok(()) => write!(self, "Appservice unregistered."),
}
.await
}
pub(super) async fn show_appservice_config(&self, appservice_identifier: String) -> Result {
match self
.services
.appservice
.get_registration(&appservice_identifier)
.await
{
| None => return Err!("Appservice does not exist."),
| Some(config) => {
let config_str = serde_saphyr::to_string(&config)?;
write!(self, "Config for {appservice_identifier}:\n\n```yaml\n{config_str}\n```")
},
}
.await
}
pub(super) async fn list_registered(&self) -> Result {
self.services
.appservice
.iter_ids()
.collect()
.map(Ok)
.and_then(|appservices: Vec<_>| {
let len = appservices.len();
let list = appservices.join(", ");
write!(self, "Appservices ({len}): {list}")
})
.await
| Err(e) => return Err!("Failed to register appservice: {e}"),
| Ok(id) => write!(self, "Appservice registered with ID: {id}"),
},
}
.await
}
#[admin_command]
pub(super) async fn unregister(&self, appservice_identifier: String) -> Result {
match self
.services
.appservice
.unregister_appservice(&appservice_identifier)
.await
{
| Err(e) => return Err!("Failed to unregister appservice: {e}"),
| Ok(()) => write!(self, "Appservice unregistered."),
}
.await
}
#[admin_command]
pub(super) async fn show_appservice_config(&self, appservice_identifier: String) -> Result {
match self
.services
.appservice
.get_registration(&appservice_identifier)
.await
{
| None => return Err!("Appservice does not exist."),
| Some(config) => {
let config_str = serde_saphyr::to_string(&config)?;
write!(self, "Config for {appservice_identifier}:\n\n```yaml\n{config_str}\n```")
},
}
.await
}
#[admin_command]
pub(super) async fn list_registered(&self) -> Result {
self.services
.appservice
.iter_ids()
.collect()
.map(Ok)
.and_then(|appservices: Vec<_>| {
let len = appservices.len();
let list = appservices.join(", ");
write!(self, "Appservices ({len}): {list}")
})
.await
}
+15 -20
View File
@@ -1,28 +1,23 @@
use conduwuit::Result;
use conduwuit_macros::implement;
use futures::StreamExt;
use crate::Context;
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 query_time = timer.elapsed();
#[implement(Context, params = "<'_>")]
pub(super) async fn check_all_users(&self) -> Result {
let timer = tokio::time::Instant::now();
let users = self.services.users.stream().collect::<Vec<_>>().await;
let query_time = timer.elapsed();
let total = users.len();
let err_count = users.iter().filter(|_user| false).count();
let ok_count = users.iter().filter(|_user| true).count();
let total = users.len();
let err_count = users.iter().filter(|_user| false).count();
let ok_count = users.iter().filter(|_user| true).count();
self.write_str(&format!(
"Database query completed in {query_time:?}:\n\n```\nTotal entries: \
{total:?}\nFailure/Invalid user count: {err_count:?}\nSuccess/Valid user count: \
{ok_count:?}\n```"
))
.await
}
self.write_str(&format!(
"Database query completed in {query_time:?}:\n\n```\nTotal entries: \
{total:?}\nFailure/Invalid user count: {err_count:?}\nSuccess/Valid user count: \
{ok_count:?}\n```"
))
.await
}
+976 -1059
View File
File diff suppressed because it is too large Load Diff
-13
View File
@@ -95,14 +95,6 @@ pub enum DebugCommand {
room_id: OwnedRoomOrAliasId,
},
/// Gets all the room state events at the specified event.
///
/// State at event might not be available for some PDUs, such as rejected
/// ones.
GetStateAt {
event_id: OwnedEventId,
},
/// Get and display signing keys from local cache or remote server.
GetSigningKeys {
server_name: Option<OwnedServerName>,
@@ -245,11 +237,6 @@ pub enum DebugCommand {
/// Send a test email to the invoking admin's email address
SendTestEmail,
/// Lists room IDs by forward extremity count in descending order
RoomsByExtremityCount {
page: Option<usize>,
},
/// Developer test stubs
#[command(subcommand)]
#[allow(non_snake_case)]
+28 -23
View File
@@ -1,6 +1,6 @@
use conduwuit::{Err, Result};
use crate::admin_command_dispatch;
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[derive(Debug, clap::Subcommand)]
@@ -11,32 +11,37 @@ pub enum TesterCommand {
Timer,
}
impl crate::Context<'_> {
#[rustfmt::skip]
async fn panic(&self) -> Result {
panic!("panicked")
}
#[rustfmt::skip]
#[admin_command]
async fn panic(&self) -> Result {
#[rustfmt::skip]
async fn failure(&self) -> Result {
Err!("failed")
}
panic!("panicked")
}
#[inline(never)]
#[rustfmt::skip]
async fn tester(&self) -> Result {
self.write_str("Ok").await
}
#[rustfmt::skip]
#[admin_command]
async fn failure(&self) -> Result {
#[inline(never)]
#[rustfmt::skip]
async fn timer(&self) -> Result {
let started = std::time::Instant::now();
timed(self.body);
Err!("failed")
}
let elapsed = started.elapsed();
self.write_str(&format!("completed in {elapsed:#?}")).await
}
#[inline(never)]
#[rustfmt::skip]
#[admin_command]
async fn tester(&self) -> Result {
self.write_str("Ok").await
}
#[inline(never)]
#[rustfmt::skip]
#[admin_command]
async fn timer(&self) -> Result {
let started = std::time::Instant::now();
timed(self.body);
let elapsed = started.elapsed();
self.write_str(&format!("completed in {elapsed:#?}")).await
}
#[inline(never)]
+115 -119
View File
@@ -4,131 +4,127 @@
use futures::StreamExt;
use ruma::{OwnedRoomId, OwnedServerName, OwnedUserId};
use crate::get_room_info;
use crate::{admin_command, get_room_info};
impl crate::Context<'_> {
pub(super) async fn disable_room(&self, room_id: OwnedRoomId) -> Result {
self.bail_restricted()?;
self.services.rooms.metadata.disable_room(&room_id, true);
self.write_str("Room disabled.").await
}
#[admin_command]
pub(super) async fn disable_room(&self, room_id: OwnedRoomId) -> Result {
self.bail_restricted()?;
self.services.rooms.metadata.disable_room(&room_id, true);
self.write_str("Room disabled.").await
}
pub(super) async fn enable_room(&self, room_id: OwnedRoomId) -> Result {
self.bail_restricted()?;
self.services.rooms.metadata.disable_room(&room_id, false);
self.write_str("Room enabled.").await
}
#[admin_command]
pub(super) async fn enable_room(&self, room_id: OwnedRoomId) -> Result {
self.bail_restricted()?;
self.services.rooms.metadata.disable_room(&room_id, false);
self.write_str("Room enabled.").await
}
pub(super) async fn incoming_federation(&self) -> Result {
let msg = {
let map = self
.services
.rooms
.event_handler
.federation_handletime
.read();
let mut msg = format!(
"Handling {} incoming PDUs across {} active transactions:\n",
map.len(),
self.services.transactions.txn_active_handle_count()
);
for (r, (e, i)) in map.iter() {
let elapsed = i.elapsed();
writeln!(
msg,
"{} {}: {}m{}s",
r,
e,
elapsed.as_secs() / 60,
elapsed.as_secs() % 60
)?;
}
msg
};
self.write_str(&msg).await
}
pub(super) async fn fetch_support_well_known(&self, server_name: OwnedServerName) -> Result {
let response = self
.services
.client
.default
.get(format!("https://{server_name}/.well-known/matrix/support"))
.send()
.await?;
let text = response
.limit_read_text(
self.services
.config
.max_request_size
.try_into()
.expect("u64 fits into usize"),
)
.await?;
if text.is_empty() {
return Err!("Response text/body is empty.");
}
if text.len() > 1500 {
return Err!(
"Response text/body is over 1500 characters, assuming no support well-known.",
);
}
let json: serde_json::Value = match serde_json::from_str(&text) {
| Ok(json) => json,
| Err(_) => {
return Err!("Response text/body is not valid JSON.",);
},
};
let pretty_json: String = match serde_json::to_string_pretty(&json) {
| Ok(json) => json,
| Err(_) => {
return Err!("Response text/body is not valid JSON.",);
},
};
self.write_str(&format!("Got JSON response:\n\n```json\n{pretty_json}\n```"))
.await
}
pub(super) async fn remote_user_in_rooms(&self, user_id: OwnedUserId) -> Result {
if user_id.server_name() == self.services.server.name {
return Err!(
"User belongs to our server, please use `list-joined-rooms` user admin command \
instead.",
);
}
let mut rooms: Vec<(OwnedRoomId, u64, String)> = self
#[admin_command]
pub(super) async fn incoming_federation(&self) -> Result {
let msg = {
let map = self
.services
.rooms
.state_cache
.rooms_joined(&user_id)
.then(async |room_id| get_room_info(self.services, &room_id).await)
.collect()
.await;
.event_handler
.federation_handletime
.read();
if rooms.is_empty() {
return Err!("User is not in any rooms.");
let mut msg = format!(
"Handling {} incoming PDUs across {} active transactions:\n",
map.len(),
self.services.transactions.txn_active_handle_count()
);
for (r, (e, i)) in map.iter() {
let elapsed = i.elapsed();
writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60)?;
}
msg
};
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let num = rooms.len();
let body = rooms
.iter()
.map(|(id, members, name)| format!("{id} | Members: {members} | Name: {name}"))
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms {user_id} shares with us ({num}):\n```\n{body}\n```"))
.await
}
self.write_str(&msg).await
}
#[admin_command]
pub(super) async fn fetch_support_well_known(&self, server_name: OwnedServerName) -> Result {
let response = self
.services
.client
.default
.get(format!("https://{server_name}/.well-known/matrix/support"))
.send()
.await?;
let text = response
.limit_read_text(
self.services
.config
.max_request_size
.try_into()
.expect("u64 fits into usize"),
)
.await?;
if text.is_empty() {
return Err!("Response text/body is empty.");
}
if text.len() > 1500 {
return Err!(
"Response text/body is over 1500 characters, assuming no support well-known.",
);
}
let json: serde_json::Value = match serde_json::from_str(&text) {
| Ok(json) => json,
| Err(_) => {
return Err!("Response text/body is not valid JSON.",);
},
};
let pretty_json: String = match serde_json::to_string_pretty(&json) {
| Ok(json) => json,
| Err(_) => {
return Err!("Response text/body is not valid JSON.",);
},
};
self.write_str(&format!("Got JSON response:\n\n```json\n{pretty_json}\n```"))
.await
}
#[admin_command]
pub(super) async fn remote_user_in_rooms(&self, user_id: OwnedUserId) -> Result {
if user_id.server_name() == self.services.server.name {
return Err!(
"User belongs to our server, please use `list-joined-rooms` user admin command \
instead.",
);
}
let mut rooms: Vec<(OwnedRoomId, u64, String)> = self
.services
.rooms
.state_cache
.rooms_joined(&user_id)
.then(async |room_id| get_room_info(self.services, &room_id).await)
.collect()
.await;
if rooms.is_empty() {
return Err!("User is not in any rooms.");
}
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let num = rooms.len();
let body = rooms
.iter()
.map(|(id, members, name)| format!("{id} | Members: {members} | Name: {name}"))
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms {user_id} shares with us ({num}):\n```\n{body}\n```"))
.await
}
+352 -354
View File
@@ -9,403 +9,401 @@
use ruma::{OwnedEventId, OwnedMxcUri, OwnedServerName};
use service::media::mxc::Mxc;
use crate::utils::parse_local_user_id;
use crate::{admin_command, utils::parse_local_user_id};
impl crate::Context<'_> {
pub(super) async fn delete(
&self,
mxc: Option<OwnedMxcUri>,
event_id: Option<OwnedEventId>,
) -> Result {
self.bail_restricted()?;
#[admin_command]
pub(super) async fn delete(
&self,
mxc: Option<OwnedMxcUri>,
event_id: Option<OwnedEventId>,
) -> Result {
self.bail_restricted()?;
if event_id.is_some() && mxc.is_some() {
return Err!("Please specify either an MXC or an event ID, not both.",);
}
if event_id.is_some() && mxc.is_some() {
return Err!("Please specify either an MXC or an event ID, not both.",);
}
if let Some(mxc) = mxc {
trace!("Got MXC URL: {mxc}");
self.services
.media
.delete(&mxc.as_str().try_into()?)
.await?;
if let Some(mxc) = mxc {
trace!("Got MXC URL: {mxc}");
self.services
.media
.delete(&mxc.as_str().try_into()?)
.await?;
return self
.write_str("Deleted the MXC from our database and on our filesystem.")
.await;
}
return self
.write_str("Deleted the MXC from our database and on our filesystem.")
.await;
}
if let Some(event_id) = event_id {
trace!("Got event ID to delete media from: {event_id}");
if let Some(event_id) = event_id {
trace!("Got event ID to delete media from: {event_id}");
let mut mxc_urls = Vec::with_capacity(4);
let mut mxc_urls = Vec::with_capacity(4);
// parsing the PDU for any MXC URLs begins here
match self.services.rooms.timeline.get_pdu_json(&event_id).await {
| Ok(event_json) => {
if let Some(content_key) = event_json.get("content") {
debug!("Event ID has \"content\".");
let content_obj = content_key.as_object();
// parsing the PDU for any MXC URLs begins here
match self.services.rooms.timeline.get_pdu_json(&event_id).await {
| Ok(event_json) => {
if let Some(content_key) = event_json.get("content") {
debug!("Event ID has \"content\".");
let content_obj = content_key.as_object();
if let Some(content) = content_obj {
// 1. attempts to parse the "url" key
debug!("Attempting to go into \"url\" key for main media file");
if let Some(url) = content.get("url") {
debug!("Got a URL in the event ID {event_id}: {url}");
if let Some(content) = content_obj {
// 1. attempts to parse the "url" key
debug!("Attempting to go into \"url\" key for main media file");
if let Some(url) = content.get("url") {
debug!("Got a URL in the event ID {event_id}: {url}");
if url.to_string().starts_with("\"mxc://") {
debug!("Pushing URL {url} to list of MXCs to delete");
let final_url = url.to_string().replace('"', "");
mxc_urls.push(final_url);
if url.to_string().starts_with("\"mxc://") {
debug!("Pushing URL {url} to list of MXCs to delete");
let final_url = url.to_string().replace('"', "");
mxc_urls.push(final_url);
} else {
info!(
"Found a URL in the event ID {event_id} but did not start \
with mxc://, ignoring"
);
}
}
// 2. attempts to parse the "info" key
debug!("Attempting to go into \"info\" key for thumbnails");
if let Some(info_key) = content.get("info") {
debug!("Event ID has \"info\".");
let info_obj = info_key.as_object();
if let Some(info) = info_obj {
if let Some(thumbnail_url) = info.get("thumbnail_url") {
debug!("Found a thumbnail_url in info key: {thumbnail_url}");
if thumbnail_url.to_string().starts_with("\"mxc://") {
debug!(
"Pushing thumbnail URL {thumbnail_url} to list of \
MXCs to delete"
);
let final_thumbnail_url =
thumbnail_url.to_string().replace('"', "");
mxc_urls.push(final_thumbnail_url);
} else {
info!(
"Found a thumbnail URL in the event ID {event_id} \
but did not start with mxc://, ignoring"
);
}
} else {
info!(
"Found a URL in the event ID {event_id} but did not \
start with mxc://, ignoring"
"No \"thumbnail_url\" key in \"info\" key, assuming no \
thumbnails."
);
}
}
}
// 2. attempts to parse the "info" key
debug!("Attempting to go into \"info\" key for thumbnails");
if let Some(info_key) = content.get("info") {
debug!("Event ID has \"info\".");
let info_obj = info_key.as_object();
// 3. attempts to parse the "file" key
debug!("Attempting to go into \"file\" key");
if let Some(file_key) = content.get("file") {
debug!("Event ID has \"file\".");
let file_obj = file_key.as_object();
if let Some(info) = info_obj {
if let Some(thumbnail_url) = info.get("thumbnail_url") {
debug!(
"Found a thumbnail_url in info key: {thumbnail_url}"
);
if let Some(file) = file_obj {
if let Some(url) = file.get("url") {
debug!("Found url in file key: {url}");
if thumbnail_url.to_string().starts_with("\"mxc://") {
debug!(
"Pushing thumbnail URL {thumbnail_url} to list \
of MXCs to delete"
);
let final_thumbnail_url =
thumbnail_url.to_string().replace('"', "");
mxc_urls.push(final_thumbnail_url);
} else {
info!(
"Found a thumbnail URL in the event ID \
{event_id} but did not start with mxc://, \
ignoring"
);
}
if url.to_string().starts_with("\"mxc://") {
debug!("Pushing URL {url} to list of MXCs to delete");
let final_url = url.to_string().replace('"', "");
mxc_urls.push(final_url);
} else {
info!(
"No \"thumbnail_url\" key in \"info\" key, assuming \
no thumbnails."
warn!(
"Found a URL in the event ID {event_id} but did not \
start with mxc://, ignoring"
);
}
} else {
error!("No \"url\" key in \"file\" key.");
}
}
// 3. attempts to parse the "file" key
debug!("Attempting to go into \"file\" key");
if let Some(file_key) = content.get("file") {
debug!("Event ID has \"file\".");
let file_obj = file_key.as_object();
if let Some(file) = file_obj {
if let Some(url) = file.get("url") {
debug!("Found url in file key: {url}");
if url.to_string().starts_with("\"mxc://") {
debug!("Pushing URL {url} to list of MXCs to delete");
let final_url = url.to_string().replace('"', "");
mxc_urls.push(final_url);
} else {
warn!(
"Found a URL in the event ID {event_id} but did \
not start with mxc://, ignoring"
);
}
} else {
error!("No \"url\" key in \"file\" key.");
}
}
}
} else {
return Err!(
"Event ID does not have a \"content\" key or failed parsing the \
event ID JSON.",
);
}
} else {
return Err!(
"Event ID does not have a \"content\" key, this is not a message or \
an event type that contains media.",
"Event ID does not have a \"content\" key or failed parsing the \
event ID JSON.",
);
}
},
| _ => {
return Err!("Event ID does not exist or is not known to us.",);
},
}
if mxc_urls.is_empty() {
return Err!("Parsed event ID but found no MXC URLs.",);
}
let mut mxc_deletion_count: usize = 0;
for mxc_url in mxc_urls {
match self
.services
.media
.delete(&mxc_url.as_str().try_into()?)
.await
{
| Ok(()) => {
debug_info!(
"Successfully deleted {mxc_url} from filesystem and database"
);
mxc_deletion_count = mxc_deletion_count.saturating_add(1);
},
| Err(e) => {
debug_warn!(
"Failed to delete {mxc_url}, ignoring error and skipping: {e}"
);
continue;
},
} else {
return Err!(
"Event ID does not have a \"content\" key, this is not a message or an \
event type that contains media.",
);
}
}
return self
.write_str(&format!(
"Deleted {mxc_deletion_count} total MXCs from our database and the \
filesystem from event ID {event_id}."
))
.await;
},
| _ => {
return Err!("Event ID does not exist or is not known to us.",);
},
}
Err!(
"Please specify either an MXC using --mxc or an event ID using --event-id of the \
message containing an image. See --help for details."
)
}
pub(super) async fn delete_list(&self) -> Result {
self.bail_restricted()?;
if self.body.len() < 2
|| !self.body[0].trim().starts_with("```")
|| self.body.last().unwrap_or(&"").trim() != "```"
{
return Err!("Expected code block in command body. Add --help for details.",);
if mxc_urls.is_empty() {
return Err!("Parsed event ID but found no MXC URLs.",);
}
let mut failed_parsed_mxcs: usize = 0;
let mxc_list = self
.body
.to_vec()
.drain(1..self.body.len().checked_sub(1).unwrap())
.filter_map(|mxc_s| {
mxc_s
.try_into()
.inspect_err(|e| {
debug_warn!("Failed to parse user-provided MXC URI: {e}");
failed_parsed_mxcs = failed_parsed_mxcs.saturating_add(1);
})
.ok()
})
.collect::<Vec<Mxc<'_>>>();
let mut mxc_deletion_count: usize = 0;
for mxc in &mxc_list {
trace!(%failed_parsed_mxcs, %mxc_deletion_count, "Deleting MXC {mxc} in bulk");
match self.services.media.delete(mxc).await {
for mxc_url in mxc_urls {
match self
.services
.media
.delete(&mxc_url.as_str().try_into()?)
.await
{
| Ok(()) => {
debug_info!("Successfully deleted {mxc} from filesystem and database");
debug_info!("Successfully deleted {mxc_url} from filesystem and database");
mxc_deletion_count = mxc_deletion_count.saturating_add(1);
},
| Err(e) => {
debug_warn!("Failed to delete {mxc}, ignoring error and skipping: {e}");
debug_warn!("Failed to delete {mxc_url}, ignoring error and skipping: {e}");
continue;
},
}
}
self.write_str(&format!(
"Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our \
database and the filesystem. {failed_parsed_mxcs} MXCs failed to be parsed from \
the database.",
))
.await
return self
.write_str(&format!(
"Deleted {mxc_deletion_count} total MXCs from our database and the filesystem \
from event ID {event_id}."
))
.await;
}
pub(super) async fn delete_past_remote_media(
&self,
duration: String,
before: bool,
after: bool,
yes_i_want_to_delete_local_media: bool,
) -> Result {
self.bail_restricted()?;
if before && after {
return Err!("Please only pick one argument, --before or --after.",);
}
assert!(!(before && after), "--before and --after should not be specified together");
let direction = if after {
TimeDirection::After
} else {
TimeDirection::Before
};
let time_boundary = parse_timepoint_ago(&duration)?;
let deleted_count = self
.services
.media
.delete_all_media_within_timeframe(
time_boundary,
direction,
yes_i_want_to_delete_local_media,
)
.await?;
self.write_str(&format!("Deleted {deleted_count} total files."))
.await
}
pub(super) async fn delete_all_from_user(&self, username: String) -> Result {
let user_id = parse_local_user_id(self.services, &username)?;
let deleted_count = self.services.media.delete_from_user(&user_id).await?;
self.write_str(&format!("Deleted {deleted_count} total files."))
.await
}
pub(super) async fn delete_all_from_server(
&self,
server_name: OwnedServerName,
yes_i_want_to_delete_local_media: bool,
) -> Result {
self.bail_restricted()?;
if server_name == self.services.globals.server_name() && !yes_i_want_to_delete_local_media
{
return Err!("This command only works for remote media by default.",);
}
let Ok(all_mxcs) = self
.services
.media
.get_all_mxcs()
.await
.inspect_err(|e| error!("Failed to get MXC URIs from our database: {e}"))
else {
return Err!("Failed to get MXC URIs from our database",);
};
let mut deleted_count: usize = 0;
for mxc in all_mxcs {
let Ok(mxc_server_name) = mxc.server_name().inspect_err(|e| {
debug_warn!(
"Failed to parse MXC {mxc} server name from database, ignoring error and \
skipping: {e}"
);
}) else {
continue;
};
if mxc_server_name != server_name
|| (self.services.globals.server_is_ours(mxc_server_name)
&& !yes_i_want_to_delete_local_media)
{
trace!("skipping MXC URI {mxc}");
continue;
}
let mxc: Mxc<'_> = mxc.as_str().try_into()?;
match self.services.media.delete(&mxc).await {
| Ok(()) => {
deleted_count = deleted_count.saturating_add(1);
},
| Err(e) => {
debug_warn!("Failed to delete {mxc}, ignoring error and skipping: {e}");
continue;
},
}
}
self.write_str(&format!("Deleted {deleted_count} total files."))
.await
}
pub(super) async fn get_file_info(&self, mxc: OwnedMxcUri) -> Result {
let mxc: Mxc<'_> = mxc.as_str().try_into()?;
let metadata = self.services.media.get_metadata(&mxc).await;
self.write_str(&format!("```\n{metadata:#?}\n```")).await
}
pub(super) async fn get_remote_file(
&self,
mxc: OwnedMxcUri,
server: Option<OwnedServerName>,
timeout: u32,
) -> Result {
let mxc: Mxc<'_> = mxc.as_str().try_into()?;
let timeout = Duration::from_millis(timeout.into());
let mut result = self
.services
.media
.fetch_remote_content(&mxc, None, server.as_deref(), timeout)
.await?;
// Grab the length of the content before clearing it to not flood the output
let len = result.content.as_ref().expect("content").len();
result.content.as_mut().expect("content").clear();
self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
.await
}
pub(super) async fn get_remote_thumbnail(
&self,
mxc: OwnedMxcUri,
server: Option<OwnedServerName>,
timeout: u32,
width: u32,
height: u32,
) -> Result {
let mxc: Mxc<'_> = mxc.as_str().try_into()?;
let timeout = Duration::from_millis(timeout.into());
let dim = Dim::new(width, height, None);
let mut result = self
.services
.media
.fetch_remote_thumbnail(&mxc, None, server.as_deref(), timeout, &dim)
.await?;
// Grab the length of the content before clearing it to not flood the output
let len = result.content.as_ref().expect("content").len();
result.content.as_mut().expect("content").clear();
self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
.await
}
pub(super) async fn delete_url_preview(&self, url: Option<String>, all: bool) -> Result {
if all {
self.services.media.clear_url_previews().await;
return self.write_str("Deleted all cached URL previews.").await;
}
let url = url.expect("clap enforces url is required unless --all");
self.services.media.remove_url_preview(&url).await?;
self.write_str(&format!("Deleted cached URL preview for: {url}"))
.await
}
Err!(
"Please specify either an MXC using --mxc or an event ID using --event-id of the \
message containing an image. See --help for details."
)
}
#[admin_command]
pub(super) async fn delete_list(&self) -> Result {
self.bail_restricted()?;
if self.body.len() < 2
|| !self.body[0].trim().starts_with("```")
|| self.body.last().unwrap_or(&"").trim() != "```"
{
return Err!("Expected code block in command body. Add --help for details.",);
}
let mut failed_parsed_mxcs: usize = 0;
let mxc_list = self
.body
.to_vec()
.drain(1..self.body.len().checked_sub(1).unwrap())
.filter_map(|mxc_s| {
mxc_s
.try_into()
.inspect_err(|e| {
debug_warn!("Failed to parse user-provided MXC URI: {e}");
failed_parsed_mxcs = failed_parsed_mxcs.saturating_add(1);
})
.ok()
})
.collect::<Vec<Mxc<'_>>>();
let mut mxc_deletion_count: usize = 0;
for mxc in &mxc_list {
trace!(%failed_parsed_mxcs, %mxc_deletion_count, "Deleting MXC {mxc} in bulk");
match self.services.media.delete(mxc).await {
| Ok(()) => {
debug_info!("Successfully deleted {mxc} from filesystem and database");
mxc_deletion_count = mxc_deletion_count.saturating_add(1);
},
| Err(e) => {
debug_warn!("Failed to delete {mxc}, ignoring error and skipping: {e}");
continue;
},
}
}
self.write_str(&format!(
"Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our database \
and the filesystem. {failed_parsed_mxcs} MXCs failed to be parsed from the database.",
))
.await
}
#[admin_command]
pub(super) async fn delete_past_remote_media(
&self,
duration: String,
before: bool,
after: bool,
yes_i_want_to_delete_local_media: bool,
) -> Result {
self.bail_restricted()?;
if before && after {
return Err!("Please only pick one argument, --before or --after.",);
}
assert!(!(before && after), "--before and --after should not be specified together");
let direction = if after {
TimeDirection::After
} else {
TimeDirection::Before
};
let time_boundary = parse_timepoint_ago(&duration)?;
let deleted_count = self
.services
.media
.delete_all_media_within_timeframe(
time_boundary,
direction,
yes_i_want_to_delete_local_media,
)
.await?;
self.write_str(&format!("Deleted {deleted_count} total files."))
.await
}
#[admin_command]
pub(super) async fn delete_all_from_user(&self, username: String) -> Result {
let user_id = parse_local_user_id(self.services, &username)?;
let deleted_count = self.services.media.delete_from_user(&user_id).await?;
self.write_str(&format!("Deleted {deleted_count} total files."))
.await
}
#[admin_command]
pub(super) async fn delete_all_from_server(
&self,
server_name: OwnedServerName,
yes_i_want_to_delete_local_media: bool,
) -> Result {
self.bail_restricted()?;
if server_name == self.services.globals.server_name() && !yes_i_want_to_delete_local_media {
return Err!("This command only works for remote media by default.",);
}
let Ok(all_mxcs) = self
.services
.media
.get_all_mxcs()
.await
.inspect_err(|e| error!("Failed to get MXC URIs from our database: {e}"))
else {
return Err!("Failed to get MXC URIs from our database",);
};
let mut deleted_count: usize = 0;
for mxc in all_mxcs {
let Ok(mxc_server_name) = mxc.server_name().inspect_err(|e| {
debug_warn!(
"Failed to parse MXC {mxc} server name from database, ignoring error and \
skipping: {e}"
);
}) else {
continue;
};
if mxc_server_name != server_name
|| (self.services.globals.server_is_ours(mxc_server_name)
&& !yes_i_want_to_delete_local_media)
{
trace!("skipping MXC URI {mxc}");
continue;
}
let mxc: Mxc<'_> = mxc.as_str().try_into()?;
match self.services.media.delete(&mxc).await {
| Ok(()) => {
deleted_count = deleted_count.saturating_add(1);
},
| Err(e) => {
debug_warn!("Failed to delete {mxc}, ignoring error and skipping: {e}");
continue;
},
}
}
self.write_str(&format!("Deleted {deleted_count} total files."))
.await
}
#[admin_command]
pub(super) async fn get_file_info(&self, mxc: OwnedMxcUri) -> Result {
let mxc: Mxc<'_> = mxc.as_str().try_into()?;
let metadata = self.services.media.get_metadata(&mxc).await;
self.write_str(&format!("```\n{metadata:#?}\n```")).await
}
#[admin_command]
pub(super) async fn get_remote_file(
&self,
mxc: OwnedMxcUri,
server: Option<OwnedServerName>,
timeout: u32,
) -> Result {
let mxc: Mxc<'_> = mxc.as_str().try_into()?;
let timeout = Duration::from_millis(timeout.into());
let mut result = self
.services
.media
.fetch_remote_content(&mxc, None, server.as_deref(), timeout)
.await?;
// Grab the length of the content before clearing it to not flood the output
let len = result.content.as_ref().expect("content").len();
result.content.as_mut().expect("content").clear();
self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
.await
}
#[admin_command]
pub(super) async fn get_remote_thumbnail(
&self,
mxc: OwnedMxcUri,
server: Option<OwnedServerName>,
timeout: u32,
width: u32,
height: u32,
) -> Result {
let mxc: Mxc<'_> = mxc.as_str().try_into()?;
let timeout = Duration::from_millis(timeout.into());
let dim = Dim::new(width, height, None);
let mut result = self
.services
.media
.fetch_remote_thumbnail(&mxc, None, server.as_deref(), timeout, &dim)
.await?;
// Grab the length of the content before clearing it to not flood the output
let len = result.content.as_ref().expect("content").len();
result.content.as_mut().expect("content").clear();
self.write_str(&format!("```\n{result:#?}\nreceived {len} bytes for file content.\n```"))
.await
}
#[admin_command]
pub(super) async fn delete_url_preview(&self, url: Option<String>, all: bool) -> Result {
if all {
self.services.media.clear_url_previews().await;
return self.write_str("Deleted all cached URL previews.").await;
}
let url = url.expect("clap enforces url is required unless --all");
self.services.media.remove_url_preview(&url).await?;
self.write_str(&format!("Deleted cached URL preview for: {url}"))
.await
}
+1 -2
View File
@@ -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;
@@ -27,7 +26,7 @@
extern crate conduwuit_core as conduwuit;
extern crate conduwuit_service as service;
pub(crate) use conduwuit_macros::admin_command_dispatch;
pub(crate) use conduwuit_macros::{admin_command, admin_command_dispatch};
pub(crate) use crate::{context::Context, utils::get_room_info};
-25
View File
@@ -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(())
}
}
-22
View File
@@ -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 -46
View File
@@ -4,7 +4,7 @@
use futures::StreamExt;
use ruma::{OwnedRoomId, OwnedUserId, exports::serde::Serialize};
use crate::admin_command_dispatch;
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
@@ -31,50 +31,50 @@ pub enum AccountDataCommand {
},
}
impl crate::Context<'_> {
async fn changes_since(
&self,
user_id: OwnedUserId,
since: u64,
room_id: Option<OwnedRoomId>,
) -> Result {
let timer = tokio::time::Instant::now();
let results: Vec<_> = self
.services
.account_data
.changes_since(room_id.as_deref(), &user_id, Some(since), None)
.collect()
.await;
let query_time = timer.elapsed();
#[admin_command]
async fn changes_since(
&self,
user_id: OwnedUserId,
since: u64,
room_id: Option<OwnedRoomId>,
) -> Result {
let timer = tokio::time::Instant::now();
let results: Vec<_> = self
.services
.account_data
.changes_since(room_id.as_deref(), &user_id, Some(since), None)
.collect()
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"))
.await
}
async fn account_data_get(
&self,
user_id: OwnedUserId,
kind: String,
room_id: Option<OwnedRoomId>,
) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.account_data
.get_raw(room_id.as_deref(), &user_id, &kind)
.await;
let query_time = timer.elapsed();
let json = serde_json::to_string_pretty(&match room_id {
| None => result
.deserialized::<ruma::serde::Raw<ruma::events::AnyGlobalAccountDataEvent>>()?
.serialize(serde_json::value::Serializer)?,
| Some(_) => result
.deserialized::<ruma::serde::Raw<ruma::events::AnyRoomAccountDataEvent>>()?
.serialize(serde_json::value::Serializer)?,
})?;
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{json}\n```"))
.await
}
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"))
.await
}
#[admin_command]
async fn account_data_get(
&self,
user_id: OwnedUserId,
kind: String,
room_id: Option<OwnedRoomId>,
) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.account_data
.get_raw(room_id.as_deref(), &user_id, &kind)
.await;
let query_time = timer.elapsed();
let json = serde_json::to_string_pretty(&match room_id {
| None => result
.deserialized::<ruma::serde::Raw<ruma::events::AnyGlobalAccountDataEvent>>()?
.serialize(serde_json::value::Serializer)?,
| Some(_) => result
.deserialized::<ruma::serde::Raw<ruma::events::AnyRoomAccountDataEvent>>()?
.serialize(serde_json::value::Serializer)?,
})?;
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{json}\n```"))
.await
}
+238 -246
View File
@@ -13,7 +13,7 @@
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
use tokio::time::Instant;
use crate::admin_command_dispatch;
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
@@ -159,287 +159,279 @@ pub enum RawCommand {
},
}
impl crate::Context<'_> {
pub(super) async fn compact(
&self,
map: Option<Vec<String>>,
start: Option<String>,
stop: Option<String>,
from: Option<usize>,
into: Option<usize>,
parallelism: Option<usize>,
exhaustive: bool,
) -> Result {
use conduwuit_database::compact::Options;
#[admin_command]
pub(super) async fn compact(
&self,
map: Option<Vec<String>>,
start: Option<String>,
stop: Option<String>,
from: Option<usize>,
into: Option<usize>,
parallelism: Option<usize>,
exhaustive: bool,
) -> Result {
use conduwuit_database::compact::Options;
let default_all_maps: Option<_> = map.is_none().then(|| {
self.services
.db
.keys()
.map(Deref::deref)
.map(ToOwned::to_owned)
});
let default_all_maps: Option<_> = map.is_none().then(|| {
self.services
.db
.keys()
.map(Deref::deref)
.map(ToOwned::to_owned)
});
let maps: Vec<_> = map
.unwrap_or_default()
.into_iter()
.chain(default_all_maps.into_iter().flatten())
.map(|map| self.services.db.get(&map))
.filter_map(Result::ok)
.cloned()
.collect();
let maps: Vec<_> = map
.unwrap_or_default()
.into_iter()
.chain(default_all_maps.into_iter().flatten())
.map(|map| self.services.db.get(&map))
.filter_map(Result::ok)
.cloned()
.collect();
if maps.is_empty() {
return Err!("--map argument invalid. not found in database");
}
let range = (
start.as_ref().map(String::as_bytes).map(Into::into),
stop.as_ref().map(String::as_bytes).map(Into::into),
);
let options = Options {
range,
level: (from, into),
exclusive: parallelism.is_some_and(is_zero!()),
exhaustive,
};
let runtime = self.services.server.runtime().clone();
let parallelism = parallelism.unwrap_or(1);
let results = maps
.into_iter()
.try_stream::<conduwuit::Error>()
.paralleln_and_then(runtime, parallelism, move |map| {
map.compact_blocking(options.clone())?;
Ok(map.name().to_owned())
})
.collect::<Vec<_>>();
let timer = Instant::now();
let results = results.await;
let query_time = timer.elapsed();
self.write_str(&format!("Jobs completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"))
.await
if maps.is_empty() {
return Err!("--map argument invalid. not found in database");
}
pub(super) async fn raw_count(&self, map: Option<String>, prefix: Option<String>) -> Result {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let range = (
start.as_ref().map(String::as_bytes).map(Into::into),
stop.as_ref().map(String::as_bytes).map(Into::into),
);
let timer = Instant::now();
let count = with_maps_or(map.as_deref(), self.services)
.then(|map| map.raw_count_prefix(&prefix))
.ready_fold(0_usize, usize::saturating_add)
.await;
let options = Options {
range,
level: (from, into),
exclusive: parallelism.is_some_and(is_zero!()),
exhaustive,
};
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{count:#?}\n```"))
.await
}
let runtime = self.services.server.runtime().clone();
let parallelism = parallelism.unwrap_or(1);
let results = maps
.into_iter()
.try_stream::<conduwuit::Error>()
.paralleln_and_then(runtime, parallelism, move |map| {
map.compact_blocking(options.clone())?;
Ok(map.name().to_owned())
})
.collect::<Vec<_>>();
pub(super) async fn raw_keys(&self, map: String, prefix: Option<String>) -> Result {
writeln!(self, "```").boxed().await?;
let timer = Instant::now();
let results = results.await;
let query_time = timer.elapsed();
self.write_str(&format!("Jobs completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"))
.await
}
let map = self.services.db.get(map.as_str())?;
let timer = Instant::now();
prefix
.as_deref()
.map_or_else(|| map.raw_keys().boxed(), |prefix| map.raw_keys_prefix(prefix).boxed())
.map_ok(String::from_utf8_lossy)
.try_for_each(|str| writeln!(self, "{str:?}"))
.boxed()
.await?;
#[admin_command]
pub(super) async fn raw_count(&self, map: Option<String>, prefix: Option<String>) -> Result {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let query_time = timer.elapsed();
self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}"))
.await
}
let timer = Instant::now();
let count = with_maps_or(map.as_deref(), self.services)
.then(|map| map.raw_count_prefix(&prefix))
.ready_fold(0_usize, usize::saturating_add)
.await;
pub(super) async fn raw_keys_sizes(
&self,
map: Option<String>,
prefix: Option<String>,
) -> Result {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{count:#?}\n```"))
.await
}
let timer = Instant::now();
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_keys_prefix(&prefix))
.flatten()
.ignore_err()
.map(<[u8]>::len)
.ready_fold_default(|mut map: BTreeMap<_, usize>, len| {
let entry = map.entry(len).or_default();
*entry = entry.saturating_add(1);
map
})
.await;
#[admin_command]
pub(super) async fn raw_keys(&self, map: String, prefix: Option<String>) -> Result {
writeln!(self, "```").boxed().await?;
let query_time = timer.elapsed();
self.write_str(&format!("```\n{result:#?}\n```\n\nQuery completed in {query_time:?}"))
.await
}
let map = self.services.db.get(map.as_str())?;
let timer = Instant::now();
prefix
.as_deref()
.map_or_else(|| map.raw_keys().boxed(), |prefix| map.raw_keys_prefix(prefix).boxed())
.map_ok(String::from_utf8_lossy)
.try_for_each(|str| writeln!(self, "{str:?}"))
.boxed()
.await?;
pub(super) async fn raw_keys_total(
&self,
map: Option<String>,
prefix: Option<String>,
) -> Result {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let query_time = timer.elapsed();
self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}"))
.await
}
let timer = Instant::now();
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_keys_prefix(&prefix))
.flatten()
.ignore_err()
.map(<[u8]>::len)
.ready_fold_default(|acc: usize, len| acc.saturating_add(len))
.await;
#[admin_command]
pub(super) async fn raw_keys_sizes(&self, map: Option<String>, prefix: Option<String>) -> Result {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let query_time = timer.elapsed();
self.write_str(&format!("```\n{result:#?}\n\n```\n\nQuery completed in {query_time:?}"))
.await
}
let timer = Instant::now();
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_keys_prefix(&prefix))
.flatten()
.ignore_err()
.map(<[u8]>::len)
.ready_fold_default(|mut map: BTreeMap<_, usize>, len| {
let entry = map.entry(len).or_default();
*entry = entry.saturating_add(1);
map
})
.await;
pub(super) async fn raw_vals_sizes(
&self,
map: Option<String>,
prefix: Option<String>,
) -> Result {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let query_time = timer.elapsed();
self.write_str(&format!("```\n{result:#?}\n```\n\nQuery completed in {query_time:?}"))
.await
}
let timer = Instant::now();
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_stream_prefix(&prefix))
.flatten()
.ignore_err()
.map(at!(1))
.map(<[u8]>::len)
.ready_fold_default(|mut map: BTreeMap<_, usize>, len| {
let entry = map.entry(len).or_default();
*entry = entry.saturating_add(1);
map
})
.await;
#[admin_command]
pub(super) async fn raw_keys_total(&self, map: Option<String>, prefix: Option<String>) -> Result {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let query_time = timer.elapsed();
self.write_str(&format!("```\n{result:#?}\n```\n\nQuery completed in {query_time:?}"))
.await
}
let timer = Instant::now();
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_keys_prefix(&prefix))
.flatten()
.ignore_err()
.map(<[u8]>::len)
.ready_fold_default(|acc: usize, len| acc.saturating_add(len))
.await;
pub(super) async fn raw_vals_total(
&self,
map: Option<String>,
prefix: Option<String>,
) -> Result {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let query_time = timer.elapsed();
self.write_str(&format!("```\n{result:#?}\n\n```\n\nQuery completed in {query_time:?}"))
.await
}
let timer = Instant::now();
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_stream_prefix(&prefix))
.flatten()
.ignore_err()
.map(at!(1))
.map(<[u8]>::len)
.ready_fold_default(|acc: usize, len| acc.saturating_add(len))
.await;
#[admin_command]
pub(super) async fn raw_vals_sizes(&self, map: Option<String>, prefix: Option<String>) -> Result {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let query_time = timer.elapsed();
self.write_str(&format!("```\n{result:#?}\n\n```\n\nQuery completed in {query_time:?}"))
.await
}
let timer = Instant::now();
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_stream_prefix(&prefix))
.flatten()
.ignore_err()
.map(at!(1))
.map(<[u8]>::len)
.ready_fold_default(|mut map: BTreeMap<_, usize>, len| {
let entry = map.entry(len).or_default();
*entry = entry.saturating_add(1);
map
})
.await;
pub(super) async fn raw_iter(&self, map: String, prefix: Option<String>) -> Result {
writeln!(self, "```").await?;
let query_time = timer.elapsed();
self.write_str(&format!("```\n{result:#?}\n```\n\nQuery completed in {query_time:?}"))
.await
}
let map = self.services.db.get(&map)?;
let timer = Instant::now();
prefix
.as_deref()
.map_or_else(
|| map.raw_stream().boxed(),
|prefix| map.raw_stream_prefix(prefix).boxed(),
)
.map_ok(apply!(2, String::from_utf8_lossy))
.map_ok(apply!(2, Cow::into_owned))
.try_for_each(|keyval| writeln!(self, "{keyval:?}"))
.boxed()
.await?;
#[admin_command]
pub(super) async fn raw_vals_total(&self, map: Option<String>, prefix: Option<String>) -> Result {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let query_time = timer.elapsed();
self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}"))
.await
}
let timer = Instant::now();
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_stream_prefix(&prefix))
.flatten()
.ignore_err()
.map(at!(1))
.map(<[u8]>::len)
.ready_fold_default(|acc: usize, len| acc.saturating_add(len))
.await;
pub(super) async fn raw_keys_from(
&self,
map: String,
start: String,
limit: Option<usize>,
) -> Result {
writeln!(self, "```").await?;
let query_time = timer.elapsed();
self.write_str(&format!("```\n{result:#?}\n\n```\n\nQuery completed in {query_time:?}"))
.await
}
let map = self.services.db.get(&map)?;
let timer = Instant::now();
map.raw_keys_from(&start)
.map_ok(String::from_utf8_lossy)
.take(limit.unwrap_or(usize::MAX))
.try_for_each(|str| writeln!(self, "{str:?}"))
.boxed()
.await?;
#[admin_command]
pub(super) async fn raw_iter(&self, map: String, prefix: Option<String>) -> Result {
writeln!(self, "```").await?;
let query_time = timer.elapsed();
self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}"))
.await
}
let map = self.services.db.get(&map)?;
let timer = Instant::now();
prefix
.as_deref()
.map_or_else(|| map.raw_stream().boxed(), |prefix| map.raw_stream_prefix(prefix).boxed())
.map_ok(apply!(2, String::from_utf8_lossy))
.map_ok(apply!(2, Cow::into_owned))
.try_for_each(|keyval| writeln!(self, "{keyval:?}"))
.boxed()
.await?;
pub(super) async fn raw_iter_from(
&self,
map: String,
start: String,
limit: Option<usize>,
) -> Result {
let map = self.services.db.get(&map)?;
let timer = Instant::now();
let result = map
.raw_stream_from(&start)
.map_ok(apply!(2, String::from_utf8_lossy))
.map_ok(apply!(2, Cow::into_owned))
.take(limit.unwrap_or(usize::MAX))
.try_collect::<Vec<(String, String)>>()
.await?;
let query_time = timer.elapsed();
self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}"))
.await
}
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
pub(super) async fn raw_keys_from(
&self,
map: String,
start: String,
limit: Option<usize>,
) -> Result {
writeln!(self, "```").await?;
pub(super) async fn raw_del(&self, map: String, key: String) -> Result {
let map = self.services.db.get(&map)?;
let timer = Instant::now();
map.remove(&key);
let map = self.services.db.get(&map)?;
let timer = Instant::now();
map.raw_keys_from(&start)
.map_ok(String::from_utf8_lossy)
.take(limit.unwrap_or(usize::MAX))
.try_for_each(|str| writeln!(self, "{str:?}"))
.boxed()
.await?;
let query_time = timer.elapsed();
self.write_str(&format!("Operation completed in {query_time:?}"))
.await
}
let query_time = timer.elapsed();
self.write_str(&format!("\n```\n\nQuery completed in {query_time:?}"))
.await
}
pub(super) async fn raw_get(&self, map: String, key: String) -> Result {
let map = self.services.db.get(&map)?;
let timer = Instant::now();
let handle = map.get(&key).await?;
#[admin_command]
pub(super) async fn raw_iter_from(
&self,
map: String,
start: String,
limit: Option<usize>,
) -> Result {
let map = self.services.db.get(&map)?;
let timer = Instant::now();
let result = map
.raw_stream_from(&start)
.map_ok(apply!(2, String::from_utf8_lossy))
.map_ok(apply!(2, Cow::into_owned))
.take(limit.unwrap_or(usize::MAX))
.try_collect::<Vec<(String, String)>>()
.await?;
let query_time = timer.elapsed();
let result = String::from_utf8_lossy(&handle);
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:?}\n```"))
.await
}
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
pub(super) async fn raw_maps(&self) -> Result {
let list: Vec<_> = self.services.db.iter().map(at!(0)).copied().collect();
#[admin_command]
pub(super) async fn raw_del(&self, map: String, key: String) -> Result {
let map = self.services.db.get(&map)?;
let timer = Instant::now();
map.remove(&key);
self.write_str(&format!("{list:#?}")).await
}
let query_time = timer.elapsed();
self.write_str(&format!("Operation completed in {query_time:?}"))
.await
}
#[admin_command]
pub(super) async fn raw_get(&self, map: String, key: String) -> Result {
let map = self.services.db.get(&map)?;
let timer = Instant::now();
let handle = map.get(&key).await?;
let query_time = timer.elapsed();
let result = String::from_utf8_lossy(&handle);
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:?}\n```"))
.await
}
#[admin_command]
pub(super) async fn raw_maps(&self) -> Result {
let list: Vec<_> = self.services.db.iter().map(at!(0)).copied().collect();
self.write_str(&format!("{list:#?}")).await
}
fn with_maps_or<'a>(
+53 -54
View File
@@ -3,7 +3,7 @@
use futures::StreamExt;
use ruma::OwnedServerName;
use crate::admin_command_dispatch;
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
@@ -39,68 +39,67 @@ pub enum ResolverCommand {
},
}
impl crate::Context<'_> {
async fn destinations_cache(&self, server_name: Option<OwnedServerName>) -> Result {
use service::resolver::cache::CachedDest;
#[admin_command]
async fn destinations_cache(&self, server_name: Option<OwnedServerName>) -> Result {
use service::resolver::cache::CachedDest;
writeln!(self, "| Server Name | Destination | Hostname | Expires |").await?;
writeln!(self, "| ----------- | ----------- | -------- | ------- |").await?;
writeln!(self, "| Server Name | Destination | Hostname | Expires |").await?;
writeln!(self, "| ----------- | ----------- | -------- | ------- |").await?;
let mut destinations = self.services.resolver.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() {
if name != *server_name {
continue;
}
while let Some((name, CachedDest { dest, host, expire })) = destinations.next().await {
if let Some(server_name) = server_name.as_ref() {
if name != *server_name {
continue;
}
let expire = time::format(expire, "%+");
self.write_str(&format!("| {name} | {dest} | {host} | {expire} |\n"))
.await?;
}
Ok(())
}
async fn overrides_cache(&self, server_name: Option<String>) -> Result {
use service::resolver::cache::CachedOverride;
writeln!(self, "| Server Name | IP | Port | Expires | Overriding |").await?;
writeln!(self, "| ----------- | --- | ----:| ------- | ---------- |").await?;
let mut overrides = self.services.resolver.cache.overrides().boxed();
while let Some((name, CachedOverride { ips, port, expire, overriding })) =
overrides.next().await
{
if let Some(server_name) = server_name.as_ref() {
if name != *server_name {
continue;
}
}
let expire = time::format(expire, "%+");
self.write_str(&format!(
"| {name} | {ips:?} | {port} | {expire} | {overriding:?} |\n"
))
let expire = time::format(expire, "%+");
self.write_str(&format!("| {name} | {dest} | {host} | {expire} |\n"))
.await?;
}
Ok(())
}
async fn flush_cache(&self, name: Option<OwnedServerName>, all: bool) -> Result {
if all {
self.services.resolver.cache.clear().await;
writeln!(self, "Resolver caches cleared!").await
} else if let Some(name) = 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 {
Err!("Missing name. Supply a name or use --all to flush the whole cache.")
Ok(())
}
#[admin_command]
async fn overrides_cache(&self, server_name: Option<String>) -> Result {
use service::resolver::cache::CachedOverride;
writeln!(self, "| Server Name | IP | Port | Expires | Overriding |").await?;
writeln!(self, "| ----------- | --- | ----:| ------- | ---------- |").await?;
let mut overrides = self.services.resolver.cache.overrides().boxed();
while let Some((name, CachedOverride { ips, port, expire, overriding })) =
overrides.next().await
{
if let Some(server_name) = server_name.as_ref() {
if name != *server_name {
continue;
}
}
let expire = time::format(expire, "%+");
self.write_str(&format!("| {name} | {ips:?} | {port} | {expire} | {overriding:?} |\n"))
.await?;
}
Ok(())
}
#[admin_command]
async fn flush_cache(&self, name: Option<OwnedServerName>, all: bool) -> Result {
if all {
self.services.resolver.cache.clear().await;
writeln!(self, "Resolver caches cleared!").await
} else if let Some(name) = 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 {
Err!("Missing name. Supply a name or use --all to flush the whole cache.")
}
}
+34 -34
View File
@@ -3,7 +3,7 @@
use futures::TryStreamExt;
use ruma::OwnedRoomOrAliasId;
use crate::admin_command_dispatch;
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
@@ -23,39 +23,39 @@ pub enum RoomTimelineCommand {
},
}
impl crate::Context<'_> {
pub(super) async fn last(&self, room_id: OwnedRoomOrAliasId) -> Result {
let room_id = self.services.rooms.alias.resolve(&room_id).await?;
#[admin_command]
pub(super) async fn last(&self, room_id: OwnedRoomOrAliasId) -> Result {
let room_id = self.services.rooms.alias.resolve(&room_id).await?;
let result = self
.services
.rooms
.timeline
.last_timeline_count(&room_id)
.await?;
let result = self
.services
.rooms
.timeline
.last_timeline_count(&room_id)
.await?;
self.write_str(&format!("{result:#?}")).await
}
pub(super) async fn pdus(
&self,
room_id: OwnedRoomOrAliasId,
from: Option<String>,
limit: Option<usize>,
) -> Result {
let room_id = self.services.rooms.alias.resolve(&room_id).await?;
let from: Option<PduCount> = from.as_deref().map(str::parse).transpose()?;
let result: Vec<_> = self
.services
.rooms
.timeline
.pdus_rev(&room_id, from)
.try_take(limit.unwrap_or(3))
.try_collect()
.await?;
self.write_str(&format!("```\n{result:#?}\n```")).await
}
self.write_str(&format!("{result:#?}")).await
}
#[admin_command]
pub(super) async fn pdus(
&self,
room_id: OwnedRoomOrAliasId,
from: Option<String>,
limit: Option<usize>,
) -> Result {
let room_id = self.services.rooms.alias.resolve(&room_id).await?;
let from: Option<PduCount> = from.as_deref().map(str::parse).transpose()?;
let result: Vec<_> = self
.services
.rooms
.timeline
.pdus_rev(&room_id, from)
.try_take(limit.unwrap_or(3))
.try_collect()
.await?;
self.write_str(&format!("```\n{result:#?}\n```")).await
}
+19 -19
View File
@@ -2,7 +2,7 @@
use conduwuit::Result;
use ruma::{OwnedEventId, OwnedRoomOrAliasId};
use crate::admin_command_dispatch;
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
@@ -17,23 +17,23 @@ pub enum ShortCommand {
},
}
impl crate::Context<'_> {
pub(super) async fn short_event_id(&self, event_id: OwnedEventId) -> Result {
let shortid = self
.services
.rooms
.short
.get_shorteventid(&event_id)
.await?;
#[admin_command]
pub(super) async fn short_event_id(&self, event_id: OwnedEventId) -> Result {
let shortid = self
.services
.rooms
.short
.get_shorteventid(&event_id)
.await?;
self.write_str(&format!("{shortid:#?}")).await
}
pub(super) async fn short_room_id(&self, room_id: OwnedRoomOrAliasId) -> Result {
let room_id = self.services.rooms.alias.resolve(&room_id).await?;
let shortid = self.services.rooms.short.get_shortroomid(&room_id).await?;
self.write_str(&format!("{shortid:#?}")).await
}
self.write_str(&format!("{shortid:#?}")).await
}
#[admin_command]
pub(super) async fn short_room_id(&self, room_id: OwnedRoomOrAliasId) -> Result {
let room_id = self.services.rooms.alias.resolve(&room_id).await?;
let shortid = self.services.rooms.short.get_shortroomid(&room_id).await?;
self.write_str(&format!("{shortid:#?}")).await
}
+259 -260
View File
@@ -3,7 +3,7 @@
use futures::stream::StreamExt;
use ruma::{OwnedDeviceId, OwnedRoomId, OwnedUserId};
use crate::admin_command_dispatch;
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
@@ -92,264 +92,263 @@ pub enum UsersCommand {
},
}
impl crate::Context<'_> {
async fn get_shared_rooms(&self, user_a: OwnedUserId, user_b: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result: Vec<_> = self
.services
.rooms
.state_cache
.get_shared_rooms(&user_a, &user_b)
.collect()
.await;
let query_time = timer.elapsed();
#[admin_command]
async fn get_shared_rooms(&self, user_a: OwnedUserId, user_b: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result: Vec<_> = self
.services
.rooms
.state_cache
.get_shared_rooms(&user_a, &user_b)
.collect()
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
async fn get_backup_session(
&self,
user_id: OwnedUserId,
version: String,
room_id: OwnedRoomId,
session_id: String,
) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_session(&user_id, &version, &room_id, &session_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
async fn get_room_backups(
&self,
user_id: OwnedUserId,
version: String,
room_id: OwnedRoomId,
) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_room(&user_id, &version, &room_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
async fn get_all_backups(&self, user_id: OwnedUserId, version: String) -> Result {
let timer = tokio::time::Instant::now();
let result = self.services.key_backups.get_all(&user_id, &version).await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
async fn get_backup_algorithm(&self, user_id: OwnedUserId, version: String) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_backup(&user_id, &version)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
async fn get_latest_backup_version(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_latest_backup_version(&user_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
async fn get_latest_backup(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result = self.services.key_backups.get_latest_backup(&user_id).await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
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 query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
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<_> = result
.into_iter()
.map(|user_id| String::from_utf8_lossy(user_id.as_bytes()).into_owned())
.collect();
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:?}\n```"))
.await
}
async fn count_users(&self) -> Result {
let timer = tokio::time::Instant::now();
let result = self.services.users.count().await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
async fn list_devices(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let devices = self
.services
.users
.all_device_ids(&user_id)
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```"))
.await
}
async fn list_devices_metadata(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let devices = self
.services
.users
.all_devices_metadata(&user_id)
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```"))
.await
}
async fn get_device_metadata(
&self,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
) -> Result {
let timer = tokio::time::Instant::now();
let device = self
.services
.users
.get_device_metadata(&user_id, &device_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```"))
.await
}
async fn get_devices_version(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let device = self.services.users.get_devicelist_version(&user_id).await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```"))
.await
}
async fn count_one_time_keys(
&self,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.count_one_time_keys(&user_id, &device_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
async fn get_device_keys(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.get_device_keys(&user_id, &device_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
async fn get_user_signing_key(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result = self.services.users.get_user_signing_key(&user_id).await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
async fn get_master_key(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.get_master_key(None, &user_id, &|_| true)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
async fn get_to_device_events(
&self,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.get_to_device_events(&user_id, &device_id, None, None)
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn get_backup_session(
&self,
user_id: OwnedUserId,
version: String,
room_id: OwnedRoomId,
session_id: String,
) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_session(&user_id, &version, &room_id, &session_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn get_room_backups(
&self,
user_id: OwnedUserId,
version: String,
room_id: OwnedRoomId,
) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_room(&user_id, &version, &room_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn get_all_backups(&self, user_id: OwnedUserId, version: String) -> Result {
let timer = tokio::time::Instant::now();
let result = self.services.key_backups.get_all(&user_id, &version).await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn get_backup_algorithm(&self, user_id: OwnedUserId, version: String) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_backup(&user_id, &version)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn get_latest_backup_version(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_latest_backup_version(&user_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn get_latest_backup(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result = self.services.key_backups.get_latest_backup(&user_id).await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn iter_users(&self) -> Result {
let timer = tokio::time::Instant::now();
let result: Vec<OwnedUserId> = self.services.users.stream().map(Into::into).collect().await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn iter_users2(&self) -> Result {
let timer = tokio::time::Instant::now();
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())
.collect();
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:?}\n```"))
.await
}
#[admin_command]
async fn count_users(&self) -> Result {
let timer = tokio::time::Instant::now();
let result = self.services.users.count().await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn list_devices(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let devices = self
.services
.users
.all_device_ids(&user_id)
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```"))
.await
}
#[admin_command]
async fn list_devices_metadata(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let devices = self
.services
.users
.all_devices_metadata(&user_id)
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```"))
.await
}
#[admin_command]
async fn get_device_metadata(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result {
let timer = tokio::time::Instant::now();
let device = self
.services
.users
.get_device_metadata(&user_id, &device_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```"))
.await
}
#[admin_command]
async fn get_devices_version(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let device = self.services.users.get_devicelist_version(&user_id).await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```"))
.await
}
#[admin_command]
async fn count_one_time_keys(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.count_one_time_keys(&user_id, &device_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn get_device_keys(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.get_device_keys(&user_id, &device_id)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn get_user_signing_key(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result = self.services.users.get_user_signing_key(&user_id).await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn get_master_key(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.get_master_key(None, &user_id, &|_| true)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn get_to_device_events(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.get_to_device_events(&user_id, &device_id, None, None)
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
+72 -72
View File
@@ -2,83 +2,83 @@
use futures::StreamExt;
use ruma::OwnedRoomId;
use crate::{PAGE_SIZE, get_room_info};
use crate::{PAGE_SIZE, admin_command, get_room_info};
impl crate::Context<'_> {
#[allow(clippy::fn_params_excessive_bools)]
pub(super) async fn list_rooms(
&self,
page: Option<usize>,
exclude_disabled: bool,
exclude_banned: bool,
include_empty: bool,
no_details: bool,
) -> Result {
// TODO: i know there's a way to do this with clap, but i can't seem to find it
let page = page.unwrap_or(1);
let mut rooms = self
.services
.rooms
.metadata
.iter_ids()
.filter_map(|room_id| async move {
(!exclude_disabled || !self.services.rooms.metadata.is_disabled(&room_id).await)
.then_some(room_id)
})
.filter_map(|room_id| async move {
(!exclude_banned || !self.services.rooms.metadata.is_banned(&room_id).await)
.then_some(room_id)
})
.then(async |room_id| get_room_info(self.services, &room_id).await)
.then(|(room_id, total_members, name)| async move {
let local_members: Vec<_> = self
.services
.rooms
.state_cache
.active_local_users_in_room(&room_id)
.collect()
.await;
let local_members = local_members.len();
(room_id, total_members, local_members, name)
})
.filter_map(|(room_id, total_members, local_members, name)| async move {
(include_empty || local_members > 0).then_some((room_id, total_members, name))
})
.collect::<Vec<_>>()
.await;
#[allow(clippy::fn_params_excessive_bools)]
#[admin_command]
pub(super) async fn list_rooms(
&self,
page: Option<usize>,
exclude_disabled: bool,
exclude_banned: bool,
include_empty: bool,
no_details: bool,
) -> Result {
// TODO: i know there's a way to do this with clap, but i can't seem to find it
let page = page.unwrap_or(1);
let mut rooms = self
.services
.rooms
.metadata
.iter_ids()
.filter_map(|room_id| async move {
(!exclude_disabled || !self.services.rooms.metadata.is_disabled(&room_id).await)
.then_some(room_id)
})
.filter_map(|room_id| async move {
(!exclude_banned || !self.services.rooms.metadata.is_banned(&room_id).await)
.then_some(room_id)
})
.then(async |room_id| get_room_info(self.services, &room_id).await)
.then(|(room_id, total_members, name)| async move {
let local_members: Vec<_> = self
.services
.rooms
.state_cache
.active_local_users_in_room(&room_id)
.collect()
.await;
let local_members = local_members.len();
(room_id, total_members, local_members, name)
})
.filter_map(|(room_id, total_members, local_members, name)| async move {
(include_empty || local_members > 0).then_some((room_id, total_members, name))
})
.collect::<Vec<_>>()
.await;
rooms.sort_by_key(|r| r.1);
rooms.reverse();
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let rooms = rooms
.into_iter()
.skip(page.saturating_sub(1).saturating_mul(PAGE_SIZE))
.take(PAGE_SIZE)
.collect::<Vec<_>>();
let rooms = rooms
.into_iter()
.skip(page.saturating_sub(1).saturating_mul(PAGE_SIZE))
.take(PAGE_SIZE)
.collect::<Vec<_>>();
if rooms.is_empty() {
return Err!("No more rooms.");
}
let body = rooms
.iter()
.map(|(id, members, name)| {
if no_details {
format!("{id}")
} else {
format!("{id}\tMembers: {members}\tName: {name}")
}
})
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms ({}):\n```\n{body}\n```", rooms.len()))
.await
if rooms.is_empty() {
return Err!("No more rooms.");
}
pub(super) async fn exists(&self, room_id: OwnedRoomId) -> Result {
let result = self.services.rooms.metadata.exists(&room_id).await;
let body = rooms
.iter()
.map(|(id, members, name)| {
if no_details {
format!("{id}")
} else {
format!("{id}\tMembers: {members}\tName: {name}")
}
})
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("{result}")).await
}
self.write_str(&format!("Rooms ({}):\n```\n{body}\n```", rooms.len()))
.await
}
#[admin_command]
pub(super) async fn exists(&self, room_id: OwnedRoomId) -> Result {
let result = self.services.rooms.metadata.exists(&room_id).await;
self.write_str(&format!("{result}")).await
}
+56 -56
View File
@@ -3,7 +3,7 @@
use futures::StreamExt;
use ruma::OwnedRoomId;
use crate::admin_command_dispatch;
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
@@ -26,62 +26,62 @@ pub enum RoomInfoCommand {
},
}
impl crate::Context<'_> {
async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> Result {
let room_name = self
.services
.rooms
.state_accessor
.get_name(&room_id)
.await
.unwrap_or_else(|_| room_id.to_string());
#[admin_command]
async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> Result {
let room_name = self
.services
.rooms
.state_accessor
.get_name(&room_id)
.await
.unwrap_or_else(|_| room_id.to_string());
let member_info: Vec<_> = self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_filter(|user_id| {
local_only
.then(|| self.services.globals.user_is_local(user_id))
.unwrap_or(true)
})
.filter_map(|user_id| async move {
Some((
self.services
.users
.displayname(&user_id)
.await
.unwrap_or_else(|_| user_id.to_string()),
user_id,
))
})
.collect()
.await;
let member_info: Vec<_> = self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_filter(|user_id| {
local_only
.then(|| self.services.globals.user_is_local(user_id))
.unwrap_or(true)
})
.filter_map(|user_id| async move {
Some((
self.services
.users
.displayname(&user_id)
.await
.unwrap_or_else(|_| user_id.to_string()),
user_id,
))
})
.collect()
.await;
let num = member_info.len();
let body = member_info
.into_iter()
.map(|(displayname, mxid)| format!("{mxid} | {displayname}"))
.collect::<Vec<_>>()
.join("\n");
let num = member_info.len();
let body = member_info
.into_iter()
.map(|(displayname, mxid)| format!("{mxid} | {displayname}"))
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("{num} Members in Room \"{room_name}\":\n```\n{body}\n```"))
.await
}
async fn view_room_topic(&self, room_id: OwnedRoomId) -> Result {
let Ok(room_topic) = self
.services
.rooms
.state_accessor
.get_room_topic(&room_id)
.await
else {
return Err!("Room does not have a room topic set.");
};
self.write_str(&format!("Room topic:\n```\n{room_topic}\n```"))
.await
}
self.write_str(&format!("{num} Members in Room \"{room_name}\":\n```\n{body}\n```"))
.await
}
#[admin_command]
async fn view_room_topic(&self, room_id: OwnedRoomId) -> Result {
let Ok(room_topic) = self
.services
.rooms
.state_accessor
.get_room_topic(&room_id)
.await
else {
return Err!("Room does not have a room topic set.");
};
self.write_str(&format!("Room topic:\n```\n{room_topic}\n```"))
.await
}
+354 -355
View File
@@ -8,7 +8,7 @@
use futures::{FutureExt, StreamExt};
use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId};
use crate::{admin_command_dispatch, get_room_info};
use crate::{admin_command, admin_command_dispatch, get_room_info};
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
@@ -45,72 +45,238 @@ pub enum RoomModerationCommand {
},
}
impl crate::Context<'_> {
async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
debug!("Got room alias or ID: {}", room);
#[admin_command]
async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
debug!("Got room alias or ID: {}", room);
let admin_room_alias = &self.services.globals.admin_alias;
let admin_room_alias = &self.services.globals.admin_alias;
if let Ok(admin_room_id) = self.services.admin.get_admin_room().await {
if room.to_string().eq(&admin_room_id) || room.to_string().eq(admin_room_alias) {
return Err!("Not allowed to ban the admin room.");
}
if let Ok(admin_room_id) = self.services.admin.get_admin_room().await {
if room.to_string().eq(&admin_room_id) || room.to_string().eq(admin_room_alias) {
return Err!("Not allowed to ban the admin room.");
}
}
let room_id = if room.is_room_id() {
let room_id = match RoomId::parse(&room) {
| Ok(room_id) => room_id,
| Err(e) => {
return Err!(
"Failed to parse room ID {room}. Please note that this requires a full \
room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`): {e}"
);
},
};
debug!("Room specified is a room ID, banning room ID");
room_id.clone()
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
| Ok(room_alias) => room_alias,
| Err(e) => {
return Err!(
"Failed to parse room ID {room}. Please note that this requires a full \
room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`): {e}"
);
},
};
debug!(
"Room specified is not a room ID, attempting to resolve room alias to a room ID \
locally, if not using get_alias_helper to fetch room ID remotely"
);
match self.services.rooms.alias.resolve_alias(&room_alias).await {
| Ok((room_id, servers)) => {
debug!(
%room_id,
?servers,
"Got federation response fetching room ID for room {room}"
);
room_id
},
| Err(e) => {
return Err!("Failed to resolve room alias {room} to a room ID: {e}");
},
}
} else {
return Err!(
"Room specified is not a room ID or room alias. Please note that this requires \
a full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`)",
);
let room_id = if room.is_room_id() {
let room_id = match RoomId::parse(&room) {
| Ok(room_id) => room_id,
| Err(e) => {
return Err!(
"Failed to parse room ID {room}. Please note that this requires a full room \
ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`): {e}"
);
},
};
info!("Making all users leave the room {room_id} and forgetting it");
debug!("Room specified is a room ID, banning room ID");
room_id.clone()
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
| Ok(room_alias) => room_alias,
| Err(e) => {
return Err!(
"Failed to parse room ID {room}. Please note that this requires a full room \
ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`): {e}"
);
},
};
debug!(
"Room specified is not a room ID, attempting to resolve room alias to a room ID \
locally, if not using get_alias_helper to fetch room ID remotely"
);
match self.services.rooms.alias.resolve_alias(&room_alias).await {
| Ok((room_id, servers)) => {
debug!(
%room_id,
?servers,
"Got federation response fetching room ID for room {room}"
);
room_id
},
| Err(e) => {
return Err!("Failed to resolve room alias {room} to a room ID: {e}");
},
}
} else {
return Err!(
"Room specified is not a room ID or room alias. Please note that this requires a \
full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`)",
);
};
info!("Making all users leave the room {room_id} and forgetting it");
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
while let Some(ref user_id) = users.next().await {
info!(
"Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \
evicting admins too)",
);
if let Err(e) = leave_room(self.services, user_id, &room_id, None)
.boxed()
.await
{
warn!("Failed to leave room: {e}");
}
self.services.rooms.state_cache.forget(&room_id, user_id);
}
self.services
.rooms
.alias
.local_aliases_for_room(&room_id)
.for_each(|local_alias| async move {
self.services
.rooms
.alias
.remove_alias(&local_alias, &self.services.globals.server_user)
.await
.ok();
})
.await;
self.services.rooms.directory.set_not_public(&room_id); // remove from the room directory
self.services.rooms.metadata.ban_room(&room_id, true); // prevent further joins
self.services.rooms.metadata.disable_room(&room_id, true); // disable federation
self.write_str(
"Room banned, removed all our local users, and disabled incoming federation with room.",
)
.await
}
#[admin_command]
async fn ban_list_of_rooms(&self) -> Result {
if self.body.len() < 2
|| !self.body[0].trim().starts_with("```")
|| self.body.last().unwrap_or(&"").trim() != "```"
{
return Err!("Expected code block in command body. Add --help for details.",);
}
let rooms_s = self
.body
.to_vec()
.drain(1..self.body.len().saturating_sub(1))
.collect::<Vec<_>>();
let admin_room_alias = &self.services.globals.admin_alias;
let mut room_ban_count: usize = 0;
let mut room_ids: Vec<OwnedRoomId> = Vec::new();
for &room in &rooms_s {
match <&RoomOrAliasId>::try_from(room) {
| Ok(room_alias_or_id) => {
if let Ok(admin_room_id) = self.services.admin.get_admin_room().await {
if room.to_owned().eq(&admin_room_id) || room.to_owned().eq(admin_room_alias)
{
warn!("User specified admin room in bulk ban list, ignoring");
continue;
}
}
if room_alias_or_id.is_room_id() {
let room_id = match RoomId::parse(room_alias_or_id) {
| Ok(room_id) => room_id,
| Err(e) => {
// ignore rooms we failed to parse
warn!(
"Error parsing room \"{room}\" during bulk room banning, \
ignoring error and logging here: {e}"
);
continue;
},
};
room_ids.push(room_id.clone());
}
if room_alias_or_id.is_room_alias_id() {
match RoomAliasId::parse(room_alias_or_id) {
| Ok(room_alias) => {
let room_id = match self
.services
.rooms
.alias
.resolve_local_alias(&room_alias)
.await
{
| Ok(room_id) => room_id,
| _ => {
debug!(
"We don't have this room alias to a room ID locally, \
attempting to fetch room ID over federation"
);
match self
.services
.rooms
.alias
.resolve_alias(&room_alias)
.await
{
| Ok((room_id, servers)) => {
debug!(
%room_id,
?servers,
"Got federation response fetching room ID for \
{room}",
);
room_id
},
| Err(e) => {
warn!(
"Failed to resolve room alias {room} to a room \
ID: {e}"
);
continue;
},
}
},
};
room_ids.push(room_id);
},
| Err(e) => {
warn!(
"Error parsing room \"{room}\" during bulk room banning, \
ignoring error and logging here: {e}"
);
continue;
},
}
}
},
| Err(e) => {
warn!(
"Error parsing room \"{room}\" during bulk room banning, ignoring error and \
logging here: {e}"
);
continue;
},
}
}
for room_id in room_ids {
debug!("Banned {room_id} successfully");
room_ban_count = room_ban_count.saturating_add(1);
debug!("Making all users leave the room {room_id} and forgetting it");
let mut users = self
.services
.rooms
@@ -120,7 +286,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
.boxed();
while let Some(ref user_id) = users.next().await {
info!(
debug!(
"Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \
evicting admins too)",
);
@@ -135,6 +301,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
self.services.rooms.state_cache.forget(&room_id, user_id);
}
// remove any local aliases, ignore errors
self.services
.rooms
.alias
@@ -149,307 +316,139 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
})
.await;
self.services.rooms.directory.set_not_public(&room_id); // remove from the room directory
self.services.rooms.metadata.ban_room(&room_id, true); // prevent further joins
self.services.rooms.metadata.disable_room(&room_id, true); // disable federation
self.write_str(
"Room banned, removed all our local users, and disabled incoming federation with \
room.",
)
.await
self.services.rooms.metadata.ban_room(&room_id, true);
// unpublish from room directory, ignore errors
self.services.rooms.directory.set_not_public(&room_id);
self.services.rooms.metadata.disable_room(&room_id, true);
}
async fn ban_list_of_rooms(&self) -> Result {
if self.body.len() < 2
|| !self.body[0].trim().starts_with("```")
|| self.body.last().unwrap_or(&"").trim() != "```"
{
return Err!("Expected code block in command body. Add --help for details.",);
}
self.write_str(&format!(
"Finished bulk room ban, banned {room_ban_count} total rooms, evicted all users, and \
disabled incoming federation with the room."
))
.await
}
let rooms_s = self
.body
.to_vec()
.drain(1..self.body.len().saturating_sub(1))
.collect::<Vec<_>>();
let admin_room_alias = &self.services.globals.admin_alias;
let mut room_ban_count: usize = 0;
let mut room_ids: Vec<OwnedRoomId> = Vec::new();
for &room in &rooms_s {
match <&RoomOrAliasId>::try_from(room) {
| Ok(room_alias_or_id) => {
if let Ok(admin_room_id) = self.services.admin.get_admin_room().await {
if room.to_owned().eq(&admin_room_id)
|| room.to_owned().eq(admin_room_alias)
{
warn!("User specified admin room in bulk ban list, ignoring");
continue;
}
}
if room_alias_or_id.is_room_id() {
let room_id = match RoomId::parse(room_alias_or_id) {
| Ok(room_id) => room_id,
| Err(e) => {
// ignore rooms we failed to parse
warn!(
"Error parsing room \"{room}\" during bulk room banning, \
ignoring error and logging here: {e}"
);
continue;
},
};
room_ids.push(room_id.clone());
}
if room_alias_or_id.is_room_alias_id() {
match RoomAliasId::parse(room_alias_or_id) {
| Ok(room_alias) => {
let room_id = match self
.services
.rooms
.alias
.resolve_local_alias(&room_alias)
.await
{
| Ok(room_id) => room_id,
| _ => {
debug!(
"We don't have this room alias to a room ID \
locally, attempting to fetch room ID over \
federation"
);
match self
.services
.rooms
.alias
.resolve_alias(&room_alias)
.await
{
| Ok((room_id, servers)) => {
debug!(
%room_id,
?servers,
"Got federation response fetching room ID for \
{room}",
);
room_id
},
| Err(e) => {
warn!(
"Failed to resolve room alias {room} to a \
room ID: {e}"
);
continue;
},
}
},
};
room_ids.push(room_id);
},
| Err(e) => {
warn!(
"Error parsing room \"{room}\" during bulk room banning, \
ignoring error and logging here: {e}"
);
continue;
},
}
}
},
| Err(e) => {
warn!(
"Error parsing room \"{room}\" during bulk room banning, ignoring error \
and logging here: {e}"
);
continue;
},
}
}
for room_id in room_ids {
debug!("Banned {room_id} successfully");
room_ban_count = room_ban_count.saturating_add(1);
debug!("Making all users leave the room {room_id} and forgetting it");
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
while let Some(ref user_id) = users.next().await {
debug!(
"Attempting leave for user {user_id} in room {room_id} (ignoring all \
errors, evicting admins too)",
#[admin_command]
async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
let room_id = if room.is_room_id() {
let room_id = match RoomId::parse(&room) {
| Ok(room_id) => room_id,
| Err(e) => {
return Err!(
"Failed to parse room ID {room}. Please note that this requires a full room \
ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`): {e}"
);
if let Err(e) = leave_room(self.services, user_id, &room_id, None)
.boxed()
.await
{
warn!("Failed to leave room: {e}");
}
self.services.rooms.state_cache.forget(&room_id, user_id);
}
// remove any local aliases, ignore errors
self.services
.rooms
.alias
.local_aliases_for_room(&room_id)
.for_each(|local_alias| async move {
self.services
.rooms
.alias
.remove_alias(&local_alias, &self.services.globals.server_user)
.await
.ok();
})
.await;
self.services.rooms.metadata.ban_room(&room_id, true);
// unpublish from room directory, ignore errors
self.services.rooms.directory.set_not_public(&room_id);
self.services.rooms.metadata.disable_room(&room_id, true);
}
self.write_str(&format!(
"Finished bulk room ban, banned {room_ban_count} total rooms, evicted all users, \
and disabled incoming federation with the room."
))
.await
}
async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
let room_id = if room.is_room_id() {
let room_id = match RoomId::parse(&room) {
| Ok(room_id) => room_id,
| Err(e) => {
return Err!(
"Failed to parse room ID {room}. Please note that this requires a full \
room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`): {e}"
);
},
};
debug!("Room specified is a room ID, unbanning room ID");
self.services.rooms.metadata.ban_room(&room_id, false);
room_id.clone()
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
| Ok(room_alias) => room_alias,
| Err(e) => {
return Err!(
"Failed to parse room ID {room}. Please note that this requires a full \
room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`): {e}"
);
},
};
debug!(
"Room specified is not a room ID, attempting to resolve room alias to a room ID \
locally, if not using get_alias_helper to fetch room ID remotely"
);
let room_id = match self
.services
.rooms
.alias
.resolve_local_alias(&room_alias)
.await
{
| Ok(room_id) => room_id,
| _ => {
debug!(
"We don't have this room alias to a room ID locally, attempting to \
fetch room ID over federation"
);
match self.services.rooms.alias.resolve_alias(&room_alias).await {
| Ok((room_id, servers)) => {
debug!(
%room_id,
?servers,
"Got federation response fetching room ID for room {room}"
);
room_id
},
| Err(e) => {
return Err!("Failed to resolve room alias {room} to a room ID: {e}");
},
}
},
};
self.services.rooms.metadata.ban_room(&room_id, false);
room_id
} else {
return Err!(
"Room specified is not a room ID or room alias. Please note that this requires \
a full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`)",
);
},
};
self.services.rooms.metadata.disable_room(&room_id, false);
self.write_str("Room unbanned and federation re-enabled.")
.await
}
debug!("Room specified is a room ID, unbanning room ID");
self.services.rooms.metadata.ban_room(&room_id, false);
async fn list_banned_rooms(&self, no_details: bool) -> Result {
let room_ids: Vec<OwnedRoomId> = self
room_id.clone()
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
| Ok(room_alias) => room_alias,
| Err(e) => {
return Err!(
"Failed to parse room ID {room}. Please note that this requires a full room \
ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`): {e}"
);
},
};
debug!(
"Room specified is not a room ID, attempting to resolve room alias to a room ID \
locally, if not using get_alias_helper to fetch room ID remotely"
);
let room_id = match self
.services
.rooms
.metadata
.list_banned_rooms()
.map(Into::into)
.collect()
.await;
if room_ids.is_empty() {
return Err!("No rooms are banned.");
}
let mut rooms = room_ids
.iter()
.stream()
.then(|room_id| get_room_info(self.services, room_id))
.collect::<Vec<_>>()
.await;
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let num = rooms.len();
let body = rooms
.iter()
.map(|(id, members, name)| {
if no_details {
format!("{id}")
} else {
format!("{id}\tMembers: {members}\tName: {name}")
}
})
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```"))
.alias
.resolve_local_alias(&room_alias)
.await
}
{
| Ok(room_id) => room_id,
| _ => {
debug!(
"We don't have this room alias to a room ID locally, attempting to fetch \
room ID over federation"
);
match self.services.rooms.alias.resolve_alias(&room_alias).await {
| Ok((room_id, servers)) => {
debug!(
%room_id,
?servers,
"Got federation response fetching room ID for room {room}"
);
room_id
},
| Err(e) => {
return Err!("Failed to resolve room alias {room} to a room ID: {e}");
},
}
},
};
self.services.rooms.metadata.ban_room(&room_id, false);
room_id
} else {
return Err!(
"Room specified is not a room ID or room alias. Please note that this requires a \
full room ID (`!awIh6gGInaS5wLQJwa:example.com`) or a room alias \
(`#roomalias:example.com`)",
);
};
self.services.rooms.metadata.disable_room(&room_id, false);
self.write_str("Room unbanned and federation re-enabled.")
.await
}
#[admin_command]
async fn list_banned_rooms(&self, no_details: bool) -> Result {
let room_ids: Vec<OwnedRoomId> = self
.services
.rooms
.metadata
.list_banned_rooms()
.map(Into::into)
.collect()
.await;
if room_ids.is_empty() {
return Err!("No rooms are banned.");
}
let mut rooms = room_ids
.iter()
.stream()
.then(|room_id| get_room_info(self.services, room_id))
.collect::<Vec<_>>()
.await;
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let num = rooms.len();
let body = rooms
.iter()
.map(|(id, members, name)| {
if no_details {
format!("{id}")
} else {
format!("{id}\tMembers: {members}\tName: {name}")
}
})
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```"))
.await
}
+237 -224
View File
@@ -7,230 +7,243 @@
};
use futures::TryStreamExt;
impl crate::Context<'_> {
pub(super) async fn uptime(&self) -> Result {
let elapsed = self
.services
.server
.started
.elapsed()
.expect("standard duration");
use crate::admin_command;
let result = time::pretty(elapsed);
self.write_str(&format!("{result}.")).await
}
#[admin_command]
pub(super) async fn uptime(&self) -> Result {
let elapsed = self
.services
.server
.started
.elapsed()
.expect("standard duration");
pub(super) async fn show_config(&self) -> Result {
self.bail_restricted()?;
self.write_str(&format!("{}", *self.services.server.config))
.await
}
pub(super) async fn reload_config(&self, path: Option<PathBuf>) -> Result {
// The path argument is only what's optionally passed via the admin command,
// so we need to merge it with the existing paths if any were given at startup.
let mut paths = Vec::new();
// Add previously saved paths to the argument list
self.services
.config
.config_paths
.clone()
.unwrap_or_default()
.iter()
.for_each(|p| paths.push(p.to_owned()));
// If a path is given, and it's not already in the list,
// add it last, so that it overrides earlier files
if let Some(p) = path {
if !paths.contains(&p) {
paths.push(p);
}
}
self.services.config.reload(&paths)?;
self.write_str(&format!("Successfully reconfigured from paths: {paths:?}"))
.await
}
pub(super) async fn memory_usage(&self) -> Result {
let services_usage = self.services.memory_usage().await?;
let database_usage = self.services.db.db.memory_usage()?;
let allocator_usage = conduwuit::alloc::memory_usage()
.map_or(String::new(), |s| format!("\nAllocator:\n{s}"));
self.write_str(&format!(
"Services:\n{services_usage}\nDatabase:\n{database_usage}{allocator_usage}",
))
.await
}
pub(super) async fn clear_caches(&self) -> Result {
self.services.clear_cache().await;
self.write_str("Done.").await
}
pub(super) async fn list_backups(&self) -> Result {
self.services
.db
.db
.backup_list()?
.try_stream()
.try_for_each(|result| writeln!(self, "{result}"))
.await
}
pub(super) async fn backup_database(&self) -> Result {
self.bail_restricted()?;
let db = Arc::clone(&self.services.db);
let result = self
.services
.server
.runtime()
.spawn_blocking(move || match db.db.backup() {
| Ok(()) => "Done".to_owned(),
| Err(e) => format!("Failed: {e}"),
})
.await?;
let count = self.services.db.db.backup_count()?;
self.write_str(&format!("{result}. Currently have {count} backups."))
.await
}
pub(super) async fn admin_notice(&self, message: Vec<String>) -> Result {
let message = message.join(" ");
self.services.admin.send_text(&message).await;
self.write_str("Notice was sent to #admins").await
}
pub(super) async fn reload_mods(&self) -> Result {
self.bail_restricted()?;
self.services.server.reload()?;
self.write_str("Reloading server...").await
}
#[cfg(unix)]
pub(super) async fn restart(&self, force: bool) -> Result {
use conduwuit::utils::sys::current_exe_deleted;
if !force && current_exe_deleted() {
return Err!(
"The server cannot be restarted because the executable changed. If this is \
expected use --force to override."
);
}
self.services.server.restart()?;
self.write_str("Restarting server...").await
}
pub(super) async fn shutdown(&self) -> Result {
self.bail_restricted()?;
warn!("shutdown command");
self.services.server.shutdown()?;
self.write_str("Shutting down server...").await
}
pub(super) async fn list_features(&self) -> Result {
let mut enabled_features = conduwuit::info::introspection::ENABLED_FEATURES
.lock()
.expect("locked")
.values()
.flat_map(|f| f.iter())
.collect::<Vec<_>>();
enabled_features.sort_unstable();
enabled_features.dedup();
let mut available_features = conduwuit::build_metadata::WORKSPACE_FEATURES
.iter()
.flat_map(|(_, f)| f.iter())
.collect::<Vec<_>>();
available_features.sort_unstable();
available_features.dedup();
let mut features = String::new();
for feature in available_features {
let active = enabled_features.contains(&feature);
let emoji = if active { "" } else { "" };
let remark = if active { "[enabled]" } else { "" };
writeln!(features, "{emoji} {feature} {remark}")?;
}
self.write_str(&features).await
}
pub(super) async fn build_info(&self) -> Result {
use conduwuit::build_metadata::built;
let mut info = String::new();
// Version information
writeln!(info, "# Build Information\n")?;
writeln!(info, "**Version:** {}", built::PKG_VERSION)?;
writeln!(info, "**Package:** {}", built::PKG_NAME)?;
writeln!(info, "**Description:** {}", built::PKG_DESCRIPTION)?;
// Git information
writeln!(info, "\n## Git Information\n")?;
if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH {
writeln!(info, "**Commit Hash:** {hash}")?;
}
if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH_SHORT {
writeln!(info, "**Commit Hash (short):** {hash}")?;
}
if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_WEB_URL {
writeln!(info, "**Repository:** {url}")?;
}
if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_COMMIT_URL {
writeln!(info, "**Commit URL:** {url}")?;
}
// Build environment
writeln!(info, "\n## Build Environment\n")?;
writeln!(info, "**Profile:** {}", built::PROFILE)?;
writeln!(info, "**Optimization Level:** {}", built::OPT_LEVEL)?;
writeln!(info, "**Debug:** {}", built::DEBUG)?;
writeln!(info, "**Target:** {}", built::TARGET)?;
writeln!(info, "**Host:** {}", built::HOST)?;
// Rust compiler information
writeln!(info, "\n## Compiler Information\n")?;
writeln!(info, "**Rustc Version:** {}", built::RUSTC_VERSION)?;
if !built::RUSTDOC_VERSION.is_empty() {
writeln!(info, "**Rustdoc Version:** {}", built::RUSTDOC_VERSION)?;
}
// Target configuration
writeln!(info, "\n## Target Configuration\n")?;
writeln!(info, "**Architecture:** {}", built::CFG_TARGET_ARCH)?;
writeln!(info, "**OS:** {}", built::CFG_OS)?;
writeln!(info, "**Family:** {}", built::CFG_FAMILY)?;
writeln!(info, "**Endianness:** {}", built::CFG_ENDIAN)?;
writeln!(info, "**Pointer Width:** {} bits", built::CFG_POINTER_WIDTH)?;
if !built::CFG_ENV.is_empty() {
writeln!(info, "**Environment:** {}", built::CFG_ENV)?;
}
// CI information
if let Some(ci) = built::CI_PLATFORM {
writeln!(info, "\n## CI Platform\n")?;
writeln!(info, "**Platform:** {ci}")?;
}
self.write_str(&info).await
}
let result = time::pretty(elapsed);
self.write_str(&format!("{result}.")).await
}
#[admin_command]
pub(super) async fn show_config(&self) -> Result {
self.bail_restricted()?;
self.write_str(&format!("{}", *self.services.server.config))
.await
}
#[admin_command]
pub(super) async fn reload_config(&self, path: Option<PathBuf>) -> Result {
// The path argument is only what's optionally passed via the admin command,
// so we need to merge it with the existing paths if any were given at startup.
let mut paths = Vec::new();
// Add previously saved paths to the argument list
self.services
.config
.config_paths
.clone()
.unwrap_or_default()
.iter()
.for_each(|p| paths.push(p.to_owned()));
// If a path is given, and it's not already in the list,
// add it last, so that it overrides earlier files
if let Some(p) = path {
if !paths.contains(&p) {
paths.push(p);
}
}
self.services.config.reload(&paths)?;
self.write_str(&format!("Successfully reconfigured from paths: {paths:?}"))
.await
}
#[admin_command]
pub(super) async fn memory_usage(&self) -> Result {
let services_usage = self.services.memory_usage().await?;
let database_usage = self.services.db.db.memory_usage()?;
let allocator_usage =
conduwuit::alloc::memory_usage().map_or(String::new(), |s| format!("\nAllocator:\n{s}"));
self.write_str(&format!(
"Services:\n{services_usage}\nDatabase:\n{database_usage}{allocator_usage}",
))
.await
}
#[admin_command]
pub(super) async fn clear_caches(&self) -> Result {
self.services.clear_cache().await;
self.write_str("Done.").await
}
#[admin_command]
pub(super) async fn list_backups(&self) -> Result {
self.services
.db
.db
.backup_list()?
.try_stream()
.try_for_each(|result| writeln!(self, "{result}"))
.await
}
#[admin_command]
pub(super) async fn backup_database(&self) -> Result {
self.bail_restricted()?;
let db = Arc::clone(&self.services.db);
let result = self
.services
.server
.runtime()
.spawn_blocking(move || match db.db.backup() {
| Ok(()) => "Done".to_owned(),
| Err(e) => format!("Failed: {e}"),
})
.await?;
let count = self.services.db.db.backup_count()?;
self.write_str(&format!("{result}. Currently have {count} backups."))
.await
}
#[admin_command]
pub(super) async fn admin_notice(&self, message: Vec<String>) -> Result {
let message = message.join(" ");
self.services.admin.send_text(&message).await;
self.write_str("Notice was sent to #admins").await
}
#[admin_command]
pub(super) async fn reload_mods(&self) -> Result {
self.bail_restricted()?;
self.services.server.reload()?;
self.write_str("Reloading server...").await
}
#[admin_command]
#[cfg(unix)]
pub(super) async fn restart(&self, force: bool) -> Result {
use conduwuit::utils::sys::current_exe_deleted;
if !force && current_exe_deleted() {
return Err!(
"The server cannot be restarted because the executable changed. If this is expected \
use --force to override."
);
}
self.services.server.restart()?;
self.write_str("Restarting server...").await
}
#[admin_command]
pub(super) async fn shutdown(&self) -> Result {
self.bail_restricted()?;
warn!("shutdown command");
self.services.server.shutdown()?;
self.write_str("Shutting down server...").await
}
#[admin_command]
pub(super) async fn list_features(&self) -> Result {
let mut enabled_features = conduwuit::info::introspection::ENABLED_FEATURES
.lock()
.expect("locked")
.values()
.flat_map(|f| f.iter())
.collect::<Vec<_>>();
enabled_features.sort_unstable();
enabled_features.dedup();
let mut available_features = conduwuit::build_metadata::WORKSPACE_FEATURES
.iter()
.flat_map(|(_, f)| f.iter())
.collect::<Vec<_>>();
available_features.sort_unstable();
available_features.dedup();
let mut features = String::new();
for feature in available_features {
let active = enabled_features.contains(&feature);
let emoji = if active { "" } else { "" };
let remark = if active { "[enabled]" } else { "" };
writeln!(features, "{emoji} {feature} {remark}")?;
}
self.write_str(&features).await
}
#[admin_command]
pub(super) async fn build_info(&self) -> Result {
use conduwuit::build_metadata::built;
let mut info = String::new();
// Version information
writeln!(info, "# Build Information\n")?;
writeln!(info, "**Version:** {}", built::PKG_VERSION)?;
writeln!(info, "**Package:** {}", built::PKG_NAME)?;
writeln!(info, "**Description:** {}", built::PKG_DESCRIPTION)?;
// Git information
writeln!(info, "\n## Git Information\n")?;
if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH {
writeln!(info, "**Commit Hash:** {hash}")?;
}
if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH_SHORT {
writeln!(info, "**Commit Hash (short):** {hash}")?;
}
if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_WEB_URL {
writeln!(info, "**Repository:** {url}")?;
}
if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_COMMIT_URL {
writeln!(info, "**Commit URL:** {url}")?;
}
// Build environment
writeln!(info, "\n## Build Environment\n")?;
writeln!(info, "**Profile:** {}", built::PROFILE)?;
writeln!(info, "**Optimization Level:** {}", built::OPT_LEVEL)?;
writeln!(info, "**Debug:** {}", built::DEBUG)?;
writeln!(info, "**Target:** {}", built::TARGET)?;
writeln!(info, "**Host:** {}", built::HOST)?;
// Rust compiler information
writeln!(info, "\n## Compiler Information\n")?;
writeln!(info, "**Rustc Version:** {}", built::RUSTC_VERSION)?;
if !built::RUSTDOC_VERSION.is_empty() {
writeln!(info, "**Rustdoc Version:** {}", built::RUSTDOC_VERSION)?;
}
// Target configuration
writeln!(info, "\n## Target Configuration\n")?;
writeln!(info, "**Architecture:** {}", built::CFG_TARGET_ARCH)?;
writeln!(info, "**OS:** {}", built::CFG_OS)?;
writeln!(info, "**Family:** {}", built::CFG_FAMILY)?;
writeln!(info, "**Endianness:** {}", built::CFG_ENDIAN)?;
writeln!(info, "**Pointer Width:** {} bits", built::CFG_POINTER_WIDTH)?;
if !built::CFG_ENV.is_empty() {
writeln!(info, "**Environment:** {}", built::CFG_ENV)?;
}
// CI information
if let Some(ci) = built::CI_PLATFORM {
writeln!(info, "\n## CI Platform\n")?;
writeln!(info, "**Platform:** {ci}")?;
}
self.write_str(&info).await
}
+87 -87
View File
@@ -1,99 +1,99 @@
use conduwuit::{Err, Result, utils};
use conduwuit_macros::admin_command;
use futures::StreamExt;
use service::registration_tokens::TokenExpires;
impl crate::Context<'_> {
pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
let expires = {
if expires.immortal {
None
} else if let Some(max_uses) = expires.max_uses {
Some(TokenExpires::AfterUses(max_uses))
} else if expires.once {
Some(TokenExpires::AfterUses(1))
} else if let Some(max_age) = expires
.max_age
.as_deref()
.map(|max_age| {
utils::time::timepoint_from_now(utils::time::parse_duration(max_age)?)
})
.transpose()?
{
Some(TokenExpires::AfterTime(max_age))
} else {
unreachable!();
}
};
#[admin_command]
pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
let expires = {
if expires.immortal {
None
} else if let Some(max_uses) = expires.max_uses {
Some(TokenExpires::AfterUses(max_uses))
} else if expires.once {
Some(TokenExpires::AfterUses(1))
} else if let Some(max_age) = expires
.max_age
.as_deref()
.map(|max_age| utils::time::timepoint_from_now(utils::time::parse_duration(max_age)?))
.transpose()?
{
Some(TokenExpires::AfterTime(max_age))
} else {
unreachable!();
}
};
let (token, info) = self
.services
.registration_tokens
.issue_token(self.sender_or_service_user().into(), expires);
let (token, info) = self
.services
.registration_tokens
.issue_token(self.sender_or_service_user().into(), expires);
self.write_str(&format!(
"New registration token issued: `{token}` . {}.",
if let Some(expires) = info.expires {
format!("{expires}")
} else {
"Never expires".to_owned()
}
))
.await?;
if self
.services
.config
.oauth
.compatibility_mode
.oauth_available()
{
self.write_str(&format!(
"New registration token issued: `{token}` . {}.",
if let Some(expires) = info.expires {
format!("{expires}")
} else {
"Never expires".to_owned()
}
"\nInvite link using this token: {}",
self.services
.config
.get_client_domain()
.join(&format!(
"{}/account/register/?flow=trusted&token={token}",
conduwuit::ROUTE_PREFIX
))
.unwrap()
))
.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(())
}
pub(super) async fn revoke_token(&self, token: String) -> Result {
let Some(token) = self
.services
.registration_tokens
.validate_token(token)
.await
else {
return Err!("This token does not exist or has already expired.");
};
self.services.registration_tokens.revoke_token(token)?;
self.write_str("Token revoked successfully.").await
}
pub(super) async fn list_tokens(&self) -> Result {
let tokens: Vec<_> = self
.services
.registration_tokens
.iterate_tokens()
.collect()
.await;
self.write_str(&format!("Found {} registration tokens:\n", tokens.len()))
.await?;
for token in tokens {
self.write_str(&format!("- {token}\n")).await?;
}
Ok(())
}
Ok(())
}
#[admin_command]
pub(super) async fn revoke_token(&self, token: String) -> Result {
let Some(token) = self
.services
.registration_tokens
.validate_token(token)
.await
else {
return Err!("This token does not exist or has already expired.");
};
self.services.registration_tokens.revoke_token(token)?;
self.write_str("Token revoked successfully.").await
}
#[admin_command]
pub(super) async fn list_tokens(&self) -> Result {
let tokens: Vec<_> = self
.services
.registration_tokens
.iterate_tokens()
.collect()
.await;
self.write_str(&format!("Found {} registration tokens:\n", tokens.len()))
.await?;
for token in tokens {
self.write_str(&format!("- {token}\n")).await?;
}
Ok(())
}
+1052 -1035
View File
File diff suppressed because it is too large Load Diff
-3
View File
@@ -27,9 +27,6 @@ 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,
},
/// Get a user's associated email address.
+8 -1
View File
@@ -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)
}
+2 -3
View File
@@ -128,8 +128,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
@@ -264,7 +263,7 @@ pub(crate) async fn deactivate_route(
let sender_user = identity.expect_sender_user()?;
if !services.config.allow_deactivation {
return Err!(Request(Forbidden(
return Err!(Request(Unauthorized(
"You may not deactivate your own account. Contact your server's administrator for \
assistance."
)));
+4 -4
View File
@@ -72,7 +72,7 @@ pub(crate) async fn register_route(
.determine_registration_user_id(body.username.clone(), None, Some(appservice_info))
.await?;
services.users.create_shadow_account(&user_id).await?;
services.users.create(&user_id, None).await?;
user_id
} else {
@@ -97,8 +97,8 @@ pub(crate) async fn register_route(
services
.users
.create_local_account(&user_id, Some(password), identity.email)
.await?;
.create_local_account(&user_id, password, identity.email)
.await;
user_id
};
@@ -106,7 +106,7 @@ pub(crate) async fn register_route(
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() {
if !services.config.oauth.compatibility_mode.uiaa_available() {
return Err!(Request(AppserviceLoginUnsupported(
"User-interactive appservice registration is not available on this server."
)));
-4
View File
@@ -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())
-87
View File
@@ -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 -2
View File
@@ -1,4 +1,3 @@
mod lock;
mod suspend;
pub(crate) use self::{lock::*, suspend::*};
pub(crate) use self::suspend::*;
+24 -21
View File
@@ -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))
}
+12 -10
View File
@@ -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))
}
+1 -3
View File
@@ -121,9 +121,7 @@ pub(crate) async fn delete_device_route(
let sender_user = body.identity.expect_sender_user()?;
// Appservices get to skip UIAA for this endpoint
if !body.identity.is_appservice() {
let sender_device = body.identity.expect_sender_device()?;
if let Some(sender_device) = body.identity.sender_device() {
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
-7
View File
@@ -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(),
@@ -232,16 +231,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(),
+1 -3
View File
@@ -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);
}
+1 -1
View File
@@ -37,7 +37,7 @@ pub(crate) fn router(state: crate::State) -> Router<crate::State> {
.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() {
if state.config.oauth.compatibility_mode.oauth_available() {
next.run(request).await
} else {
(StatusCode::NOT_FOUND, "OAuth is unavailable on this server").into_response()
+1 -1
View File
@@ -21,7 +21,7 @@ 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() {
if !services.config.oauth.compatibility_mode.oauth_available() {
return Err!(Request(Unrecognized("OAuth is unavailable on this server")));
}
+289 -58
View File
@@ -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(), &current_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(())
}
+1 -1
View File
@@ -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());
}
+4 -4
View File
@@ -43,7 +43,7 @@ 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() {
if !services.config.oauth.compatibility_mode.uiaa_available() {
return Err!(Request(Unrecognized(
"User-interactive authentication is not available on this server."
)));
@@ -88,8 +88,8 @@ 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.users.is_locked(&user_id).await? {
return Err!(Request(UserLocked("This account has been locked.")));
if !services.globals.user_is_local(&user_id) {
return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
}
if services.users.is_login_disabled(&user_id).await {
@@ -120,7 +120,7 @@ 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() {
if !services.config.oauth.compatibility_mode.uiaa_available() {
return match body.login_info {
| LoginInfo::ApplicationService(_) => {
Err!(Request(AppserviceLoginUnsupported(
-7
View File
@@ -48,13 +48,6 @@ async fn load_timeline(
ending_count: Option<PduCount>,
limit: usize,
) -> Result<TimelinePdus> {
if let (Some(starting_count), Some(ending_count)) = (starting_count, ending_count) {
debug_assert!(
starting_count <= ending_count,
"starting count {starting_count} > ending count {ending_count}"
);
}
let mut pdu_stream = match starting_count {
| Some(starting_count) => {
let last_timeline_count = services
+81 -74
View File
@@ -38,7 +38,6 @@
uint,
};
use service::{account_data::AnyRawAccountDataEvent, rooms::short::ShortStateHash};
use tokio::pin;
use super::{load_timeline, share_encrypted_room};
use crate::client::{
@@ -97,19 +96,12 @@ pub(super) async fn load_joined_room(
);
}
let state_events =
StateEvents::with_events(state_events.into_iter().map(Event::into_format).collect());
let joined_room = assign!(JoinedRoom::new(), {
account_data,
summary: summary.unwrap_or_default(),
unread_notifications: notification_counts.unwrap_or_default(),
timeline,
state: if sync_context.use_state_after {
RoomState::After(state_events)
} else {
RoomState::Before(state_events)
},
state: RoomState::Before(StateEvents::with_events(state_events.into_iter().map(Event::into_format).collect())),
ephemeral,
unread_thread_notifications: BTreeMap::new(),
});
@@ -352,7 +344,7 @@ struct ShortStateHashes {
#[tracing::instrument(level = "debug", skip_all)]
async fn fetch_shortstatehashes(
services: &Services,
SyncContext { last_sync_end_count, .. }: SyncContext<'_>,
SyncContext { last_sync_end_count, current_count, .. }: SyncContext<'_>,
room_id: &RoomId,
) -> Result<ShortStateHashes> {
// the room state currently.
@@ -362,45 +354,46 @@ async fn fetch_shortstatehashes(
.rooms
.state
.get_room_shortstatehash(room_id)
.map_err(|_| err!(Database(error!("Room {room_id} has no state"))))
.await?;
.map_err(|_| err!(Database(error!("Room {room_id} has no state"))));
// The room state as of the end of the last sync.
// This will be None if we are doing an initial sync.
// the room state as of the end of the last sync.
// this will be None if we are doing an initial sync or if we just joined this
// room.
let last_sync_end_shortstatehash =
OptionFuture::from(last_sync_end_count.map(async |last_sync_end_count| {
pin! {
let pdus = services
.rooms
.timeline
.pdus(room_id, Some(PduCount::Normal(last_sync_end_count)))
.ignore_err();
}
match pdus.next().await {
| Some((_, pdu_after_last_sync_end)) => {
trace!(?pdu_after_last_sync_end.event_id, "pdu at last sync end");
Some(
services
.rooms
.state_accessor
.pdu_shortstatehash(&pdu_after_last_sync_end.event_id)
.await
.map_err(|err| {
err!("Last sync end PDU has no shortstatehash: {err}")
}),
)
},
| None => {
// No events have been sent since the last sync, or we just joined this room
None
},
}
OptionFuture::from(last_sync_end_count.map(|last_sync_end_count| {
// look up the shortstatehash saved by the last sync's call to
// `associate_token_shortstatehash`
services
.rooms
.user
.get_token_shortstatehash(room_id, last_sync_end_count)
.inspect_err(move |_| {
debug_warn!(
token = last_sync_end_count,
"Room has no shortstatehash for this token"
);
})
.ok()
}))
.await
.flatten()
.transpose()?;
.map(Option::flatten)
.map(Ok);
let (current_shortstatehash, last_sync_end_shortstatehash) =
try_join(current_shortstatehash, last_sync_end_shortstatehash).await?;
/*
associate the `current_count` with the `current_shortstatehash`, so we can
use it on the next sync as the `last_sync_end_shortstatehash`.
TODO: the table written to by this call grows extremely fast, gaining one new entry for each
joined room on _every single sync request_. we need to find a better way to remember the shortstatehash
between syncs.
*/
services
.rooms
.user
.associate_token_shortstatehash(room_id, current_count, current_shortstatehash)
.await;
Ok(ShortStateHashes {
current_shortstatehash,
@@ -459,7 +452,6 @@ async fn build_state_events(
syncing_user,
last_sync_end_count,
full_state,
use_state_after,
..
} = sync_context;
@@ -468,15 +460,32 @@ async fn build_state_events(
last_sync_end_shortstatehash,
} = shortstatehashes;
if timeline.pdus.is_empty() {
// If the timeline is empty there can't possibly be any changes to the state
return Ok(vec![]);
}
// the spec states that the `state` property only includes state events up to
// the beginning of the timeline, so we determine the state of the syncing room
// as of the first timeline event. NOTE: this explanation is not entirely
// accurate; see the implementation of `build_state_incremental`.
let timeline_start_shortstatehash = async {
if let Some((_, pdu)) = timeline.pdus.front() {
if let Ok(shortstatehash) = services
.rooms
.state_accessor
.pdu_shortstatehash(&pdu.event_id)
.await
{
return shortstatehash;
}
}
current_shortstatehash
};
// the user IDs of members whose membership needs to be sent to the client, if
// lazy-loading is enabled.
let lazily_loaded_members =
prepare_lazily_loaded_members(services, sync_context, room_id, timeline.senders()).await;
prepare_lazily_loaded_members(services, sync_context, room_id, timeline.senders());
let (timeline_start_shortstatehash, lazily_loaded_members) =
join(timeline_start_shortstatehash, lazily_loaded_members).await;
// compute the state delta between the previous sync and this sync.
match (last_sync_end_count, last_sync_end_shortstatehash) {
@@ -485,14 +494,16 @@ async fn build_state_events(
is Some (meaning the syncing user didn't just join this room for the first time ever), and `full_state` is false,
then use `build_state_incremental`.
*/
| (Some(_), Some(last_sync_end_shortstatehash)) if !full_state =>
| (Some(last_sync_end_count), Some(last_sync_end_shortstatehash)) if !full_state =>
build_state_incremental(
services,
syncing_user,
room_id,
PduCount::Normal(last_sync_end_count),
last_sync_end_shortstatehash,
timeline_start_shortstatehash,
current_shortstatehash,
timeline,
use_state_after,
lazily_loaded_members.as_ref(),
)
.boxed()
@@ -506,9 +517,7 @@ async fn build_state_events(
build_state_initial(
services,
syncing_user,
current_shortstatehash,
timeline,
use_state_after,
timeline_start_shortstatehash,
lazily_loaded_members.as_ref(),
)
.boxed()
@@ -589,25 +598,23 @@ async fn check_joined_since_last_sync(
ShortStateHashes { last_sync_end_shortstatehash, .. }: ShortStateHashes,
SyncContext { syncing_user, .. }: SyncContext<'_>,
) -> Result<bool> {
let Some(last_sync_end_shortstatehash) = last_sync_end_shortstatehash else {
// For initial syncs always return false, since there's no "last sync" for the
// user to have joined since.
return Ok(false);
// fetch the syncing user's membership event during the last sync.
// this will be None if `previous_sync_end_shortstatehash` is None.
let membership_during_previous_sync = match last_sync_end_shortstatehash {
| Some(last_sync_end_shortstatehash) => services
.rooms
.state_accessor
.state_get_content(
last_sync_end_shortstatehash,
&StateEventType::RoomMember,
syncing_user.as_str(),
)
.await
.inspect_err(|_| debug_warn!("User has no previous membership"))
.ok(),
| None => None,
};
// Fetch the syncing user's membership event during the last sync.
let membership_during_previous_sync = services
.rooms
.state_accessor
.state_get_content(
last_sync_end_shortstatehash,
&StateEventType::RoomMember,
syncing_user.as_str(),
)
.await
.inspect_err(|_| debug_warn!("User has no previous membership"))
.ok();
// TODO: If the requesting user got state-reset out of the room, this
// will be `true` when it shouldn't be. this function should never be called
// in that situation, but it may be if the membership cache didn't get updated.
+25 -13
View File
@@ -4,7 +4,7 @@
trace,
utils::{self, IterStream, future::ReadyEqExt, stream::WidebandExt as _},
};
use futures::StreamExt;
use futures::{StreamExt, future::join};
use ruma::{
EventId, OwnedRoomId, RoomId,
api::client::sync::sync_events::v3::{
@@ -181,9 +181,6 @@ pub(super) async fn load_left_room(
.collect::<Vec<_>>()
.await;
let state_events =
StateEvents::with_events(state_events.into_iter().map(Event::into_format).collect());
Ok(Some(assign!(LeftRoom::new(), {
account_data: RoomAccountData::new(),
timeline: assign!(Timeline::new(), {
@@ -191,11 +188,7 @@ pub(super) async fn load_left_room(
prev_batch: Some(current_count.to_string()),
events: raw_timeline_pdus,
}),
state: if sync_context.use_state_after {
State::After(state_events)
} else {
State::Before(state_events)
},
state: State::Before(StateEvents::with_events(state_events.into_iter().map(Event::into_format).collect())),
})))
}
@@ -240,8 +233,29 @@ async fn build_left_state_and_timeline(
)
.await?;
let timeline_start_shortstatehash = async {
if let Some((_, pdu)) = timeline.pdus.front() {
if let Ok(shortstatehash) = services
.rooms
.state_accessor
.pdu_shortstatehash(&pdu.event_id)
.await
{
return shortstatehash;
}
}
// the timeline generally should not be empty (see the TODO further down),
// but in case it is we use `leave_shortstatehash` as the state to
// send
leave_shortstatehash
};
let lazily_loaded_members =
prepare_lazily_loaded_members(services, sync_context, room_id, timeline.senders()).await;
prepare_lazily_loaded_members(services, sync_context, room_id, timeline.senders());
let (timeline_start_shortstatehash, lazily_loaded_members) =
join(timeline_start_shortstatehash, lazily_loaded_members).await;
// TODO: calculate incremental state for incremental syncs.
// always calculating initial state _works_ but returns more data and does
@@ -249,9 +263,7 @@ async fn build_left_state_and_timeline(
let mut state = build_state_initial(
services,
syncing_user,
leave_shortstatehash,
&timeline,
sync_context.use_state_after,
timeline_start_shortstatehash,
lazily_loaded_members.as_ref(),
)
.await?;
+4 -7
View File
@@ -11,11 +11,12 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, at, error, extract_variant,
Err, Result, at, extract_variant,
utils::{
ReadyExt, TryFutureExtExt,
stream::{BroadbandExt, Tools, WidebandExt},
},
warn,
};
use conduwuit_service::Services;
use futures::{FutureExt, StreamExt, TryFutureExt, future::OptionFuture};
@@ -110,9 +111,6 @@ struct SyncContext<'a> {
/// The sync filter, which the client uses to specify what data should be
/// included in the sync response.
filter: &'a FilterDefinition,
/// Whether the state at the end of the timeline should be used when
/// calculating state diffs for sync.
use_state_after: bool,
}
impl<'a> SyncContext<'a> {
@@ -266,7 +264,6 @@ pub(crate) async fn build_sync_events(
current_count,
full_state,
filter: &filter,
use_state_after: body.use_state_after,
};
let joined_rooms = services
@@ -279,7 +276,7 @@ pub(crate) async fn build_sync_events(
match joined_room {
| Ok((room, updates)) => Some((room_id, room, updates)),
| Err(err) => {
error!(?err, %room_id, "error loading joined room");
warn!(?err, %room_id, "error loading joined room");
None
},
}
@@ -308,7 +305,7 @@ pub(crate) async fn build_sync_events(
| Ok(Some(left_room)) => Some((room_id, left_room)),
| Ok(None) => None,
| Err(err) => {
error!(?err, %room_id, "error loading joined room");
warn!(?err, %room_id, "error loading joined room");
None
},
}
+146 -63
View File
@@ -1,8 +1,11 @@
use std::collections::HashSet;
use std::{collections::BTreeSet, ops::ControlFlow};
use conduwuit::{
Result, at,
matrix::{Event, pdu::PduEvent},
Result, at, is_equal_to,
matrix::{
Event,
pdu::{PduCount, PduEvent},
},
utils::{
BoolExt, IterStream, ReadyExt, TryFutureExtExt,
stream::{BroadbandExt, TryIgnore},
@@ -13,7 +16,9 @@
rooms::{lazy_loading::MemberSet, short::ShortStateHash},
};
use futures::{FutureExt, StreamExt};
use ruma::{OwnedEventId, UserId, events::StateEventType};
use itertools::Itertools;
use ruma::{OwnedEventId, RoomId, UserId, events::StateEventType};
use service::rooms::short::ShortEventId;
use tracing::trace;
use crate::client::TimelinePdus;
@@ -33,22 +38,14 @@
pub(super) async fn build_state_initial(
services: &Services,
sender_user: &UserId,
timeline_end_shortstatehash: ShortStateHash,
timeline: &TimelinePdus,
use_state_after: bool,
timeline_start_shortstatehash: ShortStateHash,
lazily_loaded_members: Option<&MemberSet>,
) -> Result<Vec<PduEvent>> {
let event_ids_in_timeline: HashSet<_> =
timeline.pdus.iter().map(|pdu| &pdu.1.event_id).collect();
// load the keys and event IDs of the state events at the start of the timeline
let (shortstatekeys, event_ids): (Vec<_>, Vec<_>) = services
.rooms
.state_accessor
.state_full_ids(timeline_end_shortstatehash)
.ready_filter(|(_, event_id)| {
use_state_after || !event_ids_in_timeline.contains(event_id)
})
.state_full_ids(timeline_start_shortstatehash)
.unzip()
.await;
@@ -95,32 +92,82 @@ pub(super) async fn build_state_initial(
pub(super) async fn build_state_incremental<'a>(
services: &Services,
sender_user: &'a UserId,
room_id: &RoomId,
last_sync_end_count: PduCount,
last_sync_end_shortstatehash: ShortStateHash,
timeline_start_shortstatehash: ShortStateHash,
timeline_end_shortstatehash: ShortStateHash,
timeline: &TimelinePdus,
use_state_after: bool,
lazily_loaded_members: Option<&'a MemberSet>,
) -> Result<Vec<PduEvent>> {
let mut state_event_ids: HashSet<OwnedEventId> = HashSet::new();
/*
NB: a limited sync is one where `timeline.limited == true`. Synapse calls this a "gappy" sync internally.
trace!(
%use_state_after,
%last_sync_end_shortstatehash,
%timeline_end_shortstatehash,
"computing state for incremental sync"
);
The algorithm implemented in this function is, currently, quite different from the algorithm vaguely described
by the Matrix specification. This is because the specification's description of the `state` property does not accurately
reflect how Synapse behaves, and therefore how client SDKs behave. Notable differences include:
1. We do not compute the delta using the naive approach of "every state event from the end of the last sync
up to the start of this sync's timeline". see below for details.
2. If lazy-loading is enabled, we include lazily-loaded membership events. The specific users to include are determined
elsewhere and supplied to this function in the `lazily_loaded_members` parameter.
*/
// Fetch lazy-loaded membership events if lazy-loading is enabled
if let Some(lazily_loaded_members) = lazily_loaded_members
&& !lazily_loaded_members.is_empty()
{
trace!("including lazy membership events for members: {:?}", lazily_loaded_members);
/*
the `state` property of an incremental sync which isn't limited are _usually_ empty.
(note: the specification says that the `state` property is _always_ empty for limited syncs, which is incorrect.)
however, if an event in the timeline (`timeline.pdus`) merges a split in the room's DAG (i.e. has multiple `prev_events`),
the state at the _end_ of the timeline may include state events which were merged in and don't exist in the state
at the _start_ of the timeline. because this is uncommon, we check here to see if any events in the timeline
merged a split in the DAG.
services
see: https://github.com/element-hq/synapse/issues/16941
*/
let timeline_is_linear = timeline.pdus.is_empty() || {
let last_pdu_of_last_sync = services
.rooms
.short
.multi_get_eventid_from_short::<'_, OwnedEventId, _>(
lazily_loaded_members
.timeline
.pdus_rev(room_id, Some(last_sync_end_count.saturating_add(1)))
.boxed()
.next()
.await
.transpose()
.expect("last sync should have had some PDUs")
.map(at!(1));
// make sure the prev_events of each pdu in the timeline refer only to the
// previous pdu
timeline
.pdus
.iter()
.try_fold(last_pdu_of_last_sync.map(|pdu| pdu.event_id), |prev_event_id, (_, pdu)| {
if let Ok(pdu_prev_event_id) = pdu.prev_events.iter().exactly_one() {
if prev_event_id
.as_ref()
.is_none_or(is_equal_to!(pdu_prev_event_id))
{
return ControlFlow::Continue(Some(pdu_prev_event_id.to_owned()));
}
}
trace!(
"pdu {:?} has split prev_events (expected {:?}): {:?}",
pdu.event_id, prev_event_id, pdu.prev_events
);
ControlFlow::Break(())
})
.is_continue()
};
if timeline_is_linear && !timeline.limited {
// if there are no splits in the DAG and the timeline isn't limited, then
// `state` will always be empty unless lazy loading is enabled.
if let Some(lazily_loaded_members) = lazily_loaded_members {
if !timeline.pdus.is_empty() {
// lazy loading is enabled, so we return the membership events which were
// requested by the caller.
let lazy_membership_events: Vec<_> = lazily_loaded_members
.iter()
.stream()
.broad_filter_map(|user_id| async move {
@@ -131,24 +178,71 @@ pub(super) async fn build_state_incremental<'a>(
services
.rooms
.state_accessor
.state_get_shortid(
timeline_end_shortstatehash,
.state_get(
timeline_start_shortstatehash,
&StateEventType::RoomMember,
user_id.as_str(),
)
.ok()
.await
}),
)
.ignore_err()
.ready_for_each(|event_id| {
state_event_ids.insert(event_id);
})
.await;
})
.collect()
.await;
if !lazy_membership_events.is_empty() {
trace!(
"syncing lazy membership events for members: {:?}",
lazy_membership_events
.iter()
.map(|pdu| pdu.state_key().unwrap())
.collect::<Vec<_>>()
);
}
return Ok(lazy_membership_events);
}
}
// lazy loading is disabled, `state` is empty.
return Ok(vec![]);
}
// Fetch the state events added since the last sync.
services
/*
at this point, either the timeline is `limited` or the DAG has a split in it. this necessitates
computing the incremental state (which may be empty).
NOTE: this code path does not use the `lazy_membership_events` parameter. any changes to membership will be included
in the incremental state. therefore, the incremental state may include "redundant" membership events,
which we do not filter out because A. the spec forbids lazy-load filtering if the timeline is `limited`,
and B. DAG splits which require sending extra membership state events are (probably) uncommon enough that
the performance penalty is acceptable.
*/
trace!(%timeline_is_linear, %timeline.limited, "computing state for incremental sync");
// fetch the shorteventids of state events in the timeline
let state_events_in_timeline: BTreeSet<ShortEventId> = services
.rooms
.short
.multi_get_or_create_shorteventid(timeline.pdus.iter().filter_map(|(_, pdu)| {
if pdu.state_key().is_some() {
Some(pdu.event_id.as_ref())
} else {
None
}
}))
.collect()
.await;
trace!("{} state events in timeline", state_events_in_timeline.len());
/*
fetch the state events which were added since the last sync.
specifically we fetch the difference between the state at the last sync and the state at the _end_
of the timeline, and then we filter out state events in the timeline itself using the shorteventids we fetched.
this is necessary to account for splits in the DAG, as explained above.
*/
let state_diff = services
.rooms
.short
.multi_get_eventid_from_short::<'_, OwnedEventId, _>(
@@ -158,29 +252,18 @@ pub(super) async fn build_state_incremental<'a>(
.state_added((last_sync_end_shortstatehash, timeline_end_shortstatehash))
.await?
.stream()
.map(at!(1)),
.ready_filter_map(|(_, shorteventid)| {
if state_events_in_timeline.contains(&shorteventid) {
None
} else {
Some(shorteventid)
}
}),
)
.ignore_err()
.ready_for_each(|event_id| {
state_event_ids.insert(event_id);
})
.await;
.ignore_err();
if !use_state_after {
// If state_after isn't enabled, filter out state events which also exist
// in the timeline. If splits exist in the DAG, this may not be exactly the same
// thing as the state diff ending at the start of the timeline, but Synapse
// also does this and it's technically more useful behavior anyway.
// See: https://github.com/element-hq/synapse/issues/16941
for (_, pdu) in &timeline.pdus {
state_event_ids.remove(pdu.event_id());
}
}
// Finally, fetch the PDU contents and collect them into a vec
let state_diff_pdus = state_event_ids
.stream()
// finally, fetch the PDU contents and collect them into a vec
let state_diff_pdus = state_diff
.broad_filter_map(|event_id| async move {
services
.rooms
+7 -27
View File
@@ -15,7 +15,7 @@
BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt,
future::ReadyEqExt,
math::{ruma_from_usize, usize_from_ruma},
stream::{TryIgnore, WidebandExt},
stream::WidebandExt,
},
warn,
};
@@ -41,7 +41,6 @@
uint,
};
use service::account_data::AnyRawAccountDataEvent;
use tokio::pin;
use super::share_encrypted_room;
use crate::{
@@ -858,31 +857,12 @@ async fn collect_e2ee<'a, Rooms>(
continue;
};
let since_shortstatehash = async {
pin! {
let pdus_rev = services
.rooms
.timeline
.pdus_rev(room_id, Some(PduCount::Normal(globalsince.saturating_sub(1))))
.ignore_err();
}
let (count, pdu_at_last_sync_end) = pdus_rev.next().await?;
if matches!(count, PduCount::Backfilled(_)) {
None
} else {
Some(
services
.rooms
.state_accessor
.pdu_shortstatehash(&pdu_at_last_sync_end.event_id)
.await
.expect("pdu should have a shortstatehash"),
)
}
}
.await;
let since_shortstatehash = services
.rooms
.user
.get_token_shortstatehash(room_id, globalsince)
.await
.ok();
let encrypted_room = services
.rooms
+26 -30
View File
@@ -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();
-2
View File
@@ -181,8 +181,6 @@ 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)
-6
View File
@@ -166,9 +166,6 @@ async fn verify<B: AsRef<[u8]> + Sync>(
query: AuthQueryParams,
route: TypeId,
) -> Result<Self::Identity> {
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
{
@@ -283,9 +280,6 @@ async fn verify<B: AsRef<[u8]> + Sync>(
_query: AuthQueryParams,
_route: TypeId,
) -> Result<Self::Identity> {
if output.is_empty() {
return Err!(Request(Unauthorized("Missing access token.")));
}
let Ok(appservice_info) = services.appservice.find_from_token(&output).await else {
return Err!(Request(Unauthorized("Invalid appservice token.")));
};
+1 -1
View File
@@ -32,7 +32,7 @@ pub(crate) async fn get_backfill_route(
room_id: &body.room_id,
event_id: None,
}
.assert()
.check()
.await?;
if !services
.rooms
+1 -1
View File
@@ -44,7 +44,7 @@ pub(crate) async fn get_event_route(
room_id,
event_id: Some(&body.event_id),
}
.assert()
.check()
.await?;
if !services
+1 -1
View File
@@ -23,7 +23,7 @@ pub(crate) async fn get_event_authorization_route(
room_id: &body.room_id,
event_id: None,
}
.assert()
.check()
.await?;
if services
+8 -4
View File
@@ -4,11 +4,15 @@
use conduwuit::{Err, Event, Result, debug, info, trace, utils::to_canonical_object, warn};
use ruma::{OwnedEventId, api::federation::event::get_missing_events};
use serde_json::{json, value::RawValue};
use service::rooms::event_handler::GET_MISSING_EVENTS_MAX_BATCH_SIZE;
use super::AccessCheck;
use crate::Ruma;
/// arbitrary number but synapse's is 20 and we can handle lots of these anyways
const LIMIT_MAX: usize = 50;
/// spec says default is 10
const LIMIT_DEFAULT: usize = 10;
/// # `POST /_matrix/federation/v1/get_missing_events/{roomId}`
///
/// Retrieves events that the sender is missing.
@@ -22,7 +26,7 @@ pub(crate) async fn get_missing_events_route(
room_id: &body.room_id,
event_id: None,
}
.assert()
.check()
.await?;
if !services
@@ -41,8 +45,8 @@ pub(crate) async fn get_missing_events_route(
let limit = body
.limit
.try_into()
.unwrap_or(10)
.min(GET_MISSING_EVENTS_MAX_BATCH_SIZE);
.unwrap_or(LIMIT_DEFAULT)
.min(LIMIT_MAX);
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
+7 -8
View File
@@ -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()
+84 -33
View File
@@ -1,5 +1,5 @@
use std::{
collections::{BTreeMap, HashMap},
collections::{BTreeMap, HashMap, HashSet},
net::IpAddr,
time::{Duration, Instant},
};
@@ -7,8 +7,9 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Error, Result, debug, debug_error, debug_warn, err, error,
Err, Error, Result, debug, 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;
@@ -131,7 +133,6 @@ async fn wait_for_result(
}
#[instrument(
name="transaction"
skip_all,
fields(
id = ?body.transaction_id.as_str(),
@@ -173,14 +174,8 @@ async fn process_inbound_transaction(
for (id, result) in &results {
if let Err(e) = result {
match e {
| Error::BadRequest(
ErrorKind::Forbidden | ErrorKind::InvalidParam | ErrorKind::BadJson,
..,
) => {
debug_warn!("Incoming PDU {id} failed: {e:?}");
},
| _ => debug_error!("Incoming PDU {id} failed: {e:?}"),
if matches!(e, Error::BadRequest(ErrorKind::NotFound, _)) {
debug_warn!("Incoming PDU failed {id}: {e:?}");
}
}
}
@@ -274,6 +269,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 +352,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 +360,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 +370,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,8 +380,7 @@ async fn handle_room(
let result = services
.rooms
.event_handler
.handle_incoming_pdu(origin, room_id, &event_id, value.clone(), true)
.boxed()
.handle_incoming_pdu(origin, room_id, &event_id, value, true)
.await
.map(|_| ());
results.push((event_id, result));
@@ -617,14 +676,6 @@ async fn handle_edu_direct_to_device(
messages
.into_iter()
.stream()
.broad_filter_map(|(target_user_id, map)| async move {
services
.users
.status(&target_user_id)
.await
.is_active()
.then_some((target_user_id, map))
})
.for_each_concurrent(automatic_width(), |(target_user_id, map)| {
handle_edu_direct_to_device_user(services, target_user_id, sender, &ev_type, map)
})
+1 -1
View File
@@ -21,7 +21,7 @@ pub(crate) async fn get_room_state_route(
room_id: &body.room_id,
event_id: None,
}
.assert()
.check()
.await?;
if services

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