mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-02 07:11:40 +00:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61a2e236b6 | |||
| d4fdf87daa | |||
| 513259a837 | |||
| 0f14a91bf3 | |||
| d557ed9a2c | |||
| cad2bb659b | |||
| 4ee69f9061 | |||
| 9812067c39 | |||
| 10136d4f78 | |||
| d6d0694387 | |||
| 0db74089c1 | |||
| efe37dab12 | |||
| 1f16468dac | |||
| 00bdffb783 | |||
| ed83d8fbb4 | |||
| 50f22cbf10 | |||
| 476f5249ce | |||
| d4eff0256c | |||
| 23aa0f5005 | |||
| 0594892921 | |||
| 5c16cb60fd | |||
| 809a9429dc | |||
| 707587aa15 | |||
| a10c709f1a | |||
| 7407435334 | |||
| cb7c678a34 | |||
| 0db01a6763 | |||
| d7b066c03a | |||
| 5c2afd9f0d | |||
| 886aaf0e17 | |||
| 86a33b5eb3 | |||
| 01dcb9cf9d | |||
| 5effc3411e | |||
| d67000f0bc | |||
| f294361eb2 | |||
| d1eeefed77 | |||
| 1c347841ce | |||
| a73973197d | |||
| 74e0d87c0d | |||
| 3691cccf11 | |||
| 86368b46b0 | |||
| 71a44bf7ea | |||
| 81b865f258 | |||
| b0d90e614c | |||
| e6f8e6c868 | |||
| db460e0a59 | |||
| 810b40536b | |||
| 577b33599f | |||
| 573f2e4892 | |||
| 8aa93c7839 | |||
| 9aed687ee1 | |||
| c3df2e4570 | |||
| 8c178c32f9 | |||
| e8d409c6ed | |||
| b50c7027f5 | |||
| 0ed101e973 | |||
| 2d7a35310f | |||
| cf1b3c6d4b | |||
| 07f7d6f071 |
@@ -33,7 +33,7 @@ runs:
|
||||
echo "version=$(rustup --version)" >> $GITHUB_OUTPUT
|
||||
- name: Cache rustup toolchains
|
||||
if: steps.rustup-version.outputs.version == ''
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
~/.rustup
|
||||
|
||||
@@ -57,7 +57,7 @@ runs:
|
||||
|
||||
- name: Check for LLVM cache
|
||||
id: cache
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
/usr/bin/clang-*
|
||||
|
||||
@@ -65,7 +65,7 @@ runs:
|
||||
|
||||
- name: Cache toolchain binaries
|
||||
id: toolchain-cache
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
.cargo/registry/index
|
||||
|
||||
@@ -31,7 +31,7 @@ runs:
|
||||
|
||||
- name: Restore binary cache
|
||||
id: binary-cache
|
||||
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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@e49978b799e49ff429d162b7a30601a569ab6538 # 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@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ${{ env.TIMELORD_CACHE_PATH }}
|
||||
key: ${{ env.TIMELORD_KEY }}
|
||||
|
||||
@@ -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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Cache Cargo registry
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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
|
||||
|
||||
@@ -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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
|
||||
- name: Cache DNF packages
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
~/rpmbuild/BUILD/*/target/release/deps
|
||||
|
||||
@@ -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/*
|
||||
@@ -14,19 +14,23 @@ jobs:
|
||||
name: Check changelog is added
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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"
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Sync repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
node-version: 22
|
||||
|
||||
- name: Cache npm dependencies
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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') }}
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
DOCKER_MIRROR_TOKEN: ${{ secrets.DOCKER_MIRROR_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@c70ad64367908075211b10dcd2ab9fad4bfa1816 # main
|
||||
|
||||
- name: Check what images need mirroring
|
||||
run: |
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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/*
|
||||
|
||||
@@ -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.214.6@sha256:fd228b92f067204e444ddea1ec2fefb007592f9a46845e966f9334d5bd4bb52c
|
||||
options: --tmpfs /tmp:exec
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
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@caa296126883cff596d87d8935842f9db880ef25 # v5
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: |
|
||||
/tmp/osv
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
update-flake-hashes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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
+276
-233
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -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]
|
||||
@@ -296,7 +296,7 @@ default-features = false
|
||||
features = ["env", "toml"]
|
||||
|
||||
[workspace.dependencies.hickory-resolver]
|
||||
version = "0.26.0"
|
||||
version = "0.25.2"
|
||||
default-features = false
|
||||
features = [
|
||||
"serde",
|
||||
@@ -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?
|
||||
@@ -343,8 +343,8 @@ version = "1.1.1"
|
||||
# Used for matrix spec type definitions and helpers
|
||||
[workspace.dependencies.ruma]
|
||||
# version = "0.14.1"
|
||||
git = "https://github.com/ruma/ruma.git"
|
||||
rev = "3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
git = "https://github.com/gingershaped/ruwuma.git"
|
||||
rev = "a0178c4e5e1729d27cf2f1c4dacf77b763987749"
|
||||
features = [
|
||||
"appservice-api-c",
|
||||
"client-api",
|
||||
@@ -356,7 +356,6 @@ features = [
|
||||
"ring-compat",
|
||||
"compat-upload-signatures",
|
||||
"compat-optional-txn-pdus",
|
||||
"compat-get-3pids",
|
||||
"unstable-msc2666",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc2870",
|
||||
@@ -380,6 +379,7 @@ features = [
|
||||
"unstable-msc4406",
|
||||
"unstable-msc4439",
|
||||
"unstable-msc4466",
|
||||
"unstable-msc4484",
|
||||
"unstable-extensible-events",
|
||||
]
|
||||
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
Appservice device management as outlined in MSC4190 (part of Matrix 1.17) is now fully supported. Contributed by @ginger.
|
||||
@@ -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.
|
||||
@@ -1 +0,0 @@
|
||||
Added example configuration using caddy-docker-proxy in the livekit setup section of the docs. Contributed by @Cease
|
||||
@@ -1 +0,0 @@
|
||||
Fixed admin commands being ignored when they had leading whitespace before admin commands. Contributed by @kitvonsnookerz.
|
||||
@@ -1,2 +0,0 @@
|
||||
Improved the performance and reliability of fetching missing events, improving network partition recovery. Contributed
|
||||
by @nex.
|
||||
@@ -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 +0,0 @@
|
||||
Added support for MSC4380 invite blocking, which has become part of the Matrix specification in v1.18. Contributed by @nex.
|
||||
@@ -1 +0,0 @@
|
||||
Added `!admin debug get-state-at` command
|
||||
@@ -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.
|
||||
+2
-15
@@ -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
|
||||
@@ -2014,7 +2001,7 @@
|
||||
# privacy_policy = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
||||
# ```
|
||||
#
|
||||
#documents =
|
||||
#documents = {}
|
||||
|
||||
#[global.oauth]
|
||||
|
||||
|
||||
+1
-1
@@ -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.19.1
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
|
||||
|
||||
# Developer tool versions
|
||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||
ENV BINSTALL_VERSION=1.20.1
|
||||
ENV BINSTALL_VERSION=1.19.1
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -187,75 +187,6 @@ ### 4. Configure your Reverse Proxy
|
||||
```
|
||||
|
||||
</details>
|
||||
<details>
|
||||
<summary>Example docker compose file with caddy-docker-proxy labels</summary>
|
||||
```yaml
|
||||
# This setup assumes all containers share the same bridge network
|
||||
services:
|
||||
lk-jwt-service:
|
||||
image: ghcr.io/element-hq/lk-jwt-service:latest
|
||||
container_name: lk-jwt-service
|
||||
# lk-jwt-service environment config here..
|
||||
labels:
|
||||
caddy: livekit.example.com
|
||||
caddy.@lk-jwt-service.path: "/sfu/get* /healthz* /get_token*"
|
||||
caddy.reverse_proxy: "@lk-jwt-service {{upstreams 8081}}"
|
||||
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:latest
|
||||
container_name: livekit
|
||||
command: --config /etc/livekit.yaml
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
caddy: livekit.example.com
|
||||
caddy.reverse_proxy: "{{upstreams 7880}}"
|
||||
volumes:
|
||||
- ./livekit.yaml:/etc/livekit.yaml:ro
|
||||
ports:
|
||||
- "127.0.0.1:7880:7880/tcp"
|
||||
- "7881:7881/tcp"
|
||||
- "50100-50200:50100-50200/udp"
|
||||
|
||||
|
||||
caddy:
|
||||
image: lucaslorentz/caddy-docker-proxy:ci-alpine
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
environment:
|
||||
- CADDY_INGRESS_NETWORKS=caddy
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./data:/data
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
# If you already configured `[global.well_known]` with Continuwuity,
|
||||
# comment out the *_respond labels and add this line
|
||||
# caddy.reverse_proxy: /.well-known/matrix/* homeserver:8008
|
||||
caddy.1_respond: /.well-known/matrix/server {"m.server":"matrix.example.com:443"}
|
||||
caddy.2_respond: /.well-known/matrix/client {"m.server":{"base_url":"https://matrix.example.com"},"m.homeserver":{"base_url":"https://matrix.example.com"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://livekit.example.com"}]}
|
||||
|
||||
# If you are having problems with continuwuity serving headers uncomment
|
||||
# the header section below.
|
||||
|
||||
# caddy: example.com
|
||||
# caddy.0_header: "*"
|
||||
# caddy.0_header.Access-Control-Allow-Origin: "*"
|
||||
# caddy.0_header.Access-Control-Allow-Methods: "GET, POST, OPTIONS"
|
||||
# caddy.0_header.Access-Control-Allow-Headers: "Authorization"
|
||||
# caddy.0_header.Content-Type: "application/json"
|
||||
|
||||
homeserver:
|
||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||
restart: unless-stopped
|
||||
# add additional environment, volume, and network config here...
|
||||
labels:
|
||||
caddy: matrix.example.com
|
||||
caddy.reverse_proxy: "{{upstreams 8008}}"
|
||||
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
### 6. Start Everything
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+18
-18
@@ -3,11 +3,11 @@
|
||||
"advisory-db": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1781566179,
|
||||
"narHash": "sha256-Tqv8I586fYzWpEW/Smq/JqESFa3DVVzVWsnAMtvhy/I=",
|
||||
"lastModified": 1779575509,
|
||||
"narHash": "sha256-wXKYURZz76ZC5lbuDA1oVQA/MxSB3pSJ1raF1HG0oIc=",
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"rev": "74e084413d979d52d2f93b1d93b1ab7b9ee648f5",
|
||||
"rev": "831c50f4a4304068f125e603add6a8839f08b3eb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -18,11 +18,11 @@
|
||||
},
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1780532242,
|
||||
"narHash": "sha256-D+BsdpxmtUwtqGoY0IXPhHgTlmqgcZKCEo1oMyn7ep0=",
|
||||
"lastModified": 1779130139,
|
||||
"narHash": "sha256-BLrtr42azquO7MdGFU5a7KiMl3YpFlTeIXqy1fT5GlQ=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "59a82a1222dd3b2080b5cc52a1a2e8d5f1b77f37",
|
||||
"rev": "edb38893982a3338972bb4a2ec7ce7c29ba10fd9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -39,11 +39,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781527054,
|
||||
"narHash": "sha256-1fX9ev2Fh5QoKQ41G9dYutjo5j/jywu6tZse5Eb1Ck4=",
|
||||
"lastModified": 1779612045,
|
||||
"narHash": "sha256-+7lfNVnmXJDkiRYHd5NoNwYoyUcc0LcXPaIJqjO7VWM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "8c2e51dffefc040a21975da7abf6f252c8c9b783",
|
||||
"rev": "d7be747f0a65af378de515fc3cee131bf99a008f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -89,11 +89,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1781074563,
|
||||
"narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=",
|
||||
"lastModified": 1779508470,
|
||||
"narHash": "sha256-Ap9KJX+5xHIn3bPIpfNgT6MEXdAECECwo4/rmlQD74M=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca",
|
||||
"rev": "29916453413845e54a65b8a1cf996842300cd299",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -132,11 +132,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1781453968,
|
||||
"narHash": "sha256-+V3nK4pCngbmgyVGXY6Kkrlevp4ocPkJJLf2aqwkDNA=",
|
||||
"lastModified": 1779569060,
|
||||
"narHash": "sha256-NSnk5D+3KEfRdbgPijs33N2RAKSG6A74SwfnynLcouo=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "cc272809a173c2c11d0e479d639c811c1eacf049",
|
||||
"rev": "987ea33645ab1c709b1df6823038abcb2fe8973e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -153,11 +153,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1780220602,
|
||||
"narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
|
||||
"lastModified": 1775636079,
|
||||
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "db947814a175b7ca6ded66e21383d938df01c227",
|
||||
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{ inputs, ... }:
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
pkgs,
|
||||
self',
|
||||
...
|
||||
}:
|
||||
{
|
||||
_module.args.craneLib = (inputs.crane.mkLib pkgs).overrideToolchain (
|
||||
pkgs: self'.packages.stable-toolchain
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
imports = [
|
||||
./rust.nix
|
||||
./crane.nix
|
||||
./packages
|
||||
./devshell.nix
|
||||
./fmt.nix
|
||||
|
||||
+28
-29
@@ -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
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Generated
+147
-196
@@ -125,13 +125,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rsbuild/core": {
|
||||
"version": "2.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.15.tgz",
|
||||
"integrity": "sha512-O8vmMhZu1YImO6jOqt/K/vlJSvkq7UtSq5YM1DIlcEd9LW8Gf6/dkQ1B2KPI6F+hSMFBnTTTumdcIowSLCw97g==",
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.11.tgz",
|
||||
"integrity": "sha512-Mpp/viUSkVdSWJkFipdZxM2nUztrBwSnMm6Q86bPzLHtHnXqQ3VFpSMlA4wWRyySNddP6s6efKiVpx0ZOCf7Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rspack/core": "~2.0.8",
|
||||
"@rspack/core": "~2.0.6",
|
||||
"@swc/helpers": "^0.5.23"
|
||||
},
|
||||
"bin": {
|
||||
@@ -169,28 +169,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.8.tgz",
|
||||
"integrity": "sha512-3uZ+y8aQxq33ty2srMxg2Nu0XuBI6vVrG50rkDaXqwWqOohfgGUSfFuQK7EnSUNy4aFUQlCG6NHialQHJov0wg==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.6.tgz",
|
||||
"integrity": "sha512-z5EO9mPlmYNpHAlRGub0Chr6D+Klgy+tX36n7tCm7VRGRlwTmTU9wSENrYbHcCpFbegtrE0s30rDeTBeOu+JiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"@rspack/binding-darwin-arm64": "2.0.8",
|
||||
"@rspack/binding-darwin-x64": "2.0.8",
|
||||
"@rspack/binding-linux-arm64-gnu": "2.0.8",
|
||||
"@rspack/binding-linux-arm64-musl": "2.0.8",
|
||||
"@rspack/binding-linux-x64-gnu": "2.0.8",
|
||||
"@rspack/binding-linux-x64-musl": "2.0.8",
|
||||
"@rspack/binding-wasm32-wasi": "2.0.8",
|
||||
"@rspack/binding-win32-arm64-msvc": "2.0.8",
|
||||
"@rspack/binding-win32-ia32-msvc": "2.0.8",
|
||||
"@rspack/binding-win32-x64-msvc": "2.0.8"
|
||||
"@rspack/binding-darwin-arm64": "2.0.6",
|
||||
"@rspack/binding-darwin-x64": "2.0.6",
|
||||
"@rspack/binding-linux-arm64-gnu": "2.0.6",
|
||||
"@rspack/binding-linux-arm64-musl": "2.0.6",
|
||||
"@rspack/binding-linux-x64-gnu": "2.0.6",
|
||||
"@rspack/binding-linux-x64-musl": "2.0.6",
|
||||
"@rspack/binding-wasm32-wasi": "2.0.6",
|
||||
"@rspack/binding-win32-arm64-msvc": "2.0.6",
|
||||
"@rspack/binding-win32-ia32-msvc": "2.0.6",
|
||||
"@rspack/binding-win32-x64-msvc": "2.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-darwin-arm64": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.8.tgz",
|
||||
"integrity": "sha512-vCgbgH7B7qom+uID+RCZsTCOYFb9wC4/4+1U6rMfytrXGVJ72eNQs2tbdjOl0lb18CT3N/n+VkWynUiLk84GwA==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.6.tgz",
|
||||
"integrity": "sha512-0giCKiWlBfcM4i2scv1j2k9HlSecO9Ybhaa5wsMUyvcFeKr9HbNHh7C2eDFlC6zaI85IUdY71TXF/g/Tcxr9MA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -202,9 +202,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-darwin-x64": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.8.tgz",
|
||||
"integrity": "sha512-satPm2PD4B7jDTVlVAdvMVdUszwLvWUEnUDzLb77mvVkezKNDZmuhb+e8s+FfKs8hJpNbZ9VAejuA2rr8o985w==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.6.tgz",
|
||||
"integrity": "sha512-/mMo2IpI02aOKMlHbVbZue3TJxFqHGX+ibVTdEO+6bzRSuHs7+R9KM5U3XH2YxcWJy5Sid1X1T1pJAjsXcE3rA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -216,9 +216,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-arm64-gnu": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.8.tgz",
|
||||
"integrity": "sha512-pSI+npPQE/uDtiboqvcOIRJbEV2+B+H1xffmko/gw50la92oTUW60kVULFwsb6L0+GVCzIcwX3yq60GtYIn+Ug==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.6.tgz",
|
||||
"integrity": "sha512-H6ACzeM1KBxYDEF8YAim3501Jb1aCsSG79Gjm1M4pwJ5OJPK2ydiJEa438ugXmh0962eKYMHI2yZY0sQq8txaw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -233,9 +233,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-arm64-musl": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.8.tgz",
|
||||
"integrity": "sha512-igjJ43yxWQ72GZqjDDZSSHax9/Vg+6rLMmOvFglTJUkQpB4Tyvu/YjW+WRjYj2xRw6blOjLxUSJWASvuSqqlvg==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.6.tgz",
|
||||
"integrity": "sha512-QTFmBg0n+L397Wi8CIjbd5pe/hxpHnqCDaG1A7e2NWX8Fj9zulAoKLiKflQa1ELEhAY4Foq88aX75+Ilt2tHcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -250,9 +250,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-x64-gnu": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.8.tgz",
|
||||
"integrity": "sha512-zrkoEOnqj1hOEBO5T2I/2Ts2HSJsYFh1qXwMpK4dMJFGGNWDfNeUa6/LF5uq3VINF3JUl7RL47AgrucoSZJXPA==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.6.tgz",
|
||||
"integrity": "sha512-rerCAz022zf0ewxI+7n3SrqLEaxCL+MXRxKjK5FLUGFa8UkIrivq+VUP/1OB6JLh2Bucebc7Y9WoWHvtk22mLA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -267,9 +267,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-x64-musl": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.8.tgz",
|
||||
"integrity": "sha512-6CtDaGZjNDvJd9TBp7a9zABbrPORO21W96+3ZcGBn0YNUPUk4ARxIxrTTpeJ/1F41QDM8AYIkGDdqEYMqTYBsA==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.6.tgz",
|
||||
"integrity": "sha512-96IgOFXQjX6Wbxd+DCYJFy2r/VMu1OoHifW4Cr3kGTYDKoQOIMLwb0ieu/ILp2dGWFMZo5S8odiByAmNICAOIA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -284,9 +284,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-wasm32-wasi": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.8.tgz",
|
||||
"integrity": "sha512-Yf4SiqTUroT5Ju+te0YAY2xxKOb35tECsO21v7hYyGa705wrgoAK/MmF7enOvs9GR1iZIqgiLD/wxsIxl8GjJw==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.6.tgz",
|
||||
"integrity": "sha512-0aWiF+qmdb0csp1x+MaR2o1pscoquLaEbLTVdKjmoTRs6sguMemtB1ObnVTahAUL73P66WePuNpFAJ81zNdqzQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
@@ -300,9 +300,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-arm64-msvc": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.8.tgz",
|
||||
"integrity": "sha512-8NCuiQsAhXrwRBy57QZoypqrws/zLBkaQVGiB8hksr6v++8hNigNjqpQARLbd0iyMuHsQQ++8+auGk6xlDXmzw==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.6.tgz",
|
||||
"integrity": "sha512-BX638A1MXsjc2E3tUskVh3X/WBIHjLKK+lo395v7MmEL9u2BA6l3F6RyW+YaJOt5aEOOv83iA7iCZsviVZ49Uw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -314,9 +314,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-ia32-msvc": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.8.tgz",
|
||||
"integrity": "sha512-bxiekytbX7V9KFAra+HkwtNWC6pYfHEBBZFpiT0xUs3mCFOmAAFVBsBSQsoCP9AdCEXoMAvNdnrHNw3iov4OZw==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.6.tgz",
|
||||
"integrity": "sha512-DCK/+MlN35uvH7tp4j0hbg8wIs9MHArMIrNZXtiD8xP6DNw2wrXcGC1VaxxR5apyWpqXAfIL/KsXBiWS3ygCvg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -328,9 +328,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-x64-msvc": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.8.tgz",
|
||||
"integrity": "sha512-7zPs8YCe/ZVJTwd+5lpB0CP0tkn2pONf/T1ycmVY76u21Nrwt8mXQGc/2yH2eWP4B7fikYBr3hGr7mpR2fajqQ==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.6.tgz",
|
||||
"integrity": "sha512-TxutgzdEX9BkAU/5liKxdQmggJ23INz7EZDWtzSJO6C2SiSYzTJdyPQDIJi1ddkM5TX/drzH184gAJMVOQefng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -342,13 +342,13 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/core": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.8.tgz",
|
||||
"integrity": "sha512-+NLGJf8gZxihDmMFzjlly3toc2SMjeDmuvz0/Cai9AMdV4F+Pqcnt2BA9V4e3SY2jmhJQtPwgyyLtR1RiJO77g==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.6.tgz",
|
||||
"integrity": "sha512-ronRqH1T2dYdMFVOQbGvDNxYaLugQK8qhNYYtS2DbOvPKQYvdIYWDenL9k/WV+hLoknnPWMn2ME2cKJcK3Po+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rspack/binding": "2.0.8"
|
||||
"@rspack/binding": "2.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
@@ -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,26 +462,26 @@
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.2.0.tgz",
|
||||
"integrity": "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.1.0.tgz",
|
||||
"integrity": "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/primitive": "4.2.0",
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/primitive": "4.1.0",
|
||||
"@shikijs/types": "4.1.0",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4",
|
||||
"hast-util-to-html": "^9.0.5"
|
||||
@@ -491,13 +491,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-javascript": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.2.0.tgz",
|
||||
"integrity": "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.1.0.tgz",
|
||||
"integrity": "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/types": "4.1.0",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"oniguruma-to-es": "^4.3.6"
|
||||
},
|
||||
@@ -506,13 +506,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-oniguruma": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.2.0.tgz",
|
||||
"integrity": "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.1.0.tgz",
|
||||
"integrity": "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/types": "4.1.0",
|
||||
"@shikijs/vscode-textmate": "^10.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -520,26 +520,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/langs": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.2.0.tgz",
|
||||
"integrity": "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.1.0.tgz",
|
||||
"integrity": "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0"
|
||||
"@shikijs/types": "4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/primitive": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.2.0.tgz",
|
||||
"integrity": "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.1.0.tgz",
|
||||
"integrity": "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/types": "4.1.0",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
@@ -548,16 +548,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/rehype": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-4.2.0.tgz",
|
||||
"integrity": "sha512-ST3EWye/dwF1gWskczJNBnwFtDzEQ9ceytXZtyc/GfwR5V0qJrkoSGZO55O3SAKDDsXkTDcsfwd9pVe7ROlAHg==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-4.1.0.tgz",
|
||||
"integrity": "sha512-HQwltCcO2/UiFz44/8whyji4rP1VghLu++MgvQn+lQA8/gvuycGkay8DH8o8VAOvLBDKGOkBEw7cC1Cm33GObQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/types": "4.1.0",
|
||||
"@types/hast": "^3.0.4",
|
||||
"hast-util-to-string": "^3.0.1",
|
||||
"shiki": "4.2.0",
|
||||
"shiki": "4.1.0",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.1.0"
|
||||
},
|
||||
@@ -566,22 +566,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/themes": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.2.0.tgz",
|
||||
"integrity": "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.1.0.tgz",
|
||||
"integrity": "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0"
|
||||
"@shikijs/types": "4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/types": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.2.0.tgz",
|
||||
"integrity": "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.1.0.tgz",
|
||||
"integrity": "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -668,9 +668,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdx": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz",
|
||||
"integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==",
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz",
|
||||
"integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -682,9 +682,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
||||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||
"version": "19.2.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
|
||||
"integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -723,9 +723,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -1821,53 +1821,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-markdown-cjk-friendly": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown-cjk-friendly/-/mdast-util-to-markdown-cjk-friendly-1.0.0.tgz",
|
||||
"integrity": "sha512-BoaAm8mlJ+LAYz0Qs532Y3ciTuQYgBUPZcSFbvC/ZKmEMAKgulw84YvQK1gI34t/vL2euSfuaWlqczkTBgamkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"micromark-extension-cjk-friendly-util": "3.0.1",
|
||||
"micromark-util-symbol": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/mdast": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/mdast": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-markdown-cjk-friendly-gfm-strikethrough": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown-cjk-friendly-gfm-strikethrough/-/mdast-util-to-markdown-cjk-friendly-gfm-strikethrough-1.0.0.tgz",
|
||||
"integrity": "sha512-1ePVfB4P/vz3xSsm6H3D32r6VYGErxclnuLLFK02/2ReF+UdEKm7caulK6Vm0LBIp5gPRtB2Z1OYDznCkX3k2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-gfm-strikethrough": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.1.2",
|
||||
"micromark-extension-cjk-friendly-util": "3.0.1",
|
||||
"micromark-util-symbol": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/mdast": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/mdast": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
|
||||
@@ -2789,9 +2742,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/property-information": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
|
||||
"integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
||||
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -2800,9 +2753,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||
"version": "19.2.6",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
|
||||
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2810,16 +2763,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
|
||||
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
|
||||
"version": "19.2.6",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
|
||||
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.7"
|
||||
"react": "^19.2.6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-lazy-with-preload": {
|
||||
@@ -2869,9 +2822,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
|
||||
"integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==",
|
||||
"version": "7.15.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz",
|
||||
"integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2892,13 +2845,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz",
|
||||
"integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==",
|
||||
"version": "7.15.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz",
|
||||
"integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.17.0"
|
||||
"react-router": "7.15.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -3058,13 +3011,12 @@
|
||||
}
|
||||
},
|
||||
"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.0.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-cjk-friendly/-/remark-cjk-friendly-2.0.1.tgz",
|
||||
"integrity": "sha512-6WwkoQyZf/4j5k53zdFYrR8Ca+UVn992jXdLUSBDZR4eBpFhKyVxmA4gUHra/5fesjGIxrDhHesNr/sVoiiysA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-to-markdown-cjk-friendly": "1.0.0",
|
||||
"micromark-extension-cjk-friendly": "2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3081,13 +3033,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/remark-cjk-friendly-gfm-strikethrough": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.1.0.tgz",
|
||||
"integrity": "sha512-3Kyq2hjY7V7eU8MbVbWW6QQLN81pjJcIvKHvPxr8hZZmcq/9wqm3MJ3iUG34Ch9QTM4WHN+a1JVAVC1fSi5mig==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.0.1.tgz",
|
||||
"integrity": "sha512-pWKj25O2eLXIL1aBupayl1fKhco+Brw8qWUWJPVB9EBzbQNd7nGLj0nLmJpggWsGLR5j5y40PIdjxby9IEYTuA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-to-markdown-cjk-friendly-gfm-strikethrough": "1.0.0",
|
||||
"micromark-extension-cjk-friendly-gfm-strikethrough": "2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3213,18 +3164,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.2.0.tgz",
|
||||
"integrity": "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.1.0.tgz",
|
||||
"integrity": "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/core": "4.2.0",
|
||||
"@shikijs/engine-javascript": "4.2.0",
|
||||
"@shikijs/engine-oniguruma": "4.2.0",
|
||||
"@shikijs/langs": "4.2.0",
|
||||
"@shikijs/themes": "4.2.0",
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/core": "4.1.0",
|
||||
"@shikijs/engine-javascript": "4.1.0",
|
||||
"@shikijs/engine-oniguruma": "4.1.0",
|
||||
"@shikijs/langs": "4.1.0",
|
||||
"@shikijs/themes": "4.1.0",
|
||||
"@shikijs/types": "4.1.0",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
[toolchain]
|
||||
profile = "minimal"
|
||||
channel = "1.96.1"
|
||||
channel = "1.96.0"
|
||||
components = [
|
||||
# For rust-analyzer
|
||||
"rust-src",
|
||||
|
||||
@@ -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(®istration, &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(®istration, &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
-15
@@ -1,23 +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().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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
@@ -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
-1
@@ -26,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};
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
-255
@@ -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,259 +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().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().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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
+1048
-1041
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,8 @@ zstd_compression = [
|
||||
"reqwest/zstd",
|
||||
]
|
||||
|
||||
admin_api = []
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
axum-client-ip.workspace = true
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod rooms;
|
||||
@@ -1,36 +0,0 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::StreamExt;
|
||||
use ruma::OwnedRoomId;
|
||||
use ruminuwuity::admin::continuwuity::rooms;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_continuwuity/admin/rooms/list`
|
||||
///
|
||||
/// Lists all rooms known to this server, excluding banned ones.
|
||||
pub(crate) async fn list_rooms(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<rooms::list::v1::Request>,
|
||||
) -> Result<rooms::list::v1::Response> {
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
if !services.users.is_admin(sender_user).await {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
let mut rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.metadata
|
||||
.iter_ids()
|
||||
.filter_map(|room_id| async move {
|
||||
if !services.rooms.metadata.is_banned(&room_id).await {
|
||||
Some(room_id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
rooms.sort();
|
||||
Ok(rooms::list::v1::Response::new(rooms))
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod ban;
|
||||
pub mod list;
|
||||
@@ -263,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."
|
||||
)));
|
||||
|
||||
@@ -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(&user_id, None)?;
|
||||
services.users.create(&user_id, None).await?;
|
||||
|
||||
user_id
|
||||
} else {
|
||||
@@ -99,7 +99,7 @@ pub(crate) async fn register_route(
|
||||
.users
|
||||
.create_local_account(&user_id, password, identity.email)
|
||||
.await;
|
||||
|
||||
services.users.join_auto_join_rooms(&user_id).await;
|
||||
user_id
|
||||
};
|
||||
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::future::{join, join3};
|
||||
use conduwuit::Err;
|
||||
use ruma::api::client::admin::{is_user_locked, lock_user};
|
||||
|
||||
use crate::Ruma;
|
||||
use crate::router::Ruma;
|
||||
|
||||
/// # `GET /_matrix/client/v1/admin/lock/{userId}`
|
||||
///
|
||||
/// Check the account lock status of a target user
|
||||
pub(crate) async fn get_lock_status(
|
||||
pub(crate) async fn get_locked_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<is_user_locked::v1::Request>,
|
||||
) -> Result<is_user_locked::v1::Response> {
|
||||
let (admin, active) = join(
|
||||
services.users.is_admin(body.identity.expect_sender_user()?),
|
||||
services.users.is_active(&body.user_id),
|
||||
)
|
||||
.await;
|
||||
if !admin {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
if !services.globals.user_is_local(&body.user_id) {
|
||||
return Err!(Request(InvalidParam("Can only check the lock status of local users")));
|
||||
}
|
||||
if !active {
|
||||
return Err!(Request(NotFound("Unknown user")));
|
||||
) -> conduwuit::Result<is_user_locked::v1::Response> {
|
||||
if !services.users.is_active_local(&body.user_id).await {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Can only check the lock status of active local users"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(is_user_locked::v1::Response::new(
|
||||
services.users.is_locked(&body.user_id).await?,
|
||||
))
|
||||
@@ -34,34 +25,24 @@ pub(crate) async fn get_lock_status(
|
||||
/// # `PUT /_matrix/client/v1/admin/lock/{userId}`
|
||||
///
|
||||
/// Set the account lock status of a target user
|
||||
pub(crate) async fn put_lock_status(
|
||||
pub(crate) async fn put_locked_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()?;
|
||||
) -> conduwuit::Result<lock_user::v1::Response> {
|
||||
if !services.users.is_active_local(&body.user_id).await {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Can only set the locked status of active local users"
|
||||
)));
|
||||
}
|
||||
|
||||
let (sender_admin, active, target_admin) = join3(
|
||||
services.users.is_admin(sender_user),
|
||||
services.users.is_active(&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")));
|
||||
}
|
||||
if !services.globals.user_is_local(&body.user_id) {
|
||||
return Err!(Request(InvalidParam("Can only set the lock status of local users")));
|
||||
}
|
||||
if !active {
|
||||
return Err!(Request(NotFound("Unknown user")));
|
||||
}
|
||||
if body.user_id == *sender_user {
|
||||
if body.identity.sender_user() == Some(&body.user_id) {
|
||||
return Err!(Request(Forbidden("You cannot lock yourself")));
|
||||
}
|
||||
if target_admin {
|
||||
|
||||
if services.users.is_admin(&body.user_id).await {
|
||||
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));
|
||||
@@ -70,19 +51,19 @@ pub(crate) async fn put_lock_status(
|
||||
let action = if body.locked {
|
||||
services
|
||||
.users
|
||||
.suspend_account(&body.user_id, sender_user)
|
||||
.lock_account(&body.user_id, body.identity.sender_user())
|
||||
.await;
|
||||
"locked"
|
||||
"suspended"
|
||||
} else {
|
||||
services.users.unsuspend_account(&body.user_id).await;
|
||||
"unlocked"
|
||||
services.users.unlock_account(&body.user_id).await;
|
||||
"unsuspended"
|
||||
};
|
||||
|
||||
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))
|
||||
.send_text(&format!("{} has been {} by {}.", body.user_id, action, body.identity))
|
||||
.await;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod lock;
|
||||
pub(crate) mod site;
|
||||
mod suspend;
|
||||
|
||||
pub(crate) use self::{lock::*, suspend::*};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod rooms;
|
||||
pub(crate) mod users;
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
use crate::{Ruma, client::leave_room};
|
||||
|
||||
/// # `PUT /_continuwuity/admin/rooms/{roomID}/ban`
|
||||
/// # `PUT /_continuwuity/admin/v1/rooms/{roomID}/ban`
|
||||
///
|
||||
/// Bans or unbans a room.
|
||||
pub(crate) async fn ban_room(
|
||||
@@ -0,0 +1,178 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Event, Result,
|
||||
utils::stream::{BroadbandExt, WidebandExt},
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
OwnedRoomId,
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
create::RoomCreateEventContent,
|
||||
encryption::PossiblyRedactedRoomEncryptionEventContent,
|
||||
tombstone::PossiblyRedactedRoomTombstoneEventContent,
|
||||
},
|
||||
},
|
||||
};
|
||||
use ruminuwuity::admin::continuwuity::rooms;
|
||||
use tokio::join;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_continuwuity/admin/rooms`
|
||||
///
|
||||
/// Lists all room IDs known to this server, excluding banned ones.
|
||||
///
|
||||
/// This is the legacy version of the endpoint, which does not support
|
||||
/// pagination or including banned rooms. It is recommended to use the
|
||||
/// `/v1/rooms` endpoint instead. This endpoint may be removed in a future
|
||||
/// release.
|
||||
pub(crate) async fn legacy_list_rooms_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<rooms::list::unstable::Request>,
|
||||
) -> Result<rooms::list::unstable::Response> {
|
||||
let mut rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.metadata
|
||||
.iter_ids()
|
||||
.filter_map(|room_id| async move {
|
||||
if !services.rooms.metadata.is_banned(&room_id).await {
|
||||
Some(room_id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
rooms.sort();
|
||||
Ok(rooms::list::unstable::Response::new(rooms))
|
||||
}
|
||||
|
||||
/// # `GET /_continuwuity/admin/v1/rooms`
|
||||
///
|
||||
/// Lists rooms known to this server.
|
||||
pub(crate) async fn list_rooms_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<rooms::list::v1::Request>,
|
||||
) -> Result<rooms::list::v1::Response> {
|
||||
let include_banned_rooms = body.include_banned_rooms;
|
||||
let rooms = services
|
||||
.rooms
|
||||
.metadata
|
||||
.iter_ids()
|
||||
.wide_filter_map(|room_id| async move {
|
||||
if include_banned_rooms || !services.rooms.metadata.is_banned(&room_id).await {
|
||||
Some(room_id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.skip(body.offset.unwrap_or_default())
|
||||
.take(body.limit.unwrap_or(100).min(100))
|
||||
.broad_filter_map(|room_id| async move {
|
||||
let (
|
||||
banned,
|
||||
disabled,
|
||||
member_count,
|
||||
local_member_count,
|
||||
resident_server_count,
|
||||
published,
|
||||
create_event,
|
||||
encryption_event,
|
||||
name_event,
|
||||
topic_event,
|
||||
canonical_alias_event,
|
||||
join_rules_event,
|
||||
history_visibility_event,
|
||||
tombstone_event,
|
||||
) = join!(
|
||||
services.rooms.metadata.is_banned(&room_id),
|
||||
services.rooms.metadata.is_disabled(&room_id),
|
||||
services.rooms.state_cache.room_joined_count(&room_id),
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.active_local_users_in_room(&room_id)
|
||||
.count(),
|
||||
services.rooms.state_cache.room_servers(&room_id).count(),
|
||||
services.rooms.directory.is_public_room(&room_id),
|
||||
services.rooms.state_accessor.room_state_get(
|
||||
&room_id,
|
||||
&StateEventType::RoomCreate,
|
||||
""
|
||||
),
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<PossiblyRedactedRoomEncryptionEventContent>(
|
||||
&room_id,
|
||||
&StateEventType::RoomEncryption,
|
||||
""
|
||||
),
|
||||
services.rooms.state_accessor.room_state_get_content(
|
||||
&room_id,
|
||||
&StateEventType::RoomName,
|
||||
""
|
||||
),
|
||||
services.rooms.state_accessor.room_state_get_content(
|
||||
&room_id,
|
||||
&StateEventType::RoomTopic,
|
||||
""
|
||||
),
|
||||
services.rooms.state_accessor.room_state_get_content(
|
||||
&room_id,
|
||||
&StateEventType::RoomCanonicalAlias,
|
||||
""
|
||||
),
|
||||
services.rooms.state_accessor.room_state_get_content(
|
||||
&room_id,
|
||||
&StateEventType::RoomJoinRules,
|
||||
""
|
||||
),
|
||||
services.rooms.state_accessor.room_state_get_content(
|
||||
&room_id,
|
||||
&StateEventType::RoomHistoryVisibility,
|
||||
""
|
||||
),
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<PossiblyRedactedRoomTombstoneEventContent>(
|
||||
&room_id,
|
||||
&StateEventType::RoomTombstone,
|
||||
""
|
||||
),
|
||||
);
|
||||
let Ok(create_event) = create_event else {
|
||||
return None;
|
||||
};
|
||||
let create_content = create_event
|
||||
.get_content::<RoomCreateEventContent>()
|
||||
.expect("m.room.create content must be valid");
|
||||
Some(rooms::list::v1::MinimalRoomInfo {
|
||||
room_id,
|
||||
banned,
|
||||
disabled,
|
||||
member_count: usize::try_from(member_count.unwrap_or_default())
|
||||
.expect("u64 should fit in usize"),
|
||||
local_member_count,
|
||||
resident_server_count,
|
||||
creators: vec![create_event.sender],
|
||||
encrypted: encryption_event.is_ok_and(|c| c.algorithm.is_some()),
|
||||
federated: create_content.federate,
|
||||
published,
|
||||
version: create_content.room_version,
|
||||
name: name_event.unwrap_or(None),
|
||||
topic: topic_event.unwrap_or(None),
|
||||
canonical_alias: canonical_alias_event.unwrap_or(None),
|
||||
join_rules: join_rules_event.unwrap_or(None),
|
||||
history_visibility: history_visibility_event.unwrap_or(None),
|
||||
predecessor: create_content.predecessor.map(|c| c.room_id),
|
||||
successor: tombstone_event.map_or(None, |c| c.replacement_room),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
Ok(rooms::list::v1::Response::new(rooms))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod ban;
|
||||
mod list;
|
||||
|
||||
pub(crate) use ban::ban_room;
|
||||
pub(crate) use list::*;
|
||||
@@ -0,0 +1,119 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, err, error, info,
|
||||
utils::{IterStream, stream::BroadbandExt},
|
||||
warn,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::UserId;
|
||||
use ruminuwuity::admin::continuwuity::users;
|
||||
use service::users::HashedPassword;
|
||||
|
||||
use crate::router::Ruma;
|
||||
|
||||
/// # `POST /_continuwuity/admin/v1/users/create`
|
||||
///
|
||||
/// Creates a new user.
|
||||
pub(crate) async fn create_user_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<users::create::v1::Request>,
|
||||
) -> conduwuit::Result<users::create::v1::Response> {
|
||||
let email = body
|
||||
.email
|
||||
.clone()
|
||||
.map(lettre::Address::try_from)
|
||||
.transpose()
|
||||
.map_err(|e| err!(Request(BadJson("Invalid email address: {e}"))))?;
|
||||
|
||||
let ref user_id = services
|
||||
.users
|
||||
.determine_registration_user_id(Some(body.localpart.clone()), email.as_ref(), None)
|
||||
.await?;
|
||||
|
||||
services
|
||||
.users
|
||||
.create_local_account(user_id, HashedPassword::new(&body.password)?, email)
|
||||
.await;
|
||||
|
||||
if body.suspended {
|
||||
services
|
||||
.users
|
||||
.suspend_account(&user_id, body.identity.sender_user())
|
||||
.await;
|
||||
}
|
||||
if body.locked {
|
||||
services
|
||||
.users
|
||||
.lock_account(user_id, body.identity.sender_user())
|
||||
.await;
|
||||
}
|
||||
if body.login_disabled {
|
||||
services.users.disable_login(user_id);
|
||||
}
|
||||
if let Some(ref value) = body.display_name {
|
||||
services.users.set_profile_key(
|
||||
user_id,
|
||||
"displayname",
|
||||
Some(serde_json::to_value(value)?),
|
||||
);
|
||||
}
|
||||
if let Some(ref value) = body.avatar_url {
|
||||
services
|
||||
.users
|
||||
.set_profile_key(user_id, "avatar_url", Some(serde_json::to_value(value)?));
|
||||
}
|
||||
if body.admin {
|
||||
services
|
||||
.admin
|
||||
.make_user_admin(user_id)
|
||||
.await
|
||||
.inspect_err(|e| error!("failed to make new user {user_id} an admin: {e}"))
|
||||
.ok();
|
||||
}
|
||||
if !body.skip_auto_join {
|
||||
services.users.join_auto_join_rooms(user_id).await;
|
||||
}
|
||||
|
||||
body.auto_join_rooms
|
||||
.clone()
|
||||
.into_iter()
|
||||
.stream()
|
||||
.broad_filter_map(|room| async move {
|
||||
services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_with_servers(&room, None)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||
{room}: {e}"
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.for_each_concurrent(None, |(room_id, servers)| async move {
|
||||
match services
|
||||
.rooms
|
||||
.membership
|
||||
.join_room(
|
||||
user_id,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
servers.as_ref(),
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Err(e) => {
|
||||
warn!("Failed to automatically join {user_id} to {room_id}: {e}");
|
||||
},
|
||||
| _ => {
|
||||
info!("Automatically joined room {user_id} to {room_id}");
|
||||
},
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(users::create::v1::Response::new(user_id.to_owned()))
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::utils::stream::WidebandExt;
|
||||
use futures::StreamExt;
|
||||
use ruminuwuity::admin::continuwuity::users;
|
||||
use tokio::join;
|
||||
|
||||
use crate::router::Ruma;
|
||||
|
||||
/// # `GET /_continuwuity/admin/v1/users`
|
||||
///
|
||||
/// Lists all users on this homeserver.
|
||||
pub(crate) async fn list_users_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<users::list::v1::Request>,
|
||||
) -> conduwuit::Result<users::list::v1::Response> {
|
||||
let users = services
|
||||
.users
|
||||
.list_local_users()
|
||||
.skip(body.offset.unwrap_or_default())
|
||||
.take(body.limit.unwrap_or(100).min(100))
|
||||
.wide_filter_map(|user_id| async move {
|
||||
let (deactivated, suspended, locked, admin, login_disabled) = join!(
|
||||
services.users.is_deactivated(&user_id),
|
||||
services.users.is_suspended(&user_id),
|
||||
services.users.is_locked(&user_id),
|
||||
services.users.is_admin(&user_id),
|
||||
services.users.is_login_disabled(&user_id),
|
||||
);
|
||||
Some(users::list::v1::User {
|
||||
user_id: user_id.clone(),
|
||||
deactivated: deactivated.unwrap_or_default(),
|
||||
suspended: suspended.unwrap_or_default(),
|
||||
locked: locked.unwrap_or_default(),
|
||||
admin,
|
||||
login_disabled,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(users::list::v1::Response::new(users))
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod create;
|
||||
mod list;
|
||||
|
||||
pub(crate) use create::*;
|
||||
pub(crate) use list::*;
|
||||
@@ -70,7 +70,7 @@ pub(crate) async fn put_suspended_status(
|
||||
let action = if body.suspended {
|
||||
services
|
||||
.users
|
||||
.suspend_account(&body.user_id, sender_user)
|
||||
.suspend_account(&body.user_id, body.identity.sender_user())
|
||||
.await;
|
||||
"suspended"
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -39,15 +40,21 @@ 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}))?;
|
||||
}
|
||||
|
||||
Ok(get_capabilities::v3::Response::new(capabilities))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,12 @@ pub(crate) async fn get_profile_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_profile::v3::Request>,
|
||||
) -> Result<get_profile::v3::Response> {
|
||||
if services.config.require_auth_for_profile_requests && body.identity.is_none() {
|
||||
return Err!(Request(Unauthorized(
|
||||
"This server requires authentication to view user profiles."
|
||||
)));
|
||||
}
|
||||
|
||||
let Some(profile) = fetch_full_profile(&services, &body.user_id).await else {
|
||||
return Err!(Request(NotFound("This user's profile could not be fetched.")));
|
||||
};
|
||||
@@ -42,6 +48,12 @@ pub(crate) async fn get_profile_field_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_profile_field::v3::Request>,
|
||||
) -> Result<get_profile_field::v3::Response> {
|
||||
if services.config.require_auth_for_profile_requests && body.identity.is_none() {
|
||||
return Err!(Request(Unauthorized(
|
||||
"This server requires authentication to view user profiles."
|
||||
)));
|
||||
}
|
||||
|
||||
let value = fetch_profile_field(&services, &body.user_id, body.field.clone()).await?;
|
||||
|
||||
Ok(assign!(get_profile_field::v3::Response::default(), { value }))
|
||||
|
||||
@@ -92,14 +92,6 @@ pub async fn handle_login(
|
||||
return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
|
||||
}
|
||||
|
||||
if services.users.is_deactivated(&user_id).await? {
|
||||
return Err!(Request(UserDeactivated("This account has been deactivated.")));
|
||||
}
|
||||
|
||||
if services.users.is_locked(&user_id).await? {
|
||||
return Err!(Request(UserLocked("This account has been locked.")));
|
||||
}
|
||||
|
||||
if services.users.is_login_disabled(&user_id).await {
|
||||
warn!(%user_id, "user attempted to log in with a login-disabled account");
|
||||
return Err!(Request(Forbidden("This account is not permitted to log in.")));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
pub mod router;
|
||||
pub mod server;
|
||||
|
||||
pub mod admin;
|
||||
|
||||
pub(crate) use self::router::{Ruma, RumaResponse, State};
|
||||
|
||||
conduwuit::mod_ctor! {}
|
||||
|
||||
+16
-6
@@ -16,7 +16,9 @@
|
||||
|
||||
use self::handler::RouterExt;
|
||||
pub(super) use self::{args::Args as Ruma, auth::ClientIdentity, response::RumaResponse};
|
||||
use crate::{admin, client, server};
|
||||
#[cfg(feature = "admin_api")]
|
||||
use crate::client::admin::site as admin_api;
|
||||
use crate::{client, server};
|
||||
|
||||
pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
let config = &state.server.config;
|
||||
@@ -181,8 +183,8 @@ 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::get_locked_status)
|
||||
.ruma_route(&client::put_locked_status)
|
||||
.ruma_route(&client::well_known_support)
|
||||
.ruma_route(&client::well_known_client)
|
||||
.ruma_route(&client::well_known_policy_server)
|
||||
@@ -191,9 +193,7 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
.ruma_route(&client::get_authorization_server_metadata_route)
|
||||
.merge(client::oauth::router(state))
|
||||
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
||||
.ruma_route(&admin::rooms::ban::ban_room)
|
||||
.ruma_route(&admin::rooms::list::list_rooms);
|
||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version));
|
||||
|
||||
if config.allow_federation {
|
||||
router = router
|
||||
@@ -278,6 +278,16 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
.route("/_matrix/media/r0/preview_url", any(redirect_legacy_preview));
|
||||
}
|
||||
|
||||
#[cfg(feature = "admin_api")]
|
||||
{
|
||||
router = router
|
||||
.ruma_route(&admin_api::users::list_users_route)
|
||||
.ruma_route(&admin_api::users::create_user_route)
|
||||
.ruma_route(&admin_api::rooms::ban_room)
|
||||
.ruma_route(&admin_api::rooms::legacy_list_rooms_route)
|
||||
.ruma_route(&admin_api::rooms::list_rooms_route);
|
||||
};
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
|
||||
+184
-151
@@ -1,11 +1,14 @@
|
||||
use std::any::{Any, TypeId};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
fmt::Display,
|
||||
};
|
||||
|
||||
use conduwuit::{Err, Error, Result, err};
|
||||
use http::StatusCode;
|
||||
use ruma::{
|
||||
DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
|
||||
api::{
|
||||
IncomingRequest,
|
||||
IncomingRequest, OAuthScope,
|
||||
auth_scheme::{
|
||||
AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
|
||||
AuthScheme, NoAccessToken, NoAuthentication,
|
||||
@@ -76,68 +79,66 @@ pub(crate) fn appservice_info(&self) -> Option<&RegistrationInfo> {
|
||||
pub(crate) fn is_appservice(&self) -> bool { matches!(self, Self::Appservice { .. }) }
|
||||
}
|
||||
|
||||
impl Display for ClientIdentity {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
| Self::User { sender_user, sender_device } =>
|
||||
write!(f, "{sender_user} ({sender_device})"),
|
||||
| Self::Appservice { sender_user, appservice_info, .. } =>
|
||||
write!(f, "appservice `{}` using {sender_user}", appservice_info.registration.id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait CheckAuth: AuthScheme {
|
||||
type Identity: Send;
|
||||
|
||||
fn authenticate<R: IncomingRequest + Any, B: AsRef<[u8]> + Sync>(
|
||||
fn authenticate<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
incoming_request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
) -> impl Future<Output = Result<Self::Identity>> + Send {
|
||||
async move {
|
||||
let route = TypeId::of::<R>();
|
||||
|
||||
let output = Self::extract_authentication(incoming_request).map_err(|err| {
|
||||
err!(Request(Unauthorized(warn!(
|
||||
"Failed to extract authorization: {}",
|
||||
"Failed to extract request authentication: {}",
|
||||
err.into()
|
||||
))))
|
||||
})?;
|
||||
|
||||
Self::verify(services, output, incoming_request, query, route).await
|
||||
Self::verify::<R, B>(services, output, incoming_request, query).await
|
||||
}
|
||||
}
|
||||
|
||||
fn verify<B: AsRef<[u8]> + Sync>(
|
||||
fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> impl Future<Output = Result<Self::Identity>> + Send;
|
||||
}
|
||||
|
||||
impl CheckAuth for ServerSignatures {
|
||||
type Identity = OwnedServerName;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
_query: AuthQueryParams,
|
||||
_route: TypeId,
|
||||
) -> Result<Self::Identity> {
|
||||
let destination = services.globals.server_name();
|
||||
if output
|
||||
.destination
|
||||
.as_ref()
|
||||
.is_some_and(|supplied_destination| supplied_destination != destination)
|
||||
{
|
||||
return Err!(Request(Unauthorized("Destination mismatch.")));
|
||||
}
|
||||
|
||||
let key = services
|
||||
.server_keys
|
||||
.get_verify_key(&output.origin, &output.key)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
err!(Request(Unauthorized(warn!("Failed to fetch signing keys: {e}"))))
|
||||
.map_err(|err| {
|
||||
err!(Request(Unauthorized(warn!("Failed to fetch signing keys: {err}"))))
|
||||
})?;
|
||||
|
||||
let keys: PubKeys = [(output.key.to_string(), key.key)].into();
|
||||
let keys: PubKeyMap = [(output.origin.as_str().into(), keys)].into();
|
||||
|
||||
match output.verify_request(request, destination, &keys) {
|
||||
match output.verify_request(request, services.globals.server_name(), &keys) {
|
||||
| Ok(()) => {
|
||||
if services
|
||||
.moderation
|
||||
@@ -159,115 +160,36 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
impl CheckAuth for AccessToken {
|
||||
type Identity = ClientIdentity;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
_request: &hyper::Request<B>,
|
||||
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
|
||||
{
|
||||
// If the token is expired we return a soft logout
|
||||
if matches!(status, AccessTokenStatus::Expired) {
|
||||
return Err(Error::Request(
|
||||
ErrorKind::UnknownToken(
|
||||
assign!(UnknownTokenErrorData::new(), { soft_logout: true }),
|
||||
),
|
||||
"This token has expired".into(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
));
|
||||
}
|
||||
|
||||
// Locked users can only use /logout and /logout/all
|
||||
if services
|
||||
.users
|
||||
.is_locked(&sender_user)
|
||||
.await
|
||||
.is_ok_and(std::convert::identity)
|
||||
{
|
||||
if !(route == TypeId::of::<client::session::logout::v3::Request>()
|
||||
|| route == TypeId::of::<client::session::logout_all::v3::Request>())
|
||||
{
|
||||
return Err!(Request(UserLocked("Your account is locked.")));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ClientIdentity::User { sender_user, sender_device })
|
||||
} else if let Ok(appservice_info) = services.appservice.find_from_token(&output).await {
|
||||
let Ok(sender_user) = query.user_id.clone().map_or_else(
|
||||
|| {
|
||||
UserId::parse_with_server_name(
|
||||
appservice_info.registration.sender_localpart.as_str(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
},
|
||||
UserId::parse,
|
||||
) else {
|
||||
return Err!(Request(InvalidUsername("Username is invalid.")));
|
||||
};
|
||||
|
||||
if !appservice_info.is_user_match(&sender_user) {
|
||||
return Err!(Request(Exclusive("User is not in namespace.")));
|
||||
}
|
||||
|
||||
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
|
||||
// The device_id can be provided via `device_id` or
|
||||
// `org.matrix.msc3202.device_id` query parameter.
|
||||
let sender_device =
|
||||
if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
|
||||
// Verify the device exists for this user
|
||||
if services
|
||||
.users
|
||||
.get_device_metadata(&sender_user, device_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"Device does not exist for user or appservice cannot masquerade as \
|
||||
this device."
|
||||
)));
|
||||
}
|
||||
|
||||
Some(device_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ClientIdentity::Appservice {
|
||||
sender_user,
|
||||
sender_device,
|
||||
appservice_info: Box::new(appservice_info),
|
||||
})
|
||||
} else {
|
||||
Err(Error::Request(
|
||||
ErrorKind::UnknownToken(UnknownTokenErrorData::new()),
|
||||
"Invalid token".into(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
))
|
||||
}
|
||||
verify_access_token(services, output, query, TypeId::of::<R>(), R::required_scopes())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckAuth for AccessTokenOptional {
|
||||
type Identity = Option<ClientIdentity>;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
_request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> Result<Self::Identity> {
|
||||
match output {
|
||||
| Some(token) =>
|
||||
<AccessToken as CheckAuth>::verify(services, token, request, query, route)
|
||||
.await
|
||||
.map(Some),
|
||||
| Some(token) => verify_access_token(
|
||||
services,
|
||||
token,
|
||||
query,
|
||||
TypeId::of::<R>(),
|
||||
R::required_scopes(),
|
||||
)
|
||||
.await
|
||||
.map(Some),
|
||||
| None => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -276,39 +198,29 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
impl CheckAuth for AppserviceToken {
|
||||
type Identity = RegistrationInfo;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
_request: &hyper::Request<B>,
|
||||
_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.")));
|
||||
};
|
||||
|
||||
Ok(appservice_info)
|
||||
verify_appservice_access_token(services, output).await
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckAuth for AppserviceTokenOptional {
|
||||
type Identity = Option<RegistrationInfo>;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
_request: &hyper::Request<B>,
|
||||
_query: AuthQueryParams,
|
||||
) -> Result<Self::Identity> {
|
||||
match output {
|
||||
| Some(token) =>
|
||||
<AppserviceToken as CheckAuth>::verify(services, token, request, query, route)
|
||||
.await
|
||||
.map(Some),
|
||||
| Some(token) => verify_appservice_access_token(services, token)
|
||||
.await
|
||||
.map(Some),
|
||||
| None => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -317,12 +229,11 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
impl CheckAuth for NoAuthentication {
|
||||
type Identity = ();
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
_services: &Services,
|
||||
_output: Self::Output,
|
||||
_request: &hyper::Request<B>,
|
||||
_query: AuthQueryParams,
|
||||
_route: TypeId,
|
||||
) -> Result<Self::Identity> {
|
||||
Ok(())
|
||||
}
|
||||
@@ -331,31 +242,153 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
impl CheckAuth for NoAccessToken {
|
||||
type Identity = Option<ClientIdentity>;
|
||||
|
||||
async fn verify<B: AsRef<[u8]> + Sync>(
|
||||
async fn verify<R: IncomingRequest<Authentication = Self> + Any, B: AsRef<[u8]> + Sync>(
|
||||
services: &Services,
|
||||
_output: Self::Output,
|
||||
request: &hyper::Request<B>,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
) -> Result<Self::Identity> {
|
||||
// We handle these the same as AccessTokenOptional
|
||||
let token = AccessTokenOptional::extract_authentication(request).map_err(|err| {
|
||||
err!(Request(Unauthorized(warn!("Failed to extract authorization: {}", err))))
|
||||
})?;
|
||||
|
||||
// Check special access restrictions
|
||||
if (route == TypeId::of::<client::profile::get_avatar_url::v3::Request>()
|
||||
|| route == TypeId::of::<client::profile::get_display_name::v3::Request>()
|
||||
|| route == TypeId::of::<client::profile::get_profile_field::v3::Request>()
|
||||
|| route == TypeId::of::<client::profile::get_profile::v3::Request>())
|
||||
&& services.config.require_auth_for_profile_requests
|
||||
&& token.is_none()
|
||||
{
|
||||
return Err!(Request(Unauthorized(
|
||||
"This server requires authentication to access user profiles."
|
||||
)));
|
||||
match token {
|
||||
| Some(token) => verify_access_token(
|
||||
services,
|
||||
token,
|
||||
query,
|
||||
TypeId::of::<R>(),
|
||||
// Assume that no scopes are required for these endpoints since
|
||||
// ostensibly they don't require authentication
|
||||
&[],
|
||||
)
|
||||
.await
|
||||
.map(Some),
|
||||
| None => Ok(None),
|
||||
}
|
||||
|
||||
<AccessTokenOptional as CheckAuth>::verify(services, token, request, query, route).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_access_token(
|
||||
services: &Services,
|
||||
output: String,
|
||||
query: AuthQueryParams,
|
||||
route: TypeId,
|
||||
required_scopes: &[OAuthScope],
|
||||
) -> Result<ClientIdentity> {
|
||||
if let Some((sender_user, sender_device, status)) =
|
||||
services.users.find_from_token(&output).await
|
||||
{
|
||||
// If the token is expired we return a soft logout
|
||||
if matches!(status, AccessTokenStatus::Expired) {
|
||||
return Err(Error::Request(
|
||||
ErrorKind::UnknownToken(
|
||||
assign!(UnknownTokenErrorData::new(), { soft_logout: true }),
|
||||
),
|
||||
"This access token has expired.".into(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
));
|
||||
}
|
||||
|
||||
// Locked users can only use /logout and /logout/all
|
||||
if services
|
||||
.users
|
||||
.is_locked(&sender_user)
|
||||
.await
|
||||
.is_ok_and(std::convert::identity)
|
||||
{
|
||||
if !(route == TypeId::of::<client::session::logout::v3::Request>()
|
||||
|| route == TypeId::of::<client::session::logout_all::v3::Request>())
|
||||
{
|
||||
return Err!(Request(UserLocked("Your account is locked.")));
|
||||
}
|
||||
}
|
||||
|
||||
// If this device is bound to an OAuth session, check its scopes. This will also
|
||||
// handle admin-only endpoints for OAuth clients.
|
||||
if let Some(session) = services
|
||||
.oauth
|
||||
.get_session_info_for_device(&sender_user, &sender_device)
|
||||
.await
|
||||
{
|
||||
if required_scopes
|
||||
.iter()
|
||||
.all(|scope| !session.scopes.contains(scope))
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"You don't have the necessary scopes to use this endpoint."
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
// Otherwise, explicitly check if the endpoint is restricted to admins only.
|
||||
if required_scopes.contains(&OAuthScope::ServerAdministration)
|
||||
&& !services.users.is_admin(&sender_user).await
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"Only server administrators can use this endpoint."
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ClientIdentity::User { sender_user, sender_device })
|
||||
} else if let Ok(appservice_info) = services.appservice.find_from_token(&output).await {
|
||||
let Ok(sender_user) = query.user_id.clone().map_or_else(
|
||||
|| {
|
||||
UserId::parse_with_server_name(
|
||||
appservice_info.registration.sender_localpart.as_str(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
},
|
||||
UserId::parse,
|
||||
) else {
|
||||
return Err!(Request(InvalidUsername("Username is invalid.")));
|
||||
};
|
||||
|
||||
if !appservice_info.is_user_match(&sender_user) {
|
||||
return Err!(Request(Exclusive("User is not in this appservice's namespace.")));
|
||||
}
|
||||
|
||||
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
|
||||
// The device_id can be provided via `device_id` or
|
||||
// `org.matrix.msc3202.device_id` query parameter.
|
||||
let sender_device = if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
|
||||
// Verify the device exists for this user
|
||||
if services
|
||||
.users
|
||||
.get_device_metadata(&sender_user, device_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err!(Request(Forbidden("Appservice cannot masquerade as this device.")));
|
||||
}
|
||||
|
||||
Some(device_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ClientIdentity::Appservice {
|
||||
sender_user,
|
||||
sender_device,
|
||||
appservice_info: Box::new(appservice_info),
|
||||
})
|
||||
} else {
|
||||
Err(Error::Request(
|
||||
ErrorKind::UnknownToken(UnknownTokenErrorData::new()),
|
||||
"Invalid access token.".into(),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_appservice_access_token(
|
||||
services: &Services,
|
||||
output: String,
|
||||
) -> Result<RegistrationInfo> {
|
||||
let Ok(appservice_info) = services.appservice.find_from_token(&output).await else {
|
||||
return Err!(Request(Unauthorized("Invalid appservice token.")));
|
||||
};
|
||||
|
||||
Ok(appservice_info)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,7 +44,7 @@ pub(crate) async fn get_event_route(
|
||||
room_id,
|
||||
event_id: Some(&body.event_id),
|
||||
}
|
||||
.assert()
|
||||
.check()
|
||||
.await?;
|
||||
|
||||
if !services
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
+84
-32
@@ -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,13 +676,6 @@ async fn handle_edu_direct_to_device(
|
||||
messages
|
||||
.into_iter()
|
||||
.stream()
|
||||
.broad_filter_map(|(target_user_id, map)| async move {
|
||||
services
|
||||
.users
|
||||
.is_active_local(&target_user_id)
|
||||
.await
|
||||
.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)
|
||||
})
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user