mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Compare commits
337 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fe1ce521aa | |||
| ad0c5ceda4 | |||
| 68afdb22c7 | |||
| 1d02851028 | |||
| 59d5e3ebf1 | |||
| c2d97aaa5e | |||
| 513236b3ce | |||
| 9db0325b42 | |||
| e0494c1538 | |||
| 784ccd6bad | |||
| 48703173bc | |||
| c01b049910 | |||
| 9d9f403ad5 | |||
| 3109c0daba | |||
| ef9b1c6303 | |||
| b7df0a14c6 | |||
| b5006a4c41 | |||
| 320b0680bd | |||
| ed8c21ac9a | |||
| 9a9c071e82 | |||
| 89a158ab0b | |||
| 7d6710c033 | |||
| 61d9ac66fa | |||
| 3b30bd3580 | |||
| 3fbd74310f | |||
| 9263439af8 | |||
| 4a3cc9fffa | |||
| b5266ad9f5 | |||
| 6175e72f1c | |||
| 58be22e695 | |||
| 2a9bb1ce11 | |||
| 3ad6aa59f9 | |||
| 76c75cc05a | |||
| c7ae951676 | |||
| 94d7b21cf0 | |||
| 2aeee4f509 | |||
| dd8c646b63 | |||
| 527494a34b | |||
| e83fa12451 | |||
| 4f97ff98d6 | |||
| f69c596f56 | |||
| 238523f177 | |||
| c5c74febb5 | |||
| 63d1fcf213 | |||
| b20bd65d38 | |||
| 62d560e2fb | |||
| 6c66391988 | |||
| 6ccfc9ed98 | |||
| e9fee04eef | |||
| 8611cc0ee9 | |||
| 2592f83b69 | |||
| c903a71807 | |||
| 343ec59a8b | |||
| 6f1d50dda3 | |||
| 29c715a45f | |||
| 2675033aac | |||
| b87362cbf1 | |||
| 1c751168c6 | |||
| a582d0559a | |||
| 4e74a1811b | |||
| 97ad9afc86 | |||
| c519a40cb8 | |||
| 3789d60b6a | |||
| 5da42fb859 | |||
| fd4c447a2d | |||
| f30b08f015 | |||
| 5f1cab6850 | |||
| 175e1c6453 | |||
| af772b0240 | |||
| 3fe98f35f2 | |||
| 9d23a2b6f5 | |||
| f15370027e | |||
| b94eeb9580 | |||
| 3968d03868 | |||
| aea82183b2 | |||
| bae0667066 | |||
| 5256cad396 | |||
| 9100af9974 | |||
| b6d53e97a6 | |||
| 336de49e6a | |||
| ee3c58f78f | |||
| 876c6e933c | |||
| 2f2cebe84d | |||
| e257512aa7 | |||
| 411c60009d | |||
| 7680d1bd5e | |||
| 8fedc358e0 | |||
| 90106c4c33 | |||
| a05dc03100 | |||
| 26bcc7e312 | |||
| 85a6d8fc6b | |||
| 2b2793fac6 | |||
| 8f14048528 | |||
| 7f96b2f92a | |||
| b92b4e043c | |||
| 6319384072 | |||
| ead9d66797 | |||
| cd2c473bfe | |||
| 887ae84f1e | |||
| 14e3b242df | |||
| 9f7a4a012b | |||
| 5f625216aa | |||
| 20836cc3db | |||
| 59834a4b05 | |||
| 4b652f5236 | |||
| be5a04f47c | |||
| 9c95a74d56 | |||
| 6b1b464abc | |||
| f897b4daee | |||
| 666989f74c | |||
| 9783bc78ba | |||
| c23786d37f | |||
| a9c280bd4c | |||
| c1f553cf4f | |||
| b4d809c681 | |||
| 3f69f2ee73 | |||
| dac1a01216 | |||
| 44a7ac0703 | |||
| 011d44b749 | |||
| 72fb8371f9 | |||
| 4f0bdb5194 | |||
| fd2a002480 | |||
| 4296d7174f | |||
| 4fe47903c2 | |||
| 08365bf5f4 | |||
| 4ec5d1e28e | |||
| e228dec4f2 | |||
| 6ffdc1b2a6 | |||
| 004be3bf00 | |||
| 77fab2c323 | |||
| 68582dd868 | |||
| feefa43e65 | |||
| c59f474aff | |||
| 86694f2d1d | |||
| 999d731a65 | |||
| 3962333043 | |||
| 61174dd0d3 | |||
| e2afaa9f03 | |||
| 9790a6edc9 | |||
| 08a4e931a0 | |||
| 24a5ecb6b4 | |||
| 1efc52c440 | |||
| f290d1a9c8 | |||
| 7e087bb93c | |||
| 5e74391c6c | |||
| cc86feded3 | |||
| 14fce38403 | |||
| 10be301646 | |||
| 1ce3db727f | |||
| 6eba36d788 | |||
| f59e8af734 | |||
| 1f2e939fd5 | |||
| 13ef6dcbcf | |||
| 27966221f1 | |||
| 79c6b51860 | |||
| e507c31306 | |||
| f36757027e | |||
| 7450c654ae | |||
| 3ed2c17f98 | |||
| 26c890d5ac | |||
| 137e3008ea | |||
| 9da523c004 | |||
| 2e4d9cb37c | |||
| 78aeb620bc | |||
| 4a94a4c945 | |||
| 768e81741c | |||
| 8d251003a2 | |||
| 52f09fdb51 | |||
| f191b4bad4 | |||
| 8742437036 | |||
| ba1c134689 | |||
| 1f1e2d547c | |||
| f746be82c1 | |||
| 0bc6fdd589 | |||
| 6b0eb7608d | |||
| e49aee61c1 | |||
| 7fcc6d11a4 | |||
| 0eb67cfea0 | |||
| 9775694423 | |||
| a7cb1c5951 | |||
| ed76797b55 | |||
| ad117641b8 | |||
| 1fbfc983e9 | |||
| 0387871063 | |||
| 6f37a251fb | |||
| 9466aeb088 | |||
| ee6af6c90e | |||
| 6cbaef2d12 | |||
| 240c78e810 | |||
| 8ed9d49b73 | |||
| 354dc9e703 | |||
| 567a4cb441 | |||
| c71db93e22 | |||
| 0a281241ef | |||
| 85890ed425 | |||
| 065396f8f5 | |||
| d92f2c121f | |||
| 52e356d780 | |||
| 7a09ac81e0 | |||
| 6c9ecb031a | |||
| e7e606300f | |||
| 9787dfe77c | |||
| 5e6dbaa27f | |||
| d281b8d3ae | |||
| 21a67513f2 | |||
| f245389c02 | |||
| 1e7207c230 | |||
| 0426f92ac0 | |||
| 6808671751 | |||
| b7369074d4 | |||
| cf59f738b9 | |||
| 8742266ff0 | |||
| ee92a33a4d | |||
| 60cc07134f | |||
| e175b7d28d | |||
| 0e616f1d12 | |||
| 9438dc89e6 | |||
| efb28c1a99 | |||
| 49343281d4 | |||
| b921983a79 | |||
| 60d84195c5 | |||
| d6991611f0 | |||
| 0efe24a028 | |||
| 2ce91f33af | |||
| 652b04b9b6 | |||
| f29879288d | |||
| 89cc865868 | |||
| aa768b5dec | |||
| c769fcc347 | |||
| 5cb0a5f676 | |||
| 367d153380 | |||
| 3396542168 | |||
| b08c1241a8 | |||
| dd6621a720 | |||
| b8260e0104 | |||
| ca57dc7928 | |||
| d35376a90c | |||
| a74461fc9a | |||
| 0e0438e1f9 | |||
| c06f560913 | |||
| 167807e0a6 | |||
| 0e55fa2de2 | |||
| b505f0d0d7 | |||
| ac75ebee8a | |||
| 93130fbb85 | |||
| 1fdcab0319 | |||
| 828cb96ba9 | |||
| 55b8908894 | |||
| 84191656fb | |||
| 0b085ea84f | |||
| 4576313a7c | |||
| ed5b5d7877 | |||
| d0ee4b6d25 | |||
| b4ec1e9d3c | |||
| c0939c3e9a | |||
| d82ea331cf | |||
| 1a09eb0f02 | |||
| 89b5c4ee1c | |||
| 2ed0c267eb | |||
| 8258d16a94 | |||
| 19880ce12b | |||
| d3d11356ee | |||
| 2f24d7117a | |||
| fc4d109f35 | |||
| f67cfcd535 | |||
| 2a59a56eaa | |||
| c40d20cb95 | |||
| 43b0bb6a5e | |||
| a5e85727b5 | |||
| 16f82b02a0 | |||
| c9c405facf | |||
| 8ea2dccc9a | |||
| e482c0646f | |||
| f503ed918c | |||
| 57e0a5f65d | |||
| d526db681f | |||
| 55c85f6851 | |||
| f7af6966b7 | |||
| 68315ac112 | |||
| da34b43302 | |||
| 48a767d52c | |||
| 2b2055fe8a | |||
| 685eadb171 | |||
| dd9f53080a | |||
| 4485f36e34 | |||
| a2e5c3d5d3 | |||
| 08a2fecc0e | |||
| 89a3c80700 | |||
| 56dd0f5139 | |||
| 814b9e28b6 | |||
| 8eec78e9e0 | |||
| 9eace1fbbb | |||
| ba683cf534 | |||
| bd9a9cc5f8 | |||
| 2d049dacc3 | |||
| c6b7c24e99 | |||
| fa7c1200b5 | |||
| bd56d83045 | |||
| ab9a65db5d | |||
| 54a107c3c4 | |||
| 98363852b1 | |||
| 4eb7ad79d1 | |||
| 115ea03edf | |||
| a9e3e8f77a | |||
| 6a81bf23de | |||
| 7a59add8f1 | |||
| ee1580e480 | |||
| b64a235165 | |||
| 4413793f7e | |||
| 2083c38c76 | |||
| 890ee84f71 | |||
| fafe320899 | |||
| 8311952629 | |||
| 36677bb982 | |||
| ab06701ed0 | |||
| 26dcab272d | |||
| 96fcf7f94d | |||
| 6b80361c31 | |||
| a8d5cf9651 | |||
| c569881b08 | |||
| 0e8ae1e13e | |||
| 5192927a53 | |||
| 4496cf2d5b | |||
| 3f7ec4221d | |||
| 4776fe66c4 | |||
| 946ca364e0 | |||
| 6001014078 | |||
| a5de27442a | |||
| f7ce4db0b0 | |||
| a5822ebc27 | |||
| 63053640f1 | |||
| bd75ff65c9 | |||
| aa265f7ca4 | |||
| 3d4b0f10a5 | |||
| 2709995f84 | |||
| 99ad404ea9 | |||
| 2db017af37 |
+25
-27
@@ -3,15 +3,10 @@ name: CI and Artifacts
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
# documentation workflow deals with this or is not relevant for this workflow
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- 'conduwuit-example.toml'
|
||||
- 'book.toml'
|
||||
- '.gitlab-ci.yml'
|
||||
- '.gitignore'
|
||||
- 'renovate.json'
|
||||
- 'docs/**'
|
||||
- 'debian/**'
|
||||
- 'docker/**'
|
||||
branches:
|
||||
@@ -23,7 +18,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# sccache only on main repo
|
||||
@@ -65,23 +60,20 @@ permissions:
|
||||
jobs:
|
||||
tests:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
|
||||
- name: Free up more runner space
|
||||
- name: Install liburing
|
||||
run: |
|
||||
set +o pipefail
|
||||
# large docker images
|
||||
sudo docker image prune --all --force || true
|
||||
# large packages
|
||||
sudo apt-get purge -y '^llvm-.*' 'php.*' '^mongodb-.*' '^mysql-.*' azure-cli google-cloud-cli google-chrome-stable firefox powershell microsoft-edge-stable || true
|
||||
sudo apt-get autoremove -y
|
||||
sudo apt-get clean
|
||||
# large folders
|
||||
sudo rm -rf /var/lib/apt/lists/* /usr/local/games /usr/local/sqlpackage /usr/local/.ghcup /usr/local/share/powershell /usr/local/share/edge_driver /usr/local/share/gecko_driver /usr/local/share/chromium /usr/local/share/chromedriver-linux64 /usr/local/share/vcpkg /usr/local/lib/python* /usr/local/lib/node_modules /usr/local/julia* /opt/mssql-tools /etc/skel /usr/share/vim /usr/share/postgresql /usr/share/man /usr/share/apache-maven-* /usr/share/R /usr/share/alsa /usr/share/miniconda /usr/share/grub /usr/share/gradle-* /usr/share/locale /usr/share/texinfo /usr/share/kotlinc /usr/share/swift /usr/share/doc /usr/share/az_9.3.0 /usr/share/sbt /usr/share/ri /usr/share/icons /usr/share/java /usr/share/fonts /usr/lib/google-cloud-sdk /usr/lib/jvm /usr/lib/mono /usr/lib/R /usr/lib/postgresql /usr/lib/heroku /usr/lib/gcc
|
||||
set -o pipefail
|
||||
sudo apt install liburing-dev -y
|
||||
|
||||
- name: Free up a bit of runner space
|
||||
run: |
|
||||
set +o pipefail
|
||||
sudo docker image prune --all --force || true
|
||||
sudo apt purge -y 'php.*' '^mongodb-.*' '^mysql-.*' azure-cli google-cloud-cli google-chrome-stable firefox powershell microsoft-edge-stable || true
|
||||
sudo apt clean
|
||||
sudo rm -v -rf /usr/local/games /usr/local/sqlpackage /usr/local/share/powershell /usr/local/share/edge_driver /usr/local/share/gecko_driver /usr/local/share/chromium /usr/local/share/chromedriver-linux64 /usr/lib/google-cloud-sdk /usr/lib/jvm /usr/lib/mono /usr/lib/heroku
|
||||
set -o pipefail
|
||||
|
||||
- name: Sync repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -231,7 +223,7 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
needs: tests
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -239,13 +231,10 @@ jobs:
|
||||
- target: aarch64-linux-musl
|
||||
- target: x86_64-linux-musl
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
|
||||
- name: Sync repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: nixbuild/nix-quick-install-action@v28
|
||||
- uses: nixbuild/nix-quick-install-action@master
|
||||
|
||||
- name: Restore and cache Nix store
|
||||
uses: nix-community/cache-nix-action@v5.1.0
|
||||
@@ -450,6 +439,7 @@ jobs:
|
||||
steps:
|
||||
- name: Sync repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Tag comparison check
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-rc') }}
|
||||
run: |
|
||||
@@ -460,14 +450,17 @@ jobs:
|
||||
echo '# WARNING: Attempting to run this workflow for a tag that is not the latest repo tag. Aborting.' >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# use sccache for Rust
|
||||
- name: Run sccache-cache
|
||||
if: (github.event.pull_request.draft != true) && (vars.DOCKER_USERNAME != '') && (vars.GITLAB_USERNAME != '') && (vars.SCCACHE_ENDPOINT != '') && (github.event.pull_request.user.login != 'renovate[bot]')
|
||||
uses: mozilla-actions/sccache-action@main
|
||||
|
||||
# use rust-cache
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
cache-all-crates: "true"
|
||||
|
||||
# Nix can't do portable macOS builds yet
|
||||
- name: Build macOS x86_64 binary
|
||||
if: ${{ matrix.os == 'macos-13' }}
|
||||
@@ -475,22 +468,26 @@ jobs:
|
||||
CONDUWUIT_VERSION_EXTRA="$(git rev-parse --short HEAD)" cargo build --release
|
||||
cp -v -f target/release/conduit conduwuit-macos-x86_64
|
||||
otool -L conduwuit-macos-x86_64
|
||||
|
||||
# quick smoke test of the x86_64 macOS binary
|
||||
- name: Run x86_64 macOS release binary
|
||||
if: ${{ matrix.os == 'macos-13' }}
|
||||
run: |
|
||||
./conduwuit-macos-x86_64 --version
|
||||
|
||||
- name: Build macOS arm64 binary
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
run: |
|
||||
CONDUWUIT_VERSION_EXTRA="$(git rev-parse --short HEAD)" cargo build --release
|
||||
cp -v -f target/release/conduit conduwuit-macos-arm64
|
||||
otool -L conduwuit-macos-arm64
|
||||
|
||||
# quick smoke test of the arm64 macOS binary
|
||||
- name: Run arm64 macOS release binary
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
run: |
|
||||
./conduwuit-macos-arm64 --version
|
||||
|
||||
- name: Upload macOS x86_64 binary
|
||||
if: ${{ matrix.os == 'macos-13' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -498,6 +495,7 @@ jobs:
|
||||
name: conduwuit-macos-x86_64
|
||||
path: conduwuit-macos-x86_64
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload macOS arm64 binary
|
||||
if: ${{ matrix.os == 'macos-latest' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -508,7 +506,7 @@ jobs:
|
||||
|
||||
docker:
|
||||
name: Docker publish
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event.pull_request.draft != true)) && (vars.DOCKER_USERNAME != '') && (vars.GITLAB_USERNAME != '') && github.event.pull_request.user.login != 'renovate[bot]'
|
||||
env:
|
||||
|
||||
@@ -39,7 +39,7 @@ concurrency:
|
||||
jobs:
|
||||
docs:
|
||||
name: Documentation and GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
pages: write
|
||||
@@ -50,14 +50,20 @@ jobs:
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
- name: Free up a bit of runner space
|
||||
run: |
|
||||
set +o pipefail
|
||||
sudo docker image prune --all --force || true
|
||||
sudo apt purge -y 'php.*' '^mongodb-.*' '^mysql-.*' azure-cli google-cloud-cli google-chrome-stable firefox powershell microsoft-edge-stable || true
|
||||
sudo apt clean
|
||||
sudo rm -v -rf /usr/local/games /usr/local/sqlpackage /usr/local/share/powershell /usr/local/share/edge_driver /usr/local/share/gecko_driver /usr/local/share/chromium /usr/local/share/chromedriver-linux64 /usr/lib/google-cloud-sdk /usr/lib/jvm /usr/lib/mono /usr/lib/heroku
|
||||
set -o pipefail
|
||||
|
||||
- name: Sync repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup GitHub Pages
|
||||
if: github.event_name != 'pull_request'
|
||||
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') && (github.event_name != 'pull_request')
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- uses: nixbuild/nix-quick-install-action@master
|
||||
@@ -139,12 +145,12 @@ jobs:
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload generated documentation (book) as GitHub Pages artifact
|
||||
if: github.event_name != 'pull_request'
|
||||
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') && (github.event_name != 'pull_request')
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: public
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: github.event_name != 'pull_request'
|
||||
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') && (github.event_name != 'pull_request')
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
name: Trivy code and vulnerability scanning
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '*'
|
||||
schedule:
|
||||
- cron: '00 12 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
trivy-scan:
|
||||
name: Trivy Scan
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Trivy code and vulnerability scanner on repo
|
||||
uses: aquasecurity/trivy-action@0.28.0
|
||||
with:
|
||||
scan-type: repo
|
||||
format: sarif
|
||||
output: trivy-results.sarif
|
||||
severity: CRITICAL,HIGH,MEDIUM,LOW
|
||||
|
||||
- name: Run Trivy code and vulnerability scanner on filesystem
|
||||
uses: aquasecurity/trivy-action@0.28.0
|
||||
with:
|
||||
scan-type: fs
|
||||
format: sarif
|
||||
output: trivy-results.sarif
|
||||
severity: CRITICAL,HIGH,MEDIUM,LOW
|
||||
+2
-1
@@ -131,7 +131,8 @@ allowed to be licenced under the Apache-2.0 licence and all of your conduct is
|
||||
in line with the Contributor's Covenant, and conduwuit's Code of Conduct.
|
||||
|
||||
Contribution by users who violate either of these code of conducts will not have
|
||||
their contributions accepted.
|
||||
their contributions accepted. This includes users who have been banned from
|
||||
conduwuit Matrix rooms for Code of Conduct violations.
|
||||
|
||||
[issues]: https://github.com/girlbossceo/conduwuit/issues
|
||||
[conduwuit-matrix]: https://matrix.to/#/#conduwuit:puppygock.gay
|
||||
|
||||
Generated
+601
-259
File diff suppressed because it is too large
Load Diff
+81
-52
@@ -19,20 +19,30 @@ license = "Apache-2.0"
|
||||
# See also `rust-toolchain.toml`
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/girlbossceo/conduwuit"
|
||||
rust-version = "1.82.0"
|
||||
version = "0.4.7"
|
||||
rust-version = "1.83.0"
|
||||
version = "0.5.0"
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "conduit"
|
||||
|
||||
[workspace.dependencies.arrayvec]
|
||||
version = "0.7.4"
|
||||
features = ["serde"]
|
||||
|
||||
[workspace.dependencies.smallvec]
|
||||
version = "1.13.2"
|
||||
features = [
|
||||
"const_generics",
|
||||
"const_new",
|
||||
"serde",
|
||||
"write",
|
||||
]
|
||||
|
||||
[workspace.dependencies.const-str]
|
||||
version = "0.5.7"
|
||||
|
||||
[workspace.dependencies.ctor]
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
|
||||
[workspace.dependencies.cargo_toml]
|
||||
version = "0.20"
|
||||
@@ -45,20 +55,20 @@ default-features = false
|
||||
features = ["parse"]
|
||||
|
||||
[workspace.dependencies.sanitize-filename]
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
|
||||
[workspace.dependencies.jsonwebtoken]
|
||||
version = "9.3.0"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.base64]
|
||||
version = "0.22.1"
|
||||
default-features = false
|
||||
|
||||
# used for TURN server authentication
|
||||
[workspace.dependencies.hmac]
|
||||
version = "0.12.1"
|
||||
|
||||
[workspace.dependencies.sha-1]
|
||||
version = "0.10.1"
|
||||
default-features = false
|
||||
|
||||
# used for checking if an IP is in specific subnets / CIDR ranges easier
|
||||
[workspace.dependencies.ipaddress]
|
||||
@@ -69,19 +79,19 @@ version = "0.8.5"
|
||||
|
||||
# Used for the http request / response body type for Ruma endpoints used with reqwest
|
||||
[workspace.dependencies.bytes]
|
||||
version = "1.7.2"
|
||||
version = "1.8.0"
|
||||
|
||||
[workspace.dependencies.http-body-util]
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
|
||||
[workspace.dependencies.http]
|
||||
version = "1.1.0"
|
||||
|
||||
[workspace.dependencies.regex]
|
||||
version = "1.10.6"
|
||||
version = "1.11.1"
|
||||
|
||||
[workspace.dependencies.axum]
|
||||
version = "0.7.5"
|
||||
version = "0.7.9"
|
||||
default-features = false
|
||||
features = [
|
||||
"form",
|
||||
@@ -94,7 +104,7 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.axum-extra]
|
||||
version = "0.9.3"
|
||||
version = "0.9.6"
|
||||
default-features = false
|
||||
features = ["typed-header", "tracing"]
|
||||
|
||||
@@ -115,7 +125,7 @@ default-features = false
|
||||
features = ["util"]
|
||||
|
||||
[workspace.dependencies.tower-http]
|
||||
version = "0.6.0"
|
||||
version = "0.6.2"
|
||||
default-features = false
|
||||
features = [
|
||||
"add-extension",
|
||||
@@ -128,10 +138,12 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.rustls]
|
||||
version = "0.23.13"
|
||||
version = "0.23.16"
|
||||
default-features = false
|
||||
features = ["aws_lc_rs"]
|
||||
|
||||
[workspace.dependencies.reqwest]
|
||||
version = "0.12.8"
|
||||
version = "0.12.9"
|
||||
default-features = false
|
||||
features = [
|
||||
"rustls-tls-native-roots",
|
||||
@@ -141,12 +153,12 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.serde]
|
||||
version = "1.0.209"
|
||||
version = "1.0.215"
|
||||
default-features = false
|
||||
features = ["rc"]
|
||||
|
||||
[workspace.dependencies.serde_json]
|
||||
version = "1.0.124"
|
||||
version = "1.0.133"
|
||||
default-features = false
|
||||
features = ["raw_value"]
|
||||
|
||||
@@ -170,7 +182,7 @@ default-features = false
|
||||
|
||||
# Used to generate thumbnails for images
|
||||
[workspace.dependencies.image]
|
||||
version = "0.25.1"
|
||||
version = "0.25.5"
|
||||
default-features = false
|
||||
features = [
|
||||
"jpeg",
|
||||
@@ -181,16 +193,18 @@ features = [
|
||||
|
||||
# logging
|
||||
[workspace.dependencies.log]
|
||||
version = "0.4.21"
|
||||
version = "0.4.22"
|
||||
default-features = false
|
||||
[workspace.dependencies.tracing]
|
||||
version = "0.1.40"
|
||||
version = "0.1.41"
|
||||
default-features = false
|
||||
[workspace.dependencies.tracing-subscriber]
|
||||
version = "0.3.18"
|
||||
features = ["env-filter"]
|
||||
default-features = false
|
||||
features = ["env-filter", "std", "tracing", "tracing-log", "ansi", "fmt"]
|
||||
[workspace.dependencies.tracing-core]
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
default-features = false
|
||||
|
||||
# for URL previews
|
||||
[workspace.dependencies.webpage]
|
||||
@@ -199,23 +213,25 @@ default-features = false
|
||||
|
||||
# used for conduit's CLI and admin room command parsing
|
||||
[workspace.dependencies.clap]
|
||||
version = "4.5.20"
|
||||
version = "4.5.21"
|
||||
default-features = false
|
||||
features = [
|
||||
"std",
|
||||
"derive",
|
||||
"help",
|
||||
"usage",
|
||||
"env",
|
||||
"error-context",
|
||||
"help",
|
||||
"std",
|
||||
"string",
|
||||
"usage",
|
||||
]
|
||||
|
||||
[workspace.dependencies.futures-util]
|
||||
[workspace.dependencies.futures]
|
||||
version = "0.3.30"
|
||||
default-features = false
|
||||
features = ["std", "async-await"]
|
||||
|
||||
[workspace.dependencies.tokio]
|
||||
version = "1.40.0"
|
||||
version = "1.41.1"
|
||||
default-features = false
|
||||
features = [
|
||||
"fs",
|
||||
@@ -236,7 +252,7 @@ version = "0.8.5"
|
||||
|
||||
# Validating urls in config, was already a transitive dependency
|
||||
[workspace.dependencies.url]
|
||||
version = "2.5.0"
|
||||
version = "2.5.4"
|
||||
default-features = false
|
||||
features = ["serde"]
|
||||
|
||||
@@ -247,7 +263,7 @@ features = ["alloc", "std"]
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.hyper]
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
default-features = false
|
||||
features = [
|
||||
"server",
|
||||
@@ -256,26 +272,24 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.hyper-util]
|
||||
# 0.1.9 causes DNS issues
|
||||
# hyper-util >=0.1.9 seems to have DNS issues
|
||||
version = "=0.1.8"
|
||||
default-features = false
|
||||
features = [
|
||||
"client",
|
||||
"server-auto",
|
||||
"server-graceful",
|
||||
"service",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
# to support multiple variations of setting a config option
|
||||
[workspace.dependencies.either]
|
||||
version = "1.11.0"
|
||||
version = "1.13.0"
|
||||
default-features = false
|
||||
features = ["serde"]
|
||||
|
||||
# Used for reading the configuration from conduwuit.toml & environment variables
|
||||
[workspace.dependencies.figment]
|
||||
version = "0.10.18"
|
||||
version = "0.10.19"
|
||||
default-features = false
|
||||
features = ["env", "toml"]
|
||||
|
||||
@@ -285,11 +299,13 @@ default-features = false
|
||||
|
||||
# Used for conduit::Error type
|
||||
[workspace.dependencies.thiserror]
|
||||
version = "1.0.63"
|
||||
version = "2.0.3"
|
||||
default-features = false
|
||||
|
||||
# Used when hashing the state
|
||||
[workspace.dependencies.ring]
|
||||
version = "0.17.8"
|
||||
default-features = false
|
||||
|
||||
# Used to make working with iterators easier, was already a transitive depdendency
|
||||
[workspace.dependencies.itertools]
|
||||
@@ -300,12 +316,16 @@ version = "0.13.0"
|
||||
[workspace.dependencies.cyborgtime]
|
||||
version = "2.1.1"
|
||||
|
||||
# used to replace the channels of the tokio runtime
|
||||
# used for MPSC channels
|
||||
[workspace.dependencies.loole]
|
||||
version = "0.3.1"
|
||||
version = "0.4.0"
|
||||
|
||||
# used for MPMC channels
|
||||
[workspace.dependencies.async-channel]
|
||||
version = "2.3.1"
|
||||
|
||||
[workspace.dependencies.async-trait]
|
||||
version = "0.1.81"
|
||||
version = "0.1.83"
|
||||
|
||||
[workspace.dependencies.lru-cache]
|
||||
version = "0.1.2"
|
||||
@@ -314,7 +334,7 @@ version = "0.1.2"
|
||||
[workspace.dependencies.ruma]
|
||||
git = "https://github.com/girlbossceo/ruwuma"
|
||||
#branch = "conduwuit-changes"
|
||||
rev = "9900d0676564883cfade556d6e8da2a2c9061efd"
|
||||
rev = "1a550585bf025cce48ef8b734339245092bc986e"
|
||||
features = [
|
||||
"compat",
|
||||
"rand",
|
||||
@@ -327,6 +347,7 @@ features = [
|
||||
"server-util",
|
||||
"unstable-exhaustive-types",
|
||||
"ring-compat",
|
||||
"compat-upload-signatures",
|
||||
"identifiers-validation",
|
||||
"unstable-unspecified",
|
||||
"unstable-msc2409",
|
||||
@@ -345,6 +366,7 @@ features = [
|
||||
"unstable-msc4121",
|
||||
"unstable-msc4125",
|
||||
"unstable-msc4186",
|
||||
"unstable-msc4210", # remove legacy mentions
|
||||
"unstable-extensible-events",
|
||||
]
|
||||
|
||||
@@ -360,9 +382,13 @@ features = [
|
||||
"bzip2",
|
||||
]
|
||||
|
||||
# optional SHA256 media keys feature
|
||||
[workspace.dependencies.sha2]
|
||||
version = "0.10.8"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.sha1]
|
||||
version = "0.10.6"
|
||||
default-features = false
|
||||
|
||||
# optional opentelemetry, performance measurements, flamegraphs, etc for performance measurements and monitoring
|
||||
[workspace.dependencies.opentelemetry]
|
||||
@@ -430,7 +456,8 @@ default-features = false
|
||||
features = ["resource"]
|
||||
|
||||
[workspace.dependencies.sd-notify]
|
||||
version = "0.4.1"
|
||||
version = "0.4.3"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.hardened_malloc-rs]
|
||||
version = "0.1.2"
|
||||
@@ -446,23 +473,25 @@ version = "0.4.3"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.termimad]
|
||||
version = "0.30.1"
|
||||
version = "0.31.1"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.checked_ops]
|
||||
version = "0.1"
|
||||
|
||||
[workspace.dependencies.syn]
|
||||
version = "2.0.76"
|
||||
version = "2.0.87"
|
||||
default-features = false
|
||||
features = ["full", "extra-traits"]
|
||||
|
||||
[workspace.dependencies.quote]
|
||||
version = "1.0.36"
|
||||
version = "1.0.37"
|
||||
|
||||
[workspace.dependencies.proc-macro2]
|
||||
version = "1.0.89"
|
||||
|
||||
[workspace.dependencies.bytesize]
|
||||
version = "1.3.0"
|
||||
|
||||
#
|
||||
# Patches
|
||||
@@ -473,22 +502,22 @@ version = "1.0.89"
|
||||
# https://github.com/girlbossceo/tracing/commit/b348dca742af641c47bc390261f60711c2af573c
|
||||
[patch.crates-io.tracing-subscriber]
|
||||
git = "https://github.com/girlbossceo/tracing"
|
||||
rev = "4d78a14a5e03f539b8c6b475aefa08bb14e4de91"
|
||||
rev = "ccc4fbd8238c2d5ba354e61ec17ac610af11401d"
|
||||
[patch.crates-io.tracing]
|
||||
git = "https://github.com/girlbossceo/tracing"
|
||||
rev = "4d78a14a5e03f539b8c6b475aefa08bb14e4de91"
|
||||
rev = "ccc4fbd8238c2d5ba354e61ec17ac610af11401d"
|
||||
[patch.crates-io.tracing-core]
|
||||
git = "https://github.com/girlbossceo/tracing"
|
||||
rev = "4d78a14a5e03f539b8c6b475aefa08bb14e4de91"
|
||||
rev = "ccc4fbd8238c2d5ba354e61ec17ac610af11401d"
|
||||
[patch.crates-io.tracing-log]
|
||||
git = "https://github.com/girlbossceo/tracing"
|
||||
rev = "4d78a14a5e03f539b8c6b475aefa08bb14e4de91"
|
||||
rev = "ccc4fbd8238c2d5ba354e61ec17ac610af11401d"
|
||||
|
||||
# adds a tab completion callback: https://github.com/girlbossceo/rustyline-async/commit/de26100b0db03e419a3d8e1dd26895d170d1fe50
|
||||
# adds event for CTRL+\: https://github.com/girlbossceo/rustyline-async/commit/67d8c49aeac03a5ef4e818f663eaa94dd7bf339b
|
||||
[patch.crates-io.rustyline-async]
|
||||
git = "https://github.com/girlbossceo/rustyline-async"
|
||||
rev = "9654cc84e19241f6e19021eb8e677892656f5071"
|
||||
rev = "deaeb0694e2083f53d363b648da06e10fc13900c"
|
||||
|
||||
#
|
||||
# Our crates
|
||||
@@ -617,7 +646,6 @@ opt-level = 0
|
||||
panic = "unwind"
|
||||
debug-assertions = true
|
||||
incremental = true
|
||||
codegen-units = 64
|
||||
#rustflags = [
|
||||
# '--cfg', 'conduit_mods',
|
||||
# '-Ztime-passes',
|
||||
@@ -659,7 +687,6 @@ incremental = false
|
||||
|
||||
[profile.dev.package.conduit]
|
||||
inherits = "dev"
|
||||
incremental = false
|
||||
#rustflags = [
|
||||
# '--cfg', 'conduit_mods',
|
||||
# '-Ztime-passes',
|
||||
@@ -771,6 +798,7 @@ unused-qualifications = "warn"
|
||||
#unused-results = "warn" # TODO
|
||||
|
||||
## some sadness
|
||||
elided_named_lifetimes = "allow" # TODO!
|
||||
let_underscore_drop = "allow"
|
||||
missing_docs = "allow"
|
||||
# cfgs cannot be limited to expected cfgs or their de facto non-transitive/opt-in use-case e.g.
|
||||
@@ -828,6 +856,7 @@ missing_panics_doc = { level = "allow", priority = 1 }
|
||||
module_name_repetitions = { level = "allow", priority = 1 }
|
||||
no_effect_underscore_binding = { level = "allow", priority = 1 }
|
||||
similar_names = { level = "allow", priority = 1 }
|
||||
single_match_else = { level = "allow", priority = 1 }
|
||||
struct_field_names = { level = "allow", priority = 1 }
|
||||
unnecessary_wraps = { level = "allow", priority = 1 }
|
||||
unused_async = { level = "allow", priority = 1 }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# conduwuit
|
||||
|
||||
`main`: [](https://github.com/girlbossceo/conduwuit/actions/workflows/ci.yml)
|
||||
[](https://matrix.to/#/#conduwuit:puppygock.gay) [](https://matrix.to/#/#conduwuit-space:puppygock.gay) [](https://github.com/girlbossceo/conduwuit/actions/workflows/ci.yml)
|
||||
|
||||
<!-- ANCHOR: catchphrase -->
|
||||
|
||||
@@ -10,7 +9,7 @@ Artifacts](https://github.com/girlbossceo/conduwuit/actions/workflows/ci.yml/bad
|
||||
<!-- ANCHOR_END: catchphrase -->
|
||||
|
||||
Visit the [conduwuit documentation](https://conduwuit.puppyirl.gay/) for more
|
||||
information.
|
||||
information and how to deploy/setup conduwuit.
|
||||
|
||||
<!-- ANCHOR: body -->
|
||||
|
||||
@@ -63,7 +62,9 @@ and we have no plans in stopping or slowing down any time soon!
|
||||
|
||||
conduwuit is a complete drop-in replacement for Conduit. As long as you are using RocksDB,
|
||||
the only "migration" you need to do is replace the binary or container image. There
|
||||
is no harm or additional steps required for using conduwuit.
|
||||
is no harm or additional steps required for using conduwuit. See the
|
||||
[Migrating from Conduit](https://conduwuit.puppyirl.gay/deploying/generic.html#migrating-from-conduit) section
|
||||
on the generic deploying guide.
|
||||
|
||||
<!-- ANCHOR_END: body -->
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ RESULTS_FILE="$3"
|
||||
OCI_IMAGE="complement-conduwuit:main"
|
||||
|
||||
# Complement tests that are skipped due to flakiness/reliability issues
|
||||
SKIPPED_COMPLEMENT_TESTS='-skip=TestClientSpacesSummary.*|TestJoinFederatedRoomFromApplicationServiceBridgeUser.*|TestJumpToDateEndpoint.*'
|
||||
SKIPPED_COMPLEMENT_TESTS='-skip=TestClientSpacesSummary.*|TestJoinFederatedRoomFromApplicationServiceBridgeUser.*|TestJumpToDateEndpoint.*|TestUnbanViaInvite.*'
|
||||
|
||||
# $COMPLEMENT_SRC needs to be a directory to Complement source code
|
||||
if [ -f "$COMPLEMENT_SRC" ]; then
|
||||
|
||||
+9
-1
@@ -2,6 +2,14 @@ array-size-threshold = 4096
|
||||
cognitive-complexity-threshold = 94 # TODO reduce me ALARA
|
||||
excessive-nesting-threshold = 11 # TODO reduce me to 4 or 5
|
||||
future-size-threshold = 7745 # TODO reduce me ALARA
|
||||
stack-size-threshold = 144000 # reduce me ALARA
|
||||
stack-size-threshold = 196608 # reduce me ALARA
|
||||
too-many-lines-threshold = 700 # TODO reduce me to <= 100
|
||||
type-complexity-threshold = 250 # reduce me to ~200
|
||||
|
||||
disallowed-macros = [
|
||||
{ path = "log::error", reason = "use conduit_core::error" },
|
||||
{ path = "log::warn", reason = "use conduit_core::warn" },
|
||||
{ path = "log::info", reason = "use conduit_core::info" },
|
||||
{ path = "log::debug", reason = "use conduit_core::debug" },
|
||||
{ path = "log::trace", reason = "use conduit_core::trace" },
|
||||
]
|
||||
|
||||
+1211
-743
File diff suppressed because it is too large
Load Diff
Vendored
+10
-5
@@ -1,17 +1,22 @@
|
||||
# conduwuit for Debian
|
||||
|
||||
Information about downloading and deploying the Debian package. This may also be referenced for other `apt`-based distros such as Ubuntu.
|
||||
Information about downloading and deploying the Debian package. This may also be
|
||||
referenced for other `apt`-based distros such as Ubuntu.
|
||||
|
||||
### Installation
|
||||
|
||||
It is recommended to see the [generic deployment guide](../deploying/generic.md) for further information if needed as usage of the Debian package is generally related.
|
||||
It is recommended to see the [generic deployment guide](../deploying/generic.md)
|
||||
for further information if needed as usage of the Debian package is generally
|
||||
related.
|
||||
|
||||
### Configuration
|
||||
|
||||
When installed, the example config is placed at `/etc/conduwuit/conduwuit.toml` as the default config. At the minimum, you will need to change your `server_name` here.
|
||||
When installed, the example config is placed at `/etc/conduwuit/conduwuit.toml`
|
||||
as the default config. The config mentions things required to be changed before
|
||||
starting.
|
||||
|
||||
You can tweak more detailed settings by uncommenting and setting the config options
|
||||
in `/etc/conduwuit/conduwuit.toml`.
|
||||
You can tweak more detailed settings by uncommenting and setting the config
|
||||
options in `/etc/conduwuit/conduwuit.toml`.
|
||||
|
||||
### Running
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -27,7 +27,7 @@ malloc-usable-size = ["rust-rocksdb/malloc-usable-size"]
|
||||
|
||||
[dependencies.rust-rocksdb]
|
||||
git = "https://github.com/girlbossceo/rust-rocksdb-zaidoon1"
|
||||
rev = "c1e5523eae095a893deaf9056128c7dbc2d5fd73"
|
||||
rev = "4bce1bb97d8be6f0d47245c99d465ca9cef33aad"
|
||||
#branch = "master"
|
||||
default-features = false
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- [Generic](deploying/generic.md)
|
||||
- [NixOS](deploying/nixos.md)
|
||||
- [Docker](deploying/docker.md)
|
||||
- [Kubernetes](deploying/kubernetes.md)
|
||||
- [Arch Linux](deploying/arch-linux.md)
|
||||
- [Debian](deploying/debian.md)
|
||||
- [FreeBSD](deploying/freebsd.md)
|
||||
|
||||
@@ -14,9 +14,8 @@ services:
|
||||
environment:
|
||||
CONDUWUIT_SERVER_NAME: your.server.name.example # EDIT THIS
|
||||
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit
|
||||
CONDUWUIT_DATABASE_BACKEND: rocksdb
|
||||
CONDUWUIT_PORT: 6167 # should match the loadbalancer traefik label
|
||||
CONDUWUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
|
||||
CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
|
||||
CONDUWUIT_ALLOW_REGISTRATION: 'true'
|
||||
CONDUWUIT_ALLOW_FEDERATION: 'true'
|
||||
CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
|
||||
|
||||
@@ -30,9 +30,8 @@ services:
|
||||
environment:
|
||||
CONDUWUIT_SERVER_NAME: example.com # EDIT THIS
|
||||
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit
|
||||
CONDUWUIT_DATABASE_BACKEND: rocksdb
|
||||
CONDUWUIT_PORT: 6167
|
||||
CONDUWUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
|
||||
CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
|
||||
CONDUWUIT_ALLOW_REGISTRATION: 'true'
|
||||
CONDUWUIT_ALLOW_FEDERATION: 'true'
|
||||
CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
|
||||
|
||||
@@ -15,7 +15,8 @@ services:
|
||||
CONDUWUIT_SERVER_NAME: your.server.name.example # EDIT THIS
|
||||
CONDUWUIT_TRUSTED_SERVERS: '["matrix.org"]'
|
||||
CONDUWUIT_ALLOW_REGISTRATION: 'false' # After setting a secure registration token, you can enable this
|
||||
CONDUWUIT_REGISTRATION_TOKEN: # This is a token you can use to register on the server
|
||||
CONDUWUIT_REGISTRATION_TOKEN: "" # This is a token you can use to register on the server
|
||||
#CONDUWUIT_REGISTRATION_TOKEN_FILE: "" # Alternatively you can configure a path to a token file to read
|
||||
CONDUWUIT_ADDRESS: 0.0.0.0
|
||||
CONDUWUIT_PORT: 6167 # you need to match this with the traefik load balancer label if you're want to change it
|
||||
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit
|
||||
@@ -23,7 +24,6 @@ services:
|
||||
### Uncomment and change values as desired, note that conduwuit has plenty of config options, so you should check out the example example config too
|
||||
# Available levels are: error, warn, info, debug, trace - more info at: https://docs.rs/env_logger/*/env_logger/#enabling-logging
|
||||
# CONDUWUIT_LOG: info # default is: "warn,state_res=warn"
|
||||
# CONDUWUIT_ALLOW_JAEGER: 'false'
|
||||
# CONDUWUIT_ALLOW_ENCRYPTION: 'true'
|
||||
# CONDUWUIT_ALLOW_FEDERATION: 'true'
|
||||
# CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
# CONDUWUIT_ALLOW_OUTGOING_PRESENCE: true
|
||||
# CONDUWUIT_ALLOW_LOCAL_PRESENCE: true
|
||||
# CONDUWUIT_WORKERS: 10
|
||||
# CONDUWUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
|
||||
# CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
|
||||
# CONDUWUIT_NEW_USER_DISPLAYNAME_SUFFIX = "🏳<200d>⚧"
|
||||
|
||||
# We need some way to serve the client and server .well-known json. The simplest way is via the CONDUWUIT_WELL_KNOWN
|
||||
|
||||
@@ -14,9 +14,8 @@ services:
|
||||
environment:
|
||||
CONDUWUIT_SERVER_NAME: your.server.name # EDIT THIS
|
||||
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit
|
||||
CONDUWUIT_DATABASE_BACKEND: rocksdb
|
||||
CONDUWUIT_PORT: 6167
|
||||
CONDUWUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
|
||||
CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
|
||||
CONDUWUIT_ALLOW_REGISTRATION: 'true'
|
||||
CONDUWUIT_ALLOW_FEDERATION: 'true'
|
||||
CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
|
||||
|
||||
+26
-12
@@ -11,9 +11,9 @@ OCI images for conduwuit are available in the registries listed below.
|
||||
|
||||
| Registry | Image | Size | Notes |
|
||||
| --------------- | --------------------------------------------------------------- | ----------------------------- | ---------------------- |
|
||||
| GitHub Registry | [ghcr.io/girlbossceo/conduwuit:latest][gh] | ![Image Size][shield-latest] | Stable tagged image. |
|
||||
| GitLab Registry | [registry.gitlab.com/conduwuit/conduwuit:latest][gl] | ![Image Size][shield-latest] | Stable tagged image. |
|
||||
| Docker Hub | [docker.io/girlbossceo/conduwuit:latest][dh] | ![Image Size][shield-latest] | Stable tagged image. |
|
||||
| GitHub Registry | [ghcr.io/girlbossceo/conduwuit:latest][gh] | ![Image Size][shield-latest] | Stable latest tagged image. |
|
||||
| GitLab Registry | [registry.gitlab.com/conduwuit/conduwuit:latest][gl] | ![Image Size][shield-latest] | Stable latest tagged image. |
|
||||
| Docker Hub | [docker.io/girlbossceo/conduwuit:latest][dh] | ![Image Size][shield-latest] | Stable latest tagged image. |
|
||||
| GitHub Registry | [ghcr.io/girlbossceo/conduwuit:main][gh] | ![Image Size][shield-main] | Stable main branch. |
|
||||
| GitLab Registry | [registry.gitlab.com/conduwuit/conduwuit:main][gl] | ![Image Size][shield-main] | Stable main branch. |
|
||||
| Docker Hub | [docker.io/girlbossceo/conduwuit:main][dh] | ![Image Size][shield-main] | Stable main branch. |
|
||||
@@ -40,7 +40,6 @@ When you have the image you can simply run it with
|
||||
docker run -d -p 8448:6167 \
|
||||
-v db:/var/lib/conduwuit/ \
|
||||
-e CONDUWUIT_SERVER_NAME="your.server.name" \
|
||||
-e CONDUWUIT_DATABASE_BACKEND="rocksdb" \
|
||||
-e CONDUWUIT_ALLOW_REGISTRATION=false \
|
||||
--name conduit $LINK
|
||||
```
|
||||
@@ -93,16 +92,28 @@ Additional info about deploying conduwuit can be found [here](generic.md).
|
||||
|
||||
### Build
|
||||
|
||||
To build the conduwuit image with docker-compose, you first need to open and
|
||||
modify the `docker-compose.yml` file. There you need to comment the `image:`
|
||||
option and uncomment the `build:` option. Then call docker compose with:
|
||||
Official conduwuit images are built using Nix's
|
||||
[`buildLayeredImage`][nix-buildlayeredimage]. This ensures all OCI images are
|
||||
repeatable and reproducible by anyone, keeps the images lightweight, and can be
|
||||
built offline.
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
This also ensures portability of our images because `buildLayeredImage` builds
|
||||
OCI images, not Docker images, and works with other container software.
|
||||
|
||||
This will also start the container right afterwards, so if want it to run in
|
||||
detached mode, you also should use the `-d` flag.
|
||||
The OCI images are OS-less with only a very minimal environment of the `tini`
|
||||
init system, CA certificates, and the conduwuit binary. This does mean there is
|
||||
not a shell, but in theory you can get a shell by adding the necessary layers
|
||||
to the layered image. However it's very unlikely you will need a shell for any
|
||||
real troubleshooting.
|
||||
|
||||
The flake file for the OCI image definition is at [`nix/pkgs/oci-image/default.nix`][oci-image-def].
|
||||
|
||||
To build an OCI image using Nix, the following outputs can be built:
|
||||
- `nix build -L .#oci-image` (default features, x86_64 glibc)
|
||||
- `nix build -L .#oci-image-x86_64-linux-musl` (default features, x86_64 musl)
|
||||
- `nix build -L .#oci-image-aarch64-linux-musl` (default features, aarch64 musl)
|
||||
- `nix build -L .#oci-image-x86_64-linux-musl-all-features` (all features, x86_64 musl)
|
||||
- `nix build -L .#oci-image-aarch64-linux-musl-all-features` (all features, aarch64 musl)
|
||||
|
||||
### Run
|
||||
|
||||
@@ -137,3 +148,6 @@ those two files.
|
||||
## Voice communication
|
||||
|
||||
See the [TURN](../turn.md) page.
|
||||
|
||||
[nix-buildlayeredimage]: https://ryantm.github.io/nixpkgs/builders/images/dockertools/#ssec-pkgs-dockerTools-buildLayeredImage
|
||||
[oci-image-def]: https://github.com/girlbossceo/conduwuit/blob/main/nix/pkgs/oci-image/default.nix
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# conduwuit for FreeBSD
|
||||
|
||||
conduwuit at the moment does not provide FreeBSD builds. Building conduwuit on
|
||||
FreeBSD requires a specific environment variable to use the system prebuilt
|
||||
RocksDB library instead of rust-rocksdb / rust-librocksdb-sys which does *not*
|
||||
work and will cause a build error or coredump.
|
||||
conduwuit at the moment does not provide FreeBSD builds or have FreeBSD packaging, however conduwuit does build and work on FreeBSD using the system-provided RocksDB.
|
||||
|
||||
Use the following environment variable: `ROCKSDB_LIB_DIR=/usr/local/lib`
|
||||
|
||||
Such example commandline with it can be: `ROCKSDB_LIB_DIR=/usr/local/lib cargo
|
||||
build --release`
|
||||
Contributions for getting conduwuit packaged are welcome.
|
||||
|
||||
+38
-10
@@ -42,6 +42,9 @@ replace the binary / container image / etc.
|
||||
this will **NOT** work on conduwuit and you must configure delegation manually.
|
||||
This is not a mistake and no support for this feature will be added.
|
||||
|
||||
If you are using SQLite, you **MUST** migrate to RocksDB. You can use this
|
||||
tool to migrate from SQLite to RocksDB: <https://github.com/ShadowJonathan/conduit_toolbox/>
|
||||
|
||||
See the `[global.well_known]` config section, or configure your web server
|
||||
appropriately to send the delegation responses.
|
||||
|
||||
@@ -51,13 +54,13 @@ While conduwuit can run as any user it is better to use dedicated users for
|
||||
different services. This also allows you to make sure that the file permissions
|
||||
are correctly set up.
|
||||
|
||||
In Debian or Fedora/RHEL, you can use this command to create a conduwuit user:
|
||||
In Debian, you can use this command to create a conduwuit user:
|
||||
|
||||
```bash
|
||||
sudo adduser --system conduwuit --group --disabled-login --no-create-home
|
||||
```
|
||||
|
||||
For distros without `adduser`:
|
||||
For distros without `adduser` (or where it's a symlink to `useradd`):
|
||||
|
||||
```bash
|
||||
sudo useradd -r --shell /usr/bin/nologin --no-create-home conduwuit
|
||||
@@ -65,13 +68,25 @@ sudo useradd -r --shell /usr/bin/nologin --no-create-home conduwuit
|
||||
|
||||
## Forwarding ports in the firewall or the router
|
||||
|
||||
conduwuit uses the ports 443 and 8448 both of which need to be open in the
|
||||
firewall.
|
||||
Matrix's default federation port is port 8448, and clients must be using port 443.
|
||||
If you would like to use only port 443, or a different port, you will need to setup
|
||||
delegation. conduwuit has config options for doing delegation, or you can configure
|
||||
your reverse proxy to manually serve the necessary JSON files to do delegation
|
||||
(see the `[global.well_known]` config section).
|
||||
|
||||
If conduwuit runs behind a router or in a container and has a different public
|
||||
IP address than the host system these public ports need to be forwarded directly
|
||||
or indirectly to the port mentioned in the config.
|
||||
|
||||
Note for NAT users; if you have trouble connecting to your server from the inside
|
||||
of your network, you need to research your router and see if it supports "NAT
|
||||
hairpinning" or "NAT loopback".
|
||||
|
||||
If your router does not support this feature, you need to research doing local
|
||||
DNS overrides and force your Matrix DNS records to use your local IP internally.
|
||||
This can be done at the host level using `/etc/hosts`. If you need this to be
|
||||
on the network level, consider something like NextDNS or Pi-Hole.
|
||||
|
||||
## Setting up a systemd service
|
||||
|
||||
The systemd unit for conduwuit can be found
|
||||
@@ -119,12 +134,16 @@ is the recommended reverse proxy for new users and is very trivial to use
|
||||
(handles TLS, reverse proxy headers, etc transparently with proper defaults).
|
||||
|
||||
Lighttpd is not supported as it seems to mess with the `X-Matrix` Authorization
|
||||
header, making federation non-functional. If using Apache, you need to use
|
||||
`nocanon` in your `ProxyPass` directive to prevent this (note that Apache
|
||||
isn't very good as a general reverse proxy).
|
||||
header, making federation non-functional. If a workaround is found, feel free to share to get it added to the documentation here.
|
||||
|
||||
Nginx users may need to set `proxy_buffering off;` if there are issues with
|
||||
uploading media like images.
|
||||
If using Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent this (note that Apache isn't very good as a general reverse proxy and we discourage the usage of it if you can).
|
||||
|
||||
If using Nginx, you need to give conduwuit the request URI using `$request_uri`, or like so:
|
||||
- `proxy_pass http://127.0.0.1:6167$request_uri;`
|
||||
- `proxy_pass http://127.0.0.1:6167;`
|
||||
|
||||
Nginx users need to increase `client_max_body_size` (default is 1M) to match
|
||||
`max_request_size` defined in conduwuit.toml.
|
||||
|
||||
You will need to reverse proxy everything under following routes:
|
||||
- `/_matrix/` - core Matrix C-S and S-S APIs
|
||||
@@ -133,11 +152,20 @@ You will need to reverse proxy everything under following routes:
|
||||
|
||||
You can optionally reverse proxy the following individual routes:
|
||||
- `/.well-known/matrix/client` and `/.well-known/matrix/server` if using
|
||||
conduwuit to perform delegation
|
||||
conduwuit to perform delegation (see the `[global.well_known]` config section)
|
||||
- `/.well-known/matrix/support` if using conduwuit to send the homeserver admin
|
||||
contact and support page (formerly known as MSC1929)
|
||||
- `/` if you would like to see `hewwo from conduwuit woof!` at the root
|
||||
|
||||
See the following spec pages for more details on these files:
|
||||
- [`/.well-known/matrix/server`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixserver)
|
||||
- [`/.well-known/matrix/client`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient)
|
||||
- [`/.well-known/matrix/support`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixsupport)
|
||||
|
||||
Examples of delegation:
|
||||
- <https://puppygock.gay/.well-known/matrix/server>
|
||||
- <https://puppygock.gay/.well-known/matrix/client>
|
||||
|
||||
### Caddy
|
||||
|
||||
Create `/etc/caddy/conf.d/conduwuit_caddyfile` and enter this (substitute for
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# conduwuit for Kubernetes
|
||||
|
||||
conduwuit doesn't support horizontal scalability or distributed loading
|
||||
natively, however a community maintained Helm Chart is available here to run
|
||||
conduwuit on Kubernetes: <https://gitlab.cronce.io/charts/conduwuit>
|
||||
|
||||
Should changes need to be made, please reach out to the maintainer in our
|
||||
Matrix room as this is not maintained/controlled by the conduwuit maintainers.
|
||||
+30
-5
@@ -39,6 +39,15 @@ The `flake.nix` and `default.nix` do not currently provide a NixOS module (contr
|
||||
welcome!), so [`services.matrix-conduit`][module] from Nixpkgs can be used to configure
|
||||
conduwuit.
|
||||
|
||||
### Conduit NixOS Config Module and SQLite
|
||||
|
||||
Beware! The [`services.matrix-conduit`][module] module defaults to SQLite as a database backend.
|
||||
Conduwuit dropped SQLite support in favor of exclusively supporting the much faster RocksDB.
|
||||
Make sure that you are using the RocksDB backend before migrating!
|
||||
|
||||
There is a [tool to migrate a Conduit SQLite database to
|
||||
RocksDB](https://github.com/ShadowJonathan/conduit_toolbox/).
|
||||
|
||||
If you want to run the latest code, you should get conduwuit from the `flake.nix`
|
||||
or `default.nix` and set [`services.matrix-conduit.package`][package]
|
||||
appropriately to use conduwuit instead of Conduit.
|
||||
@@ -46,15 +55,31 @@ appropriately to use conduwuit instead of Conduit.
|
||||
### UNIX sockets
|
||||
|
||||
Due to the lack of a conduwuit NixOS module, when using the `services.matrix-conduit` module
|
||||
it is not possible to use UNIX sockets. This is because the UNIX socket option does not exist
|
||||
in Conduit, and their module forces listening on `[::1]:6167` by default if unspecified.
|
||||
a workaround like the one below is necessary to use UNIX sockets. This is because the UNIX
|
||||
socket option does not exist in Conduit, and the module forcibly sets the `address` and
|
||||
`port` config options.
|
||||
|
||||
```nix
|
||||
options.services.matrix-conduit.settings = lib.mkOption {
|
||||
apply = old: old // (
|
||||
if (old.global ? "unix_socket_path")
|
||||
then { global = builtins.removeAttrs old.global [ "address" "port" ]; }
|
||||
else { }
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
Additionally, the [`matrix-conduit` systemd unit][systemd-unit] in the module does not allow
|
||||
the `AF_UNIX` socket address family in their systemd unit's `RestrictAddressFamilies=` which
|
||||
disallows the namespace from accessing or creating UNIX sockets.
|
||||
disallows the namespace from accessing or creating UNIX sockets and has to be enabled like so:
|
||||
|
||||
There is no known workaround these. A conduwuit NixOS configuration module must be developed and
|
||||
published by the community.
|
||||
```nix
|
||||
systemd.services.conduit.serviceConfig.RestrictAddressFamilies = [ "AF_UNIX" ];
|
||||
```
|
||||
|
||||
Even though those workarounds are feasible a conduwuit NixOS configuration module, developed and
|
||||
published by the community, would be appreciated.
|
||||
|
||||
### jemalloc and hardened profile
|
||||
|
||||
|
||||
+14
-10
@@ -75,21 +75,21 @@ development (unresponsive or slow upstream), conduwuit-specific usecases, or
|
||||
lack of time to upstream some things.
|
||||
|
||||
- [ruma/ruma][1]: <https://github.com/girlbossceo/ruwuma> - various performance
|
||||
improvements, more features, faster-paced development, client/server interop
|
||||
improvements, more features, faster-paced development, better client/server interop
|
||||
hacks upstream won't accept, etc
|
||||
- [facebook/rocksdb][2]: <https://github.com/girlbossceo/rocksdb> - liburing
|
||||
build fixes, GCC build fix, and logging callback C API for Rust tracing
|
||||
integration
|
||||
build fixes and GCC debug build fix
|
||||
- [tikv/jemallocator][3]: <https://github.com/girlbossceo/jemallocator> - musl
|
||||
builds seem to be broken on upstream
|
||||
builds seem to be broken on upstream, fixes some broken/suspicious code in
|
||||
places, additional safety measures, and support redzones for Valgrind
|
||||
- [zyansheep/rustyline-async][4]:
|
||||
<https://github.com/girlbossceo/rustyline-async> - tab completion callback and
|
||||
`CTRL+\` signal quit event for CLI
|
||||
`CTRL+\` signal quit event for conduwuit console CLI
|
||||
- [rust-rocksdb/rust-rocksdb][5]:
|
||||
<https://github.com/girlbossceo/rust-rocksdb-zaidoon1> - [`@zaidoon1`'s][8] fork
|
||||
has quicker updates, more up to date dependencies. Our changes fix musl build
|
||||
issues, Rust part of the logging callback C API, removes unnecessary `gtest`
|
||||
include, and uses our RocksDB and jemallocator
|
||||
<https://github.com/girlbossceo/rust-rocksdb-zaidoon1> - [`@zaidoon1`][8]'s fork
|
||||
has quicker updates, more up to date dependencies, etc. Our fork fixes musl build
|
||||
issues, removes unnecessary `gtest` include, and uses our RocksDB and jemallocator
|
||||
forks.
|
||||
- [tokio-rs/tracing][6]: <https://github.com/girlbossceo/tracing> - Implements
|
||||
`Clone` for `EnvFilter` to support dynamically changing tracing envfilter's
|
||||
alongside other logging/metrics things
|
||||
@@ -103,12 +103,16 @@ tokio_unstable` flag to enable experimental tokio APIs. A build might look like
|
||||
this:
|
||||
|
||||
```bash
|
||||
RUSTFLAGS="--cfg tokio_unstable" cargo build \
|
||||
RUSTFLAGS="--cfg tokio_unstable" cargo +nightly build \
|
||||
--release \
|
||||
--no-default-features \
|
||||
--features=systemd,element_hacks,gzip_compression,brotli_compression,zstd_compression,tokio_console
|
||||
```
|
||||
|
||||
You will also need to enable the `tokio_console` config option in conduwuit when
|
||||
starting it. This was due to tokio-console causing gradual memory leak/usage
|
||||
if left enabled.
|
||||
|
||||
[1]: https://github.com/ruma/ruma/
|
||||
[2]: https://github.com/facebook/rocksdb/
|
||||
[3]: https://github.com/tikv/jemallocator/
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
Have a look at [Complement's repository][complement] for an explanation of what
|
||||
it is.
|
||||
|
||||
To test against Complement, with Nix (or [Lix](https://lix.systems) and direnv installed
|
||||
and set up, you can:
|
||||
To test against Complement, with Nix (or [Lix](https://lix.systems) and direnv
|
||||
installed and set up, you can:
|
||||
|
||||
* Run `./bin/complement "$COMPLEMENT_SRC" ./path/to/logs.jsonl
|
||||
./path/to/results.jsonl` to build a Complement image, run the tests, and output
|
||||
|
||||
+1
-2
@@ -241,8 +241,7 @@ both new users and power users
|
||||
- Fixed every single clippy (default lints) and rustc warnings, including some
|
||||
that were performance related or potential safety issues / unsoundness
|
||||
- Add a **lot** of other clippy and rustc lints and a rustfmt.toml file
|
||||
- Repo uses [Renovate](https://docs.renovatebot.com/),
|
||||
[Trivy](https://github.com/aquasecurity/trivy-action), and keeps ALL
|
||||
- Repo uses [Renovate](https://docs.renovatebot.com/) and keeps ALL
|
||||
dependencies as up to date as possible
|
||||
- Purge unmaintained/irrelevant/broken database backends (heed, sled, persy) and
|
||||
other unnecessary code or overhead
|
||||
|
||||
+37
-14
@@ -47,10 +47,11 @@ and communicate with your host's DNS servers (host's `/etc/resolv.conf`)
|
||||
|
||||
Some filesystems may not like RocksDB using [Direct
|
||||
IO](https://github.com/facebook/rocksdb/wiki/Direct-IO). Direct IO is for
|
||||
non-buffered I/O which improves conduwuit performance, but at least FUSE is a
|
||||
filesystem potentially known to not like this. See the [example
|
||||
config](configuration/examples.md) for disabling it if needed. Issues from
|
||||
Direct IO on unsupported filesystems are usually shown as startup errors.
|
||||
non-buffered I/O which improves conduwuit performance and reduces system CPU
|
||||
usage, but at least FUSE and possibly ZFS are filesystems potentially known
|
||||
to not like this. See the [example config](configuration/examples.md) for
|
||||
disabling it if needed. Issues from Direct IO on unsupported filesystems are
|
||||
usually shown as startup errors.
|
||||
|
||||
#### Database corruption
|
||||
|
||||
@@ -105,24 +106,46 @@ Various debug commands can be found in `!admin debug`.
|
||||
|
||||
#### Debug/Trace log level
|
||||
|
||||
conduwuit builds without debug or trace log levels by default for at least
|
||||
performance reasons. This may change in the future and/or binaries providing
|
||||
such configurations may be provided. If you need to access debug/trace log
|
||||
levels, you will need to build without the `release_max_log_level` feature.
|
||||
conduwuit builds without debug or trace log levels at compile time by default
|
||||
for substantial performance gains in CPU usage and improved compile times. If
|
||||
you need to access debug/trace log levels, you will need to build without the
|
||||
`release_max_log_level` feature or use our provided static debug binaries.
|
||||
|
||||
#### Changing log level dynamically
|
||||
|
||||
conduwuit supports changing the tracing log environment filter on-the-fly using
|
||||
the admin command `!admin debug change-log-level`. This accepts a string
|
||||
**without quotes** the same format as the `log` config option.
|
||||
the admin command `!admin debug change-log-level <log env filter>`. This accepts
|
||||
a string **without quotes** the same format as the `log` config option.
|
||||
|
||||
Example: `!admin debug change-log-level debug`
|
||||
|
||||
This can also accept complex filters such as:
|
||||
`!admin debug change-log-level info,conduit_service[{dest="example.com"}]=trace,ruma_state_res=trace`
|
||||
`!admin debug change-log-level info,conduit_service[{dest="example.com"}]=trace,conduit_service[send{dest="example.org"}]=trace`
|
||||
|
||||
And to reset the log level to the one that was set at startup / last config
|
||||
load, simply pass the `--reset` flag.
|
||||
|
||||
`!admin debug change-log-level --reset`
|
||||
|
||||
#### Pinging servers
|
||||
|
||||
conduwuit can ping other servers using `!admin debug ping`. This takes a server
|
||||
name and goes through the server discovery process and queries
|
||||
conduwuit can ping other servers using `!admin debug ping <server>`. This takes
|
||||
a server name and goes through the server discovery process and queries
|
||||
`/_matrix/federation/v1/version`. Errors are outputted.
|
||||
|
||||
While it does measure the latency of the request, it is not indicative of
|
||||
server performance on either side as that endpoint is completely unauthenticated
|
||||
and simply fetches a string on a static JSON endpoint. It is very low cost both
|
||||
bandwidth and computationally.
|
||||
|
||||
#### Allocator memory stats
|
||||
|
||||
When using jemalloc with jemallocator's `stats` feature, you can see conduwuit's
|
||||
jemalloc memory stats by using `!admin debug memory-stats`
|
||||
When using jemalloc with jemallocator's `stats` feature (`--enable-stats`), you
|
||||
can see conduwuit's high-level allocator stats by using
|
||||
`!admin server memory-usage` at the bottom.
|
||||
|
||||
If you are a developer, you can also view the raw jemalloc statistics with
|
||||
`!admin debug memory-stats`. Please note that this output is extremely large
|
||||
which may only be visible in the conduwuit console CLI due to PDU size limits,
|
||||
and is not easy for non-developers to understand.
|
||||
|
||||
+10
@@ -188,6 +188,16 @@ cargo test \
|
||||
--color=always
|
||||
"""
|
||||
|
||||
# Checks if the generated example config differs from the checked in repo's
|
||||
# example config.
|
||||
[[task]]
|
||||
name = "example-config"
|
||||
group = "tests"
|
||||
depends = ["cargo/default"]
|
||||
script = """
|
||||
git diff --exit-code conduwuit-example.toml
|
||||
"""
|
||||
|
||||
# Ensure that the flake's default output can build and run without crashing
|
||||
#
|
||||
# This is a dynamically-linked jemalloc build, which is a case not covered by
|
||||
|
||||
Generated
+4
-4
@@ -922,16 +922,16 @@
|
||||
"rocksdb": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1729712930,
|
||||
"narHash": "sha256-jlp4kPkRTpoJaUdobEoHd8rCGAQNBy4ZHZ6y5zL/ibw=",
|
||||
"lastModified": 1731690620,
|
||||
"narHash": "sha256-Xd4TJYqPERMJLXaGa6r6Ny1Wlw8Uy5Cyf/8q7nS58QM=",
|
||||
"owner": "girlbossceo",
|
||||
"repo": "rocksdb",
|
||||
"rev": "871eda6953c3f399aae39808dcfccdd014885beb",
|
||||
"rev": "292446aa2bc41699204d817a1e4b091679a886eb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "girlbossceo",
|
||||
"ref": "v9.7.3",
|
||||
"ref": "v9.7.4",
|
||||
"repo": "rocksdb",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
flake-utils.url = "github:numtide/flake-utils?ref=main";
|
||||
nix-filter.url = "github:numtide/nix-filter?ref=main";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs?ref=nixpkgs-unstable";
|
||||
rocksdb = { url = "github:girlbossceo/rocksdb?ref=v9.7.3"; flake = false; };
|
||||
rocksdb = { url = "github:girlbossceo/rocksdb?ref=v9.7.4"; flake = false; };
|
||||
liburing = { url = "github:axboe/liburing?ref=master"; flake = false; };
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
file = ./rust-toolchain.toml;
|
||||
|
||||
# See also `rust-toolchain.toml`
|
||||
sha256 = "sha256-yMuSb5eQPO/bHv+Bcf/US8LVMbf/G/0MSfiPwBhiPpk=";
|
||||
sha256 = "sha256-s1RPtyvDGJaX/BisLT+ifVfuhDT1nZkZ1NcK8sbwELM=";
|
||||
};
|
||||
|
||||
mkScope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
|
||||
|
||||
@@ -25,6 +25,7 @@ let
|
||||
"tokio_console"
|
||||
# sentry telemetry isn't useful for complement, disabled by default anyways
|
||||
"sentry_telemetry"
|
||||
"perf_measurements"
|
||||
# the containers don't use or need systemd signal support
|
||||
"systemd"
|
||||
# this is non-functional on nix for some reason
|
||||
@@ -96,6 +97,7 @@ dockerTools.buildImage {
|
||||
Env = [
|
||||
"SSL_CERT_FILE=/complement/ca/ca.crt"
|
||||
"CONDUWUIT_CONFIG=${./config.toml}"
|
||||
"RUST_BACKTRACE=full"
|
||||
];
|
||||
|
||||
ExposedPorts = {
|
||||
|
||||
@@ -176,7 +176,7 @@ commonAttrs = {
|
||||
#
|
||||
# <https://github.com/input-output-hk/haskell.nix/issues/829>
|
||||
postInstall = with pkgsBuildHost; ''
|
||||
find "$out" -type f -exec remove-references-to -t ${stdenv.cc} -t ${gcc} -t ${rustc.unwrapped} -t ${rustc} -t ${libidn2} -t ${libunistring} '{}' +
|
||||
find "$out" -type f -exec remove-references-to -t ${stdenv.cc} -t ${gcc} -t ${libgcc} -t ${llvm} -t ${libllvm} -t ${rustc.unwrapped} -t ${rustc} -t ${libidn2} -t ${libunistring} '{}' +
|
||||
'';
|
||||
};
|
||||
in
|
||||
|
||||
@@ -14,6 +14,7 @@ dockerTools.buildLayeredImage {
|
||||
created = "@${toString inputs.self.lastModified}";
|
||||
contents = [
|
||||
dockerTools.caCertificates
|
||||
main
|
||||
];
|
||||
config = {
|
||||
Entrypoint = if !stdenv.hostPlatform.isDarwin
|
||||
@@ -24,5 +25,8 @@ dockerTools.buildLayeredImage {
|
||||
Cmd = [
|
||||
"${lib.getExe main}"
|
||||
];
|
||||
Env = [
|
||||
"RUST_BACKTRACE=full"
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
# If you're having trouble making the relevant changes, bug a maintainer.
|
||||
|
||||
[toolchain]
|
||||
channel = "1.82.0"
|
||||
channel = "1.83.0"
|
||||
profile = "minimal"
|
||||
components = [
|
||||
# For rust-analyzer
|
||||
|
||||
+14
-15
@@ -1,28 +1,27 @@
|
||||
edition = "2021"
|
||||
|
||||
array_width = 80
|
||||
chain_width = 60
|
||||
comment_width = 80
|
||||
condense_wildcard_suffixes = true
|
||||
edition = "2021"
|
||||
fn_call_width = 80
|
||||
fn_params_layout = "Compressed"
|
||||
fn_single_line = true
|
||||
format_code_in_doc_comments = true
|
||||
format_macro_bodies = true
|
||||
format_macro_matchers = true
|
||||
format_strings = true
|
||||
hex_literal_case = "Upper"
|
||||
max_width = 120
|
||||
tab_spaces = 4
|
||||
array_width = 80
|
||||
comment_width = 80
|
||||
wrap_comments = true
|
||||
fn_params_layout = "Compressed"
|
||||
fn_call_width = 80
|
||||
fn_single_line = true
|
||||
group_imports = "StdExternalCrate"
|
||||
hard_tabs = true
|
||||
match_block_trailing_comma = true
|
||||
hex_literal_case = "Upper"
|
||||
imports_granularity = "Crate"
|
||||
match_block_trailing_comma = true
|
||||
max_width = 120
|
||||
newline_style = "Unix"
|
||||
normalize_comments = false
|
||||
reorder_impl_items = true
|
||||
reorder_imports = true
|
||||
group_imports = "StdExternalCrate"
|
||||
newline_style = "Unix"
|
||||
tab_spaces = 4
|
||||
use_field_init_shorthand = true
|
||||
use_small_heuristics = "Off"
|
||||
use_try_shorthand = true
|
||||
chain_width = 60
|
||||
wrap_comments = true
|
||||
|
||||
@@ -29,10 +29,11 @@ release_max_log_level = [
|
||||
clap.workspace = true
|
||||
conduit-api.workspace = true
|
||||
conduit-core.workspace = true
|
||||
conduit-database.workspace = true
|
||||
conduit-macros.workspace = true
|
||||
conduit-service.workspace = true
|
||||
const-str.workspace = true
|
||||
futures-util.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
ruma.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ use crate::{
|
||||
};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "admin", version = env!("CARGO_PKG_VERSION"))]
|
||||
#[command(name = "conduwuit", version = conduit::version())]
|
||||
pub(super) enum AdminCommand {
|
||||
#[command(subcommand)]
|
||||
/// - Commands for managing appservices
|
||||
|
||||
@@ -11,19 +11,25 @@ pub(super) async fn register(&self) -> Result<RoomMessageEventContent> {
|
||||
));
|
||||
}
|
||||
|
||||
let appservice_config = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
|
||||
let parsed_config = serde_yaml::from_str::<Registration>(&appservice_config);
|
||||
let appservice_config_body = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
|
||||
let parsed_config = serde_yaml::from_str::<Registration>(&appservice_config_body);
|
||||
match parsed_config {
|
||||
Ok(yaml) => match self.services.appservice.register_appservice(yaml).await {
|
||||
Ok(id) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Appservice registered with ID: {id}."
|
||||
Ok(registration) => match self
|
||||
.services
|
||||
.appservice
|
||||
.register_appservice(®istration, &appservice_config_body)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Appservice registered with ID: {}",
|
||||
registration.id
|
||||
))),
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to register appservice: {e}"
|
||||
))),
|
||||
},
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Could not parse appservice config: {e}"
|
||||
"Could not parse appservice config as YAML: {e}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use conduit::Result;
|
||||
use conduit_macros::implement;
|
||||
use futures::StreamExt;
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
|
||||
use crate::Command;
|
||||
@@ -10,14 +11,12 @@ use crate::Command;
|
||||
#[implement(Command, params = "<'_>")]
|
||||
pub(super) async fn check_all_users(&self) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = self.services.users.db.iter();
|
||||
let users = self.services.users.iter().collect::<Vec<_>>().await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
let users = results.collect::<Vec<_>>();
|
||||
|
||||
let total = users.len();
|
||||
let err_count = users.iter().filter(|user| user.is_err()).count();
|
||||
let ok_count = users.iter().filter(|user| user.is_ok()).count();
|
||||
let err_count = users.iter().filter(|_user| false).count();
|
||||
let ok_count = users.iter().filter(|_user| true).count();
|
||||
|
||||
let message = format!(
|
||||
"Database query completed in {query_time:?}:\n\n```\nTotal entries: {total:?}\nFailure/Invalid user count: \
|
||||
|
||||
+167
-139
@@ -1,18 +1,19 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
collections::HashMap,
|
||||
fmt::Write,
|
||||
iter::once,
|
||||
sync::Arc,
|
||||
time::{Instant, SystemTime},
|
||||
};
|
||||
|
||||
use api::client::validate_and_add_event_id;
|
||||
use conduit::{debug, debug_error, err, info, trace, utils, warn, Error, PduEvent, Result};
|
||||
use conduit::{debug_error, err, info, trace, utils, utils::string::EMPTY, warn, Error, PduEvent, Result};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::{
|
||||
api::{client::error::ErrorKind, federation::event::get_room_state},
|
||||
events::room::message::RoomMessageEventContent,
|
||||
CanonicalJsonObject, EventId, OwnedRoomOrAliasId, RoomId, RoomVersionId, ServerName,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
use service::rooms::state_compressor::HashSetCompressStateEvent;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use crate::admin_command;
|
||||
@@ -26,37 +27,39 @@ pub(super) async fn echo(&self, message: Vec<String>) -> Result<RoomMessageEvent
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn get_auth_chain(&self, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
|
||||
let event_id = Arc::<EventId>::from(event_id);
|
||||
if let Some(event) = self.services.rooms.timeline.get_pdu_json(&event_id)? {
|
||||
let room_id_str = event
|
||||
.get("room_id")
|
||||
.and_then(|val| val.as_str())
|
||||
.ok_or_else(|| Error::bad_database("Invalid event in database"))?;
|
||||
let Ok(event) = self.services.rooms.timeline.get_pdu_json(&event_id).await else {
|
||||
return Ok(RoomMessageEventContent::notice_plain("Event not found."));
|
||||
};
|
||||
|
||||
let room_id = <&RoomId>::try_from(room_id_str)
|
||||
.map_err(|_| Error::bad_database("Invalid room id field in event in database"))?;
|
||||
let room_id_str = event
|
||||
.get("room_id")
|
||||
.and_then(|val| val.as_str())
|
||||
.ok_or_else(|| Error::bad_database("Invalid event in database"))?;
|
||||
|
||||
let start = Instant::now();
|
||||
let count = self
|
||||
.services
|
||||
.rooms
|
||||
.auth_chain
|
||||
.event_ids_iter(room_id, vec![event_id])
|
||||
.await?
|
||||
.count();
|
||||
let room_id = <&RoomId>::try_from(room_id_str)
|
||||
.map_err(|_| Error::bad_database("Invalid room id field in event in database"))?;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Loaded auth chain with length {count} in {elapsed:?}"
|
||||
)))
|
||||
} else {
|
||||
Ok(RoomMessageEventContent::text_plain("Event not found."))
|
||||
}
|
||||
let start = Instant::now();
|
||||
let count = self
|
||||
.services
|
||||
.rooms
|
||||
.auth_chain
|
||||
.event_ids_iter(room_id, once(event_id.as_ref()))
|
||||
.await?
|
||||
.count()
|
||||
.await;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Loaded auth chain with length {count} in {elapsed:?}"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn parse_pdu(&self) -> Result<RoomMessageEventContent> {
|
||||
if self.body.len() < 2 || !self.body[0].trim().starts_with("```") || self.body.last().unwrap_or(&"").trim() != "```"
|
||||
if self.body.len() < 2
|
||||
|| !self.body[0].trim().starts_with("```")
|
||||
|| self.body.last().unwrap_or(&EMPTY).trim() != "```"
|
||||
{
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
@@ -91,25 +94,28 @@ pub(super) async fn get_pdu(&self, event_id: Box<EventId>) -> Result<RoomMessage
|
||||
.services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_non_outlier_pdu_json(&event_id)?;
|
||||
if pdu_json.is_none() {
|
||||
.get_non_outlier_pdu_json(&event_id)
|
||||
.await;
|
||||
|
||||
if pdu_json.is_err() {
|
||||
outlier = true;
|
||||
pdu_json = self.services.rooms.timeline.get_pdu_json(&event_id)?;
|
||||
pdu_json = self.services.rooms.timeline.get_pdu_json(&event_id).await;
|
||||
}
|
||||
|
||||
match pdu_json {
|
||||
Some(json) => {
|
||||
Ok(json) => {
|
||||
let json_text = serde_json::to_string_pretty(&json).expect("canonical json is valid json");
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"{}\n```json\n{}\n```",
|
||||
if outlier {
|
||||
"Outlier PDU found in our database"
|
||||
"Outlier (Rejected / Soft Failed) PDU found in our database"
|
||||
} else {
|
||||
"PDU found in our database"
|
||||
},
|
||||
json_text
|
||||
)))
|
||||
},
|
||||
None => Ok(RoomMessageEventContent::text_plain("PDU not found locally.")),
|
||||
Err(_) => Ok(RoomMessageEventContent::text_plain("PDU not found locally.")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +136,9 @@ pub(super) async fn get_remote_pdu_list(
|
||||
));
|
||||
}
|
||||
|
||||
if self.body.len() < 2 || !self.body[0].trim().starts_with("```") || self.body.last().unwrap_or(&"").trim() != "```"
|
||||
if self.body.len() < 2
|
||||
|| !self.body[0].trim().starts_with("```")
|
||||
|| self.body.last().unwrap_or(&EMPTY).trim() != "```"
|
||||
{
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
@@ -157,7 +165,8 @@ pub(super) async fn get_remote_pdu_list(
|
||||
.send_message(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to get remote PDU, ignoring error: {e}"
|
||||
)))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
warn!("Failed to get remote PDU, ignoring error: {e}");
|
||||
} else {
|
||||
success_count = success_count.saturating_add(1);
|
||||
@@ -196,6 +205,7 @@ pub(super) async fn get_remote_pdu(
|
||||
&server,
|
||||
ruma::api::federation::event::get_event::v1::Request {
|
||||
event_id: event_id.clone().into(),
|
||||
include_unredacted_content: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -210,12 +220,14 @@ pub(super) async fn get_remote_pdu(
|
||||
})?;
|
||||
|
||||
trace!("Attempting to parse PDU: {:?}", &response.pdu);
|
||||
let parsed_pdu = {
|
||||
let _parsed_pdu = {
|
||||
let parsed_result = self
|
||||
.services
|
||||
.rooms
|
||||
.event_handler
|
||||
.parse_incoming_pdu(&response.pdu);
|
||||
.parse_incoming_pdu(&response.pdu)
|
||||
.await;
|
||||
|
||||
let (event_id, value, room_id) = match parsed_result {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
@@ -230,22 +242,12 @@ pub(super) async fn get_remote_pdu(
|
||||
vec![(event_id, value, room_id)]
|
||||
};
|
||||
|
||||
let pub_key_map = RwLock::new(BTreeMap::new());
|
||||
|
||||
debug!("Attempting to fetch homeserver signing keys for {server}");
|
||||
self.services
|
||||
.server_keys
|
||||
.fetch_required_signing_keys(parsed_pdu.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("Could not fetch all signatures for PDUs from {server}: {e:?}");
|
||||
});
|
||||
|
||||
info!("Attempting to handle event ID {event_id} as backfilled PDU");
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.backfill_pdu(&server, response.pdu, &pub_key_map)
|
||||
.backfill_pdu(&server, response.pdu)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
let json_text = serde_json::to_string_pretty(&json).expect("canonical json is valid json");
|
||||
@@ -264,15 +266,15 @@ pub(super) async fn get_remote_pdu(
|
||||
#[admin_command]
|
||||
pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result<RoomMessageEventContent> {
|
||||
let room_id = self.services.rooms.alias.resolve(&room).await?;
|
||||
let room_state = self
|
||||
let room_state: Vec<_> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full(&room_id)
|
||||
.await?
|
||||
.values()
|
||||
.map(|pdu| pdu.to_state_event())
|
||||
.collect::<Vec<_>>();
|
||||
.map(PduEvent::to_state_event)
|
||||
.collect();
|
||||
|
||||
if room_state.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
@@ -333,9 +335,12 @@ pub(super) async fn ping(&self, server: Box<ServerName>) -> Result<RoomMessageEv
|
||||
#[admin_command]
|
||||
pub(super) async fn force_device_list_updates(&self) -> Result<RoomMessageEventContent> {
|
||||
// Force E2EE device list updates for all users
|
||||
for user_id in self.services.users.iter().filter_map(Result::ok) {
|
||||
self.services.users.mark_device_key_update(&user_id)?;
|
||||
}
|
||||
self.services
|
||||
.users
|
||||
.stream()
|
||||
.for_each(|user_id| self.services.users.mark_device_key_update(user_id))
|
||||
.await;
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(
|
||||
"Marked all devices for all users as having new keys to update",
|
||||
))
|
||||
@@ -419,12 +424,10 @@ pub(super) async fn sign_json(&self) -> Result<RoomMessageEventContent> {
|
||||
let string = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
|
||||
match serde_json::from_str(&string) {
|
||||
Ok(mut value) => {
|
||||
ruma::signatures::sign_json(
|
||||
self.services.globals.server_name().as_str(),
|
||||
self.services.globals.keypair(),
|
||||
&mut value,
|
||||
)
|
||||
.expect("our request json is what ruma expects");
|
||||
self.services
|
||||
.server_keys
|
||||
.sign_json(&mut value)
|
||||
.expect("our request json is what ruma expects");
|
||||
let json_text = serde_json::to_string_pretty(&value).expect("canonical json is valid json");
|
||||
Ok(RoomMessageEventContent::text_plain(json_text))
|
||||
},
|
||||
@@ -442,27 +445,31 @@ pub(super) async fn verify_json(&self) -> Result<RoomMessageEventContent> {
|
||||
}
|
||||
|
||||
let string = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
|
||||
match serde_json::from_str(&string) {
|
||||
Ok(value) => {
|
||||
let pub_key_map = RwLock::new(BTreeMap::new());
|
||||
|
||||
self.services
|
||||
.server_keys
|
||||
.fetch_required_signing_keys([&value], &pub_key_map)
|
||||
.await?;
|
||||
|
||||
let pub_key_map = pub_key_map.read().await;
|
||||
match ruma::signatures::verify_json(&pub_key_map, &value) {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Signature correct")),
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Signature verification failed: {e}"
|
||||
))),
|
||||
}
|
||||
match serde_json::from_str::<CanonicalJsonObject>(&string) {
|
||||
Ok(value) => match self.services.server_keys.verify_json(&value, None).await {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Signature correct")),
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Signature verification failed: {e}"
|
||||
))),
|
||||
},
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn verify_pdu(&self, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
|
||||
let mut event = self.services.rooms.timeline.get_pdu_json(&event_id).await?;
|
||||
|
||||
event.remove("event_id");
|
||||
let msg = match self.services.server_keys.verify_event(&event, None).await {
|
||||
Ok(ruma::signatures::Verified::Signatures) => "signatures OK, but content hash failed (redaction).",
|
||||
Ok(ruma::signatures::Verified::All) => "signatures and hashes OK.",
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
Ok(RoomMessageEventContent::notice_plain(msg))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub(super) async fn first_pdu_in_room(&self, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
|
||||
@@ -470,7 +477,8 @@ pub(super) async fn first_pdu_in_room(&self, room_id: Box<RoomId>) -> Result<Roo
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(&self.services.globals.config.server_name, &room_id)?
|
||||
.server_in_room(&self.services.globals.config.server_name, &room_id)
|
||||
.await
|
||||
{
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"We are not participating in the room / we don't know about the room ID.",
|
||||
@@ -481,8 +489,9 @@ pub(super) async fn first_pdu_in_room(&self, room_id: Box<RoomId>) -> Result<Roo
|
||||
.services
|
||||
.rooms
|
||||
.timeline
|
||||
.first_pdu_in_room(&room_id)?
|
||||
.ok_or_else(|| Error::bad_database("Failed to find the first PDU in database"))?;
|
||||
.first_pdu_in_room(&room_id)
|
||||
.await
|
||||
.map_err(|_| Error::bad_database("Failed to find the first PDU in database"))?;
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!("{first_pdu:?}")))
|
||||
}
|
||||
@@ -494,7 +503,8 @@ pub(super) async fn latest_pdu_in_room(&self, room_id: Box<RoomId>) -> Result<Ro
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(&self.services.globals.config.server_name, &room_id)?
|
||||
.server_in_room(&self.services.globals.config.server_name, &room_id)
|
||||
.await
|
||||
{
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"We are not participating in the room / we don't know about the room ID.",
|
||||
@@ -505,8 +515,9 @@ pub(super) async fn latest_pdu_in_room(&self, room_id: Box<RoomId>) -> Result<Ro
|
||||
.services
|
||||
.rooms
|
||||
.timeline
|
||||
.latest_pdu_in_room(&room_id)?
|
||||
.ok_or_else(|| Error::bad_database("Failed to find the latest PDU in database"))?;
|
||||
.latest_pdu_in_room(&room_id)
|
||||
.await
|
||||
.map_err(|_| Error::bad_database("Failed to find the latest PDU in database"))?;
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!("{latest_pdu:?}")))
|
||||
}
|
||||
@@ -520,7 +531,8 @@ pub(super) async fn force_set_room_state_from_server(
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(&self.services.globals.config.server_name, &room_id)?
|
||||
.server_in_room(&self.services.globals.config.server_name, &room_id)
|
||||
.await
|
||||
{
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"We are not participating in the room / we don't know about the room ID.",
|
||||
@@ -531,13 +543,13 @@ pub(super) async fn force_set_room_state_from_server(
|
||||
.services
|
||||
.rooms
|
||||
.timeline
|
||||
.latest_pdu_in_room(&room_id)?
|
||||
.ok_or_else(|| Error::bad_database("Failed to find the latest PDU in database"))?;
|
||||
.latest_pdu_in_room(&room_id)
|
||||
.await
|
||||
.map_err(|_| Error::bad_database("Failed to find the latest PDU in database"))?;
|
||||
|
||||
let room_version = self.services.rooms.state.get_room_version(&room_id)?;
|
||||
let room_version = self.services.rooms.state.get_room_version(&room_id).await?;
|
||||
|
||||
let mut state: HashMap<u64, Arc<EventId>> = HashMap::new();
|
||||
let pub_key_map = RwLock::new(BTreeMap::new());
|
||||
|
||||
let remote_state_response = self
|
||||
.services
|
||||
@@ -551,30 +563,28 @@ pub(super) async fn force_set_room_state_from_server(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut events = Vec::with_capacity(remote_state_response.pdus.len());
|
||||
|
||||
for pdu in remote_state_response.pdus.clone() {
|
||||
events.push(match self.services.rooms.event_handler.parse_incoming_pdu(&pdu) {
|
||||
match self
|
||||
.services
|
||||
.rooms
|
||||
.event_handler
|
||||
.parse_incoming_pdu(&pdu)
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
warn!("Could not parse PDU, ignoring: {e}");
|
||||
continue;
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
info!("Fetching required signing keys for all the state events we got");
|
||||
self.services
|
||||
.server_keys
|
||||
.fetch_required_signing_keys(events.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map)
|
||||
.await?;
|
||||
|
||||
info!("Going through room_state response PDUs");
|
||||
for result in remote_state_response
|
||||
.pdus
|
||||
.iter()
|
||||
.map(|pdu| validate_and_add_event_id(self.services, pdu, &room_version, &pub_key_map))
|
||||
{
|
||||
for result in remote_state_response.pdus.iter().map(|pdu| {
|
||||
self.services
|
||||
.server_keys
|
||||
.validate_and_add_event_id(pdu, &room_version)
|
||||
}) {
|
||||
let Ok((event_id, value)) = result.await else {
|
||||
continue;
|
||||
};
|
||||
@@ -587,23 +597,26 @@ pub(super) async fn force_set_room_state_from_server(
|
||||
self.services
|
||||
.rooms
|
||||
.outlier
|
||||
.add_pdu_outlier(&event_id, &value)?;
|
||||
.add_pdu_outlier(&event_id, &value);
|
||||
|
||||
if let Some(state_key) = &pdu.state_key {
|
||||
let shortstatekey = self
|
||||
.services
|
||||
.rooms
|
||||
.short
|
||||
.get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key)?;
|
||||
.get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key)
|
||||
.await;
|
||||
|
||||
state.insert(shortstatekey, pdu.event_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
info!("Going through auth_chain response");
|
||||
for result in remote_state_response
|
||||
.auth_chain
|
||||
.iter()
|
||||
.map(|pdu| validate_and_add_event_id(self.services, pdu, &room_version, &pub_key_map))
|
||||
{
|
||||
for result in remote_state_response.auth_chain.iter().map(|pdu| {
|
||||
self.services
|
||||
.server_keys
|
||||
.validate_and_add_event_id(pdu, &room_version)
|
||||
}) {
|
||||
let Ok((event_id, value)) = result.await else {
|
||||
continue;
|
||||
};
|
||||
@@ -611,7 +624,7 @@ pub(super) async fn force_set_room_state_from_server(
|
||||
self.services
|
||||
.rooms
|
||||
.outlier
|
||||
.add_pdu_outlier(&event_id, &value)?;
|
||||
.add_pdu_outlier(&event_id, &value);
|
||||
}
|
||||
|
||||
let new_room_state = self
|
||||
@@ -622,17 +635,22 @@ pub(super) async fn force_set_room_state_from_server(
|
||||
.await?;
|
||||
|
||||
info!("Forcing new room state");
|
||||
let (short_state_hash, new, removed) = self
|
||||
let HashSetCompressStateEvent {
|
||||
shortstatehash: short_state_hash,
|
||||
added,
|
||||
removed,
|
||||
} = self
|
||||
.services
|
||||
.rooms
|
||||
.state_compressor
|
||||
.save_state(room_id.clone().as_ref(), new_room_state)?;
|
||||
.save_state(room_id.clone().as_ref(), new_room_state)
|
||||
.await?;
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&room_id).await;
|
||||
self.services
|
||||
.rooms
|
||||
.state
|
||||
.force_state(room_id.clone().as_ref(), short_state_hash, new, removed, &state_lock)
|
||||
.force_state(room_id.clone().as_ref(), short_state_hash, added, removed, &state_lock)
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
@@ -642,7 +660,8 @@ pub(super) async fn force_set_room_state_from_server(
|
||||
self.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.update_joined_count(&room_id)?;
|
||||
.update_joined_count(&room_id)
|
||||
.await;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
@@ -653,10 +672,33 @@ pub(super) async fn force_set_room_state_from_server(
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn get_signing_keys(
|
||||
&self, server_name: Option<Box<ServerName>>, _cached: bool,
|
||||
&self, server_name: Option<Box<ServerName>>, notary: Option<Box<ServerName>>, query: bool,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
let server_name = server_name.unwrap_or_else(|| self.services.server.config.server_name.clone().into());
|
||||
let signing_keys = self.services.globals.signing_keys_for(&server_name)?;
|
||||
|
||||
if let Some(notary) = notary {
|
||||
let signing_keys = self
|
||||
.services
|
||||
.server_keys
|
||||
.notary_request(¬ary, &server_name)
|
||||
.await?;
|
||||
|
||||
return Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"```rs\n{signing_keys:#?}\n```"
|
||||
)));
|
||||
}
|
||||
|
||||
let signing_keys = if query {
|
||||
self.services
|
||||
.server_keys
|
||||
.server_request(&server_name)
|
||||
.await?
|
||||
} else {
|
||||
self.services
|
||||
.server_keys
|
||||
.signing_keys_for(&server_name)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"```rs\n{signing_keys:#?}\n```"
|
||||
@@ -664,34 +706,20 @@ pub(super) async fn get_signing_keys(
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
#[allow(dead_code)]
|
||||
pub(super) async fn get_verify_keys(
|
||||
&self, server_name: Option<Box<ServerName>>, cached: bool,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
pub(super) async fn get_verify_keys(&self, server_name: Option<Box<ServerName>>) -> Result<RoomMessageEventContent> {
|
||||
let server_name = server_name.unwrap_or_else(|| self.services.server.config.server_name.clone().into());
|
||||
let mut out = String::new();
|
||||
|
||||
if cached {
|
||||
writeln!(out, "| Key ID | VerifyKey |")?;
|
||||
writeln!(out, "| --- | --- |")?;
|
||||
for (key_id, verify_key) in self.services.globals.verify_keys_for(&server_name)? {
|
||||
writeln!(out, "| {key_id} | {verify_key:?} |")?;
|
||||
}
|
||||
|
||||
return Ok(RoomMessageEventContent::notice_markdown(out));
|
||||
}
|
||||
|
||||
let signature_ids: Vec<String> = Vec::new();
|
||||
let keys = self
|
||||
.services
|
||||
.server_keys
|
||||
.fetch_signing_keys_for_server(&server_name, signature_ids)
|
||||
.await?;
|
||||
.verify_keys_for(&server_name)
|
||||
.await;
|
||||
|
||||
let mut out = String::new();
|
||||
writeln!(out, "| Key ID | Public Key |")?;
|
||||
writeln!(out, "| --- | --- |")?;
|
||||
for (key_id, key) in keys {
|
||||
writeln!(out, "| {key_id} | {key} |")?;
|
||||
writeln!(out, "| {key_id} | {key:?} |")?;
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(out))
|
||||
@@ -814,10 +842,10 @@ pub(super) async fn database_stats(
|
||||
&self, property: Option<String>, map: Option<String>,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
let property = property.unwrap_or_else(|| "rocksdb.stats".to_owned());
|
||||
let map_name = map.as_ref().map_or(utils::string::EMPTY, String::as_str);
|
||||
let map_name = map.as_ref().map_or(EMPTY, String::as_str);
|
||||
|
||||
let mut out = String::new();
|
||||
for (name, map) in self.services.db.iter_maps() {
|
||||
for (name, map) in self.services.db.iter() {
|
||||
if !map_name.is_empty() && *map_name != *name {
|
||||
continue;
|
||||
}
|
||||
|
||||
+16
-1
@@ -80,8 +80,16 @@ pub(super) enum DebugCommand {
|
||||
GetSigningKeys {
|
||||
server_name: Option<Box<ServerName>>,
|
||||
|
||||
#[arg(long)]
|
||||
notary: Option<Box<ServerName>>,
|
||||
|
||||
#[arg(short, long)]
|
||||
cached: bool,
|
||||
query: bool,
|
||||
},
|
||||
|
||||
/// - Get and display signing keys from local cache or remote server.
|
||||
GetVerifyKeys {
|
||||
server_name: Option<Box<ServerName>>,
|
||||
},
|
||||
|
||||
/// - Sends a federation request to the remote server's
|
||||
@@ -119,6 +127,13 @@ pub(super) enum DebugCommand {
|
||||
/// the command.
|
||||
VerifyJson,
|
||||
|
||||
/// - Verify PDU
|
||||
///
|
||||
/// This re-verifies a PDU existing in the database found by ID.
|
||||
VerifyPdu {
|
||||
event_id: Box<EventId>,
|
||||
},
|
||||
|
||||
/// - Prints the very first PDU in the specified room (typically
|
||||
/// m.room.create)
|
||||
FirstPduInRoom {
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use conduit::Result;
|
||||
use futures::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomId, ServerName, UserId};
|
||||
|
||||
use crate::{admin_command, escape_html, get_room_info};
|
||||
use crate::{admin_command, get_room_info};
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn disable_room(&self, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
|
||||
self.services.rooms.metadata.disable_room(&room_id, true)?;
|
||||
self.services.rooms.metadata.disable_room(&room_id, true);
|
||||
Ok(RoomMessageEventContent::text_plain("Room disabled."))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn enable_room(&self, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
|
||||
self.services.rooms.metadata.disable_room(&room_id, false)?;
|
||||
self.services.rooms.metadata.disable_room(&room_id, false);
|
||||
Ok(RoomMessageEventContent::text_plain("Room enabled."))
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: Box<UserId>) -> Result<
|
||||
));
|
||||
}
|
||||
|
||||
if !self.services.users.exists(&user_id)? {
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Remote user does not exist in our database.",
|
||||
));
|
||||
@@ -96,9 +97,9 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: Box<UserId>) -> Result<
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&user_id)
|
||||
.filter_map(Result::ok)
|
||||
.map(|room_id| get_room_info(self.services, &room_id))
|
||||
.collect();
|
||||
.then(|room_id| get_room_info(self.services, room_id))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
if rooms.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain("User is not in any rooms."));
|
||||
@@ -107,33 +108,15 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: Box<UserId>) -> Result<
|
||||
rooms.sort_by_key(|r| r.1);
|
||||
rooms.reverse();
|
||||
|
||||
let output_plain = format!(
|
||||
"Rooms {user_id} shares with us ({}):\n{}",
|
||||
let output = format!(
|
||||
"Rooms {user_id} shares with us ({}):\n```\n{}\n```",
|
||||
rooms.len(),
|
||||
rooms
|
||||
.iter()
|
||||
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
|
||||
.map(|(id, members, name)| format!("{id} | Members: {members} | Name: {name}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
let output_html = format!(
|
||||
"<table><caption>Rooms {user_id} shares with us \
|
||||
({})</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
|
||||
rooms.len(),
|
||||
rooms
|
||||
.iter()
|
||||
.fold(String::new(), |mut output, (id, members, name)| {
|
||||
writeln!(
|
||||
output,
|
||||
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
|
||||
id,
|
||||
members,
|
||||
escape_html(name)
|
||||
)
|
||||
.expect("should be able to write to string buffer");
|
||||
output
|
||||
})
|
||||
);
|
||||
|
||||
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
|
||||
Ok(RoomMessageEventContent::text_markdown(output))
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ pub(super) async fn delete(
|
||||
let mut mxc_urls = Vec::with_capacity(4);
|
||||
|
||||
// parsing the PDU for any MXC URLs begins here
|
||||
if let Some(event_json) = self.services.rooms.timeline.get_pdu_json(&event_id)? {
|
||||
if let Ok(event_json) = self.services.rooms.timeline.get_pdu_json(&event_id).await {
|
||||
if let Some(content_key) = event_json.get("content") {
|
||||
debug!("Event ID has \"content\".");
|
||||
let content_obj = content_key.as_object();
|
||||
@@ -300,7 +300,7 @@ pub(super) async fn delete_all_from_server(
|
||||
#[admin_command]
|
||||
pub(super) async fn get_file_info(&self, mxc: OwnedMxcUri) -> Result<RoomMessageEventContent> {
|
||||
let mxc: Mxc<'_> = mxc.as_str().try_into()?;
|
||||
let metadata = self.services.media.get_metadata(&mxc);
|
||||
let metadata = self.services.media.get_metadata(&mxc).await;
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!("```\n{metadata:#?}\n```")))
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use conduit::{
|
||||
utils::string::{collect_stream, common_prefix},
|
||||
warn, Error, Result,
|
||||
};
|
||||
use futures_util::future::FutureExt;
|
||||
use futures::future::FutureExt;
|
||||
use ruma::{
|
||||
events::{
|
||||
relation::InReplyTo,
|
||||
@@ -157,10 +157,7 @@ fn parse<'a>(
|
||||
let message = error
|
||||
.to_string()
|
||||
.replace("server.name", services.globals.server_name().as_str());
|
||||
Err(reply(
|
||||
RoomMessageEventContent::notice_markdown(message),
|
||||
input.reply_id.as_deref(),
|
||||
))
|
||||
Err(reply(RoomMessageEventContent::notice_plain(message), input.reply_id.as_deref()))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use clap::Subcommand;
|
||||
use conduit::Result;
|
||||
use ruma::{
|
||||
events::{room::message::RoomMessageEventContent, RoomAccountDataEventType},
|
||||
RoomId, UserId,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, RoomId, UserId};
|
||||
|
||||
use crate::Command;
|
||||
|
||||
@@ -25,7 +23,7 @@ pub(crate) enum AccountDataCommand {
|
||||
/// Full user ID
|
||||
user_id: Box<UserId>,
|
||||
/// Account data event type
|
||||
kind: RoomAccountDataEventType,
|
||||
kind: String,
|
||||
/// Optional room ID of the account data
|
||||
room_id: Option<Box<RoomId>>,
|
||||
},
|
||||
@@ -42,9 +40,11 @@ pub(super) async fn process(subcommand: AccountDataCommand, context: &Command<'_
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services
|
||||
let results: Vec<_> = services
|
||||
.account_data
|
||||
.changes_since(room_id.as_deref(), &user_id, since)?;
|
||||
.changes_since(room_id.as_deref(), &user_id, since)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -59,7 +59,8 @@ pub(super) async fn process(subcommand: AccountDataCommand, context: &Command<'_
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services
|
||||
.account_data
|
||||
.get(room_id.as_deref(), &user_id, kind)?;
|
||||
.get_raw(room_id.as_deref(), &user_id, &kind)
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
|
||||
@@ -26,10 +26,8 @@ pub(super) async fn process(subcommand: AppserviceCommand, context: &Command<'_>
|
||||
appservice_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services
|
||||
.appservice
|
||||
.db
|
||||
.get_registration(appservice_id.as_ref());
|
||||
let results = services.appservice.get_registration(&appservice_id).await;
|
||||
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -38,7 +36,7 @@ pub(super) async fn process(subcommand: AppserviceCommand, context: &Command<'_>
|
||||
},
|
||||
AppserviceCommand::All => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.appservice.all();
|
||||
let results = services.appservice.all().await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
|
||||
@@ -13,8 +13,6 @@ pub(crate) enum GlobalsCommand {
|
||||
|
||||
LastCheckForUpdatesId,
|
||||
|
||||
LoadKeypair,
|
||||
|
||||
/// - This returns an empty `Ok(BTreeMap<..>)` when there are no keys found
|
||||
/// for the server.
|
||||
SigningKeysFor {
|
||||
@@ -29,7 +27,7 @@ pub(super) async fn process(subcommand: GlobalsCommand, context: &Command<'_>) -
|
||||
match subcommand {
|
||||
GlobalsCommand::DatabaseVersion => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.globals.db.database_version();
|
||||
let results = services.globals.db.database_version().await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -47,16 +45,7 @@ pub(super) async fn process(subcommand: GlobalsCommand, context: &Command<'_>) -
|
||||
},
|
||||
GlobalsCommand::LastCheckForUpdatesId => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.updates.last_check_for_updates_id();
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
|
||||
)))
|
||||
},
|
||||
GlobalsCommand::LoadKeypair => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.globals.db.load_keypair();
|
||||
let results = services.updates.last_check_for_updates_id().await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -67,7 +56,7 @@ pub(super) async fn process(subcommand: GlobalsCommand, context: &Command<'_>) -
|
||||
origin,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.globals.db.verify_keys_for(&origin);
|
||||
let results = services.server_keys.verify_keys_for(&origin).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use clap::Subcommand;
|
||||
use conduit::Result;
|
||||
use futures::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, UserId};
|
||||
|
||||
use crate::Command;
|
||||
@@ -30,7 +31,7 @@ pub(super) async fn process(subcommand: PresenceCommand, context: &Command<'_>)
|
||||
user_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.presence.db.get_presence(&user_id)?;
|
||||
let results = services.presence.db.get_presence(&user_id).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -41,12 +42,16 @@ pub(super) async fn process(subcommand: PresenceCommand, context: &Command<'_>)
|
||||
since,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.presence.db.presence_since(since);
|
||||
let presence_since: Vec<(_, _, _)> = results.collect();
|
||||
let results: Vec<(_, _, _)> = services
|
||||
.presence
|
||||
.presence_since(since)
|
||||
.map(|(user_id, count, bytes)| (user_id.to_owned(), count, bytes.to_vec()))
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{presence_since:#?}\n```"
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
|
||||
)))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ pub(super) async fn process(subcommand: PusherCommand, context: &Command<'_>) ->
|
||||
user_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.pusher.get_pushers(&user_id)?;
|
||||
let results = services.pusher.get_pushers(&user_id).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use clap::Subcommand;
|
||||
use conduit::Result;
|
||||
use futures::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId, RoomId};
|
||||
|
||||
use crate::Command;
|
||||
@@ -31,7 +32,7 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Command<'_>)
|
||||
alias,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.rooms.alias.resolve_local_alias(&alias);
|
||||
let results = services.rooms.alias.resolve_local_alias(&alias).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -42,8 +43,13 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Command<'_>)
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.rooms.alias.local_aliases_for_room(&room_id);
|
||||
let aliases: Vec<_> = results.collect();
|
||||
let aliases: Vec<_> = services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -52,8 +58,13 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Command<'_>)
|
||||
},
|
||||
RoomAliasCommand::AllLocalAliases => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.rooms.alias.all_local_aliases();
|
||||
let aliases: Vec<_> = results.collect();
|
||||
let aliases = services
|
||||
.rooms
|
||||
.alias
|
||||
.all_local_aliases()
|
||||
.map(|(room_id, alias)| (room_id.to_owned(), alias.to_owned()))
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use clap::Subcommand;
|
||||
use conduit::Result;
|
||||
use futures::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, RoomId, ServerName, UserId};
|
||||
|
||||
use crate::Command;
|
||||
@@ -86,7 +87,11 @@ pub(super) async fn process(
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result = services.rooms.state_cache.server_in_room(&server, &room_id);
|
||||
let result = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(&server, &room_id)
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -97,7 +102,13 @@ pub(super) async fn process(
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results: Result<Vec<_>> = services.rooms.state_cache.room_servers(&room_id).collect();
|
||||
let results: Vec<_> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_servers(&room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -108,7 +119,13 @@ pub(super) async fn process(
|
||||
server,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results: Result<Vec<_>> = services.rooms.state_cache.server_rooms(&server).collect();
|
||||
let results: Vec<_> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_rooms(&server)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -119,7 +136,13 @@ pub(super) async fn process(
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results: Result<Vec<_>> = services.rooms.state_cache.room_members(&room_id).collect();
|
||||
let results: Vec<_> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -134,7 +157,9 @@ pub(super) async fn process(
|
||||
.rooms
|
||||
.state_cache
|
||||
.local_users_in_room(&room_id)
|
||||
.collect();
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -149,7 +174,9 @@ pub(super) async fn process(
|
||||
.rooms
|
||||
.state_cache
|
||||
.active_local_users_in_room(&room_id)
|
||||
.collect();
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -160,7 +187,7 @@ pub(super) async fn process(
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.rooms.state_cache.room_joined_count(&room_id);
|
||||
let results = services.rooms.state_cache.room_joined_count(&room_id).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -171,7 +198,11 @@ pub(super) async fn process(
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.rooms.state_cache.room_invited_count(&room_id);
|
||||
let results = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_invited_count(&room_id)
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -182,11 +213,13 @@ pub(super) async fn process(
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results: Result<Vec<_>> = services
|
||||
let results: Vec<_> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_useroncejoined(&room_id)
|
||||
.collect();
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -197,11 +230,13 @@ pub(super) async fn process(
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results: Result<Vec<_>> = services
|
||||
let results: Vec<_> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members_invited(&room_id)
|
||||
.collect();
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -216,7 +251,8 @@ pub(super) async fn process(
|
||||
let results = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.get_invite_count(&room_id, &user_id);
|
||||
.get_invite_count(&room_id, &user_id)
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -231,7 +267,8 @@ pub(super) async fn process(
|
||||
let results = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.get_left_count(&room_id, &user_id);
|
||||
.get_left_count(&room_id, &user_id)
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -242,7 +279,13 @@ pub(super) async fn process(
|
||||
user_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results: Result<Vec<_>> = services.rooms.state_cache.rooms_joined(&user_id).collect();
|
||||
let results: Vec<_> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&user_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -253,7 +296,12 @@ pub(super) async fn process(
|
||||
user_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results: Result<Vec<_>> = services.rooms.state_cache.rooms_invited(&user_id).collect();
|
||||
let results: Vec<_> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_invited(&user_id)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -264,7 +312,12 @@ pub(super) async fn process(
|
||||
user_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results: Result<Vec<_>> = services.rooms.state_cache.rooms_left(&user_id).collect();
|
||||
let results: Vec<_> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_left(&user_id)
|
||||
.collect()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -276,7 +329,11 @@ pub(super) async fn process(
|
||||
room_id,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.rooms.state_cache.invite_state(&user_id, &room_id);
|
||||
let results = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.invite_state(&user_id, &room_id)
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use clap::Subcommand;
|
||||
use conduit::Result;
|
||||
use futures::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, ServerName, UserId};
|
||||
use service::sending::Destination;
|
||||
|
||||
@@ -68,7 +69,7 @@ pub(super) async fn process(subcommand: SendingCommand, context: &Command<'_>) -
|
||||
SendingCommand::ActiveRequests => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.sending.db.active_requests();
|
||||
let active_requests: Result<Vec<(_, _, _)>> = results.collect();
|
||||
let active_requests = results.collect::<Vec<_>>().await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -133,7 +134,7 @@ pub(super) async fn process(subcommand: SendingCommand, context: &Command<'_>) -
|
||||
},
|
||||
};
|
||||
|
||||
let queued_requests = results.collect::<Result<Vec<(_, _)>>>();
|
||||
let queued_requests = results.collect::<Vec<_>>().await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -199,7 +200,7 @@ pub(super) async fn process(subcommand: SendingCommand, context: &Command<'_>) -
|
||||
},
|
||||
};
|
||||
|
||||
let active_requests = results.collect::<Result<Vec<(_, _)>>>();
|
||||
let active_requests = results.collect::<Vec<_>>().await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
@@ -210,7 +211,7 @@ pub(super) async fn process(subcommand: SendingCommand, context: &Command<'_>) -
|
||||
server_name,
|
||||
} => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.sending.db.get_latest_educount(&server_name);
|
||||
let results = services.sending.db.get_latest_educount(&server_name).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
|
||||
+333
-18
@@ -1,29 +1,344 @@
|
||||
use clap::Subcommand;
|
||||
use conduit::Result;
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
use futures::stream::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedDeviceId, OwnedRoomId, OwnedUserId};
|
||||
|
||||
use crate::Command;
|
||||
use crate::{admin_command, admin_command_dispatch};
|
||||
|
||||
#[admin_command_dispatch]
|
||||
#[derive(Debug, Subcommand)]
|
||||
/// All the getters and iterators from src/database/key_value/users.rs
|
||||
pub(crate) enum UsersCommand {
|
||||
Iter,
|
||||
CountUsers,
|
||||
|
||||
IterUsers,
|
||||
|
||||
PasswordHash {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
ListDevices {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
ListDevicesMetadata {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
GetDeviceMetadata {
|
||||
user_id: OwnedUserId,
|
||||
device_id: OwnedDeviceId,
|
||||
},
|
||||
|
||||
GetDevicesVersion {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
CountOneTimeKeys {
|
||||
user_id: OwnedUserId,
|
||||
device_id: OwnedDeviceId,
|
||||
},
|
||||
|
||||
GetDeviceKeys {
|
||||
user_id: OwnedUserId,
|
||||
device_id: OwnedDeviceId,
|
||||
},
|
||||
|
||||
GetUserSigningKey {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
GetMasterKey {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
GetToDeviceEvents {
|
||||
user_id: OwnedUserId,
|
||||
device_id: OwnedDeviceId,
|
||||
},
|
||||
|
||||
GetLatestBackup {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
GetLatestBackupVersion {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
GetBackupAlgorithm {
|
||||
user_id: OwnedUserId,
|
||||
version: String,
|
||||
},
|
||||
|
||||
GetAllBackups {
|
||||
user_id: OwnedUserId,
|
||||
version: String,
|
||||
},
|
||||
|
||||
GetRoomBackups {
|
||||
user_id: OwnedUserId,
|
||||
version: String,
|
||||
room_id: OwnedRoomId,
|
||||
},
|
||||
|
||||
GetBackupSession {
|
||||
user_id: OwnedUserId,
|
||||
version: String,
|
||||
room_id: OwnedRoomId,
|
||||
session_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// All the getters and iterators in key_value/users.rs
|
||||
pub(super) async fn process(subcommand: UsersCommand, context: &Command<'_>) -> Result<RoomMessageEventContent> {
|
||||
let services = context.services;
|
||||
#[admin_command]
|
||||
async fn get_backup_session(
|
||||
&self, user_id: OwnedUserId, version: String, room_id: OwnedRoomId, session_id: String,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
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();
|
||||
|
||||
match subcommand {
|
||||
UsersCommand::Iter => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.users.db.iter();
|
||||
let users = results.collect::<Vec<_>>();
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{users:#?}\n```"
|
||||
)))
|
||||
},
|
||||
}
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn get_room_backups(
|
||||
&self, user_id: OwnedUserId, version: String, room_id: OwnedRoomId,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
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();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn get_all_backups(&self, user_id: OwnedUserId, version: String) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result = self.services.key_backups.get_all(&user_id, &version).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn get_backup_algorithm(&self, user_id: OwnedUserId, version: String) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result = self
|
||||
.services
|
||||
.key_backups
|
||||
.get_backup(&user_id, &version)
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn get_latest_backup_version(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result = self
|
||||
.services
|
||||
.key_backups
|
||||
.get_latest_backup_version(&user_id)
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn get_latest_backup(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result = self.services.key_backups.get_latest_backup(&user_id).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn iter_users(&self) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result: Vec<OwnedUserId> = self.services.users.stream().map(Into::into).collect().await;
|
||||
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn count_users(&self) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result = self.services.users.count().await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn password_hash(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result = self.services.users.password_hash(&user_id).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn list_devices(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let devices = self
|
||||
.services
|
||||
.users
|
||||
.all_device_ids(&user_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn list_devices_metadata(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let devices = self
|
||||
.services
|
||||
.users
|
||||
.all_devices_metadata(&user_id)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn get_device_metadata(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let device = self
|
||||
.services
|
||||
.users
|
||||
.get_device_metadata(&user_id, &device_id)
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn get_devices_version(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let device = self.services.users.get_devicelist_version(&user_id).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn count_one_time_keys(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result<RoomMessageEventContent> {
|
||||
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();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn get_device_keys(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result = self
|
||||
.services
|
||||
.users
|
||||
.get_device_keys(&user_id, &device_id)
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn get_user_signing_key(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result = self.services.users.get_user_signing_key(&user_id).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn get_master_key(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result = self
|
||||
.services
|
||||
.users
|
||||
.get_master_key(None, &user_id, &|_| true)
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn get_to_device_events(
|
||||
&self, user_id: OwnedUserId, device_id: OwnedDeviceId,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result = self
|
||||
.services
|
||||
.users
|
||||
.get_to_device_events(&user_id, &device_id)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
|
||||
)))
|
||||
}
|
||||
|
||||
+57
-63
@@ -2,7 +2,8 @@ use std::fmt::Write;
|
||||
|
||||
use clap::Subcommand;
|
||||
use conduit::Result;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId, RoomId};
|
||||
use futures::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId};
|
||||
|
||||
use crate::{escape_html, Command};
|
||||
|
||||
@@ -66,8 +67,8 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Command<'_>) ->
|
||||
force,
|
||||
room_id,
|
||||
..
|
||||
} => match (force, services.rooms.alias.resolve_local_alias(&room_alias)) {
|
||||
(true, Ok(Some(id))) => match services
|
||||
} => match (force, services.rooms.alias.resolve_local_alias(&room_alias).await) {
|
||||
(true, Ok(id)) => match services
|
||||
.rooms
|
||||
.alias
|
||||
.set_alias(&room_alias, &room_id, server_user)
|
||||
@@ -77,10 +78,10 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Command<'_>) ->
|
||||
))),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))),
|
||||
},
|
||||
(false, Ok(Some(id))) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
(false, Ok(id)) => Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Refusing to overwrite in use alias for {id}, use -f or --force to overwrite"
|
||||
))),
|
||||
(_, Ok(None)) => match services
|
||||
(_, Err(_)) => match services
|
||||
.rooms
|
||||
.alias
|
||||
.set_alias(&room_alias, &room_id, server_user)
|
||||
@@ -88,12 +89,11 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Command<'_>) ->
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Successfully set alias")),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))),
|
||||
},
|
||||
(_, Err(err)) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {err}"))),
|
||||
},
|
||||
RoomAliasCommand::Remove {
|
||||
..
|
||||
} => match services.rooms.alias.resolve_local_alias(&room_alias) {
|
||||
Ok(Some(id)) => match services
|
||||
} => match services.rooms.alias.resolve_local_alias(&room_alias).await {
|
||||
Ok(id) => match services
|
||||
.rooms
|
||||
.alias
|
||||
.remove_alias(&room_alias, server_user)
|
||||
@@ -102,15 +102,13 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Command<'_>) ->
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!("Removed alias from {id}"))),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))),
|
||||
},
|
||||
Ok(None) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {err}"))),
|
||||
Err(_) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
|
||||
},
|
||||
RoomAliasCommand::Which {
|
||||
..
|
||||
} => match services.rooms.alias.resolve_local_alias(&room_alias) {
|
||||
Ok(Some(id)) => Ok(RoomMessageEventContent::text_plain(format!("Alias resolves to {id}"))),
|
||||
Ok(None) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {err}"))),
|
||||
} => match services.rooms.alias.resolve_local_alias(&room_alias).await {
|
||||
Ok(id) => Ok(RoomMessageEventContent::text_plain(format!("Alias resolves to {id}"))),
|
||||
Err(_) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
|
||||
},
|
||||
RoomAliasCommand::List {
|
||||
..
|
||||
@@ -121,67 +119,63 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Command<'_>) ->
|
||||
room_id,
|
||||
} => {
|
||||
if let Some(room_id) = room_id {
|
||||
let aliases = services
|
||||
let aliases: Vec<OwnedRoomAliasId> = services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&room_id)
|
||||
.collect::<Result<Vec<_>, _>>();
|
||||
match aliases {
|
||||
Ok(aliases) => {
|
||||
let plain_list = aliases.iter().fold(String::new(), |mut output, alias| {
|
||||
writeln!(output, "- {alias}").expect("should be able to write to string buffer");
|
||||
output
|
||||
});
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let html_list = aliases.iter().fold(String::new(), |mut output, alias| {
|
||||
writeln!(output, "<li>{}</li>", escape_html(alias.as_ref()))
|
||||
.expect("should be able to write to string buffer");
|
||||
output
|
||||
});
|
||||
let plain_list = aliases.iter().fold(String::new(), |mut output, alias| {
|
||||
writeln!(output, "- {alias}").expect("should be able to write to string buffer");
|
||||
output
|
||||
});
|
||||
|
||||
let plain = format!("Aliases for {room_id}:\n{plain_list}");
|
||||
let html = format!("Aliases for {room_id}:\n<ul>{html_list}</ul>");
|
||||
Ok(RoomMessageEventContent::text_html(plain, html))
|
||||
},
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to list aliases: {err}"))),
|
||||
}
|
||||
let html_list = aliases.iter().fold(String::new(), |mut output, alias| {
|
||||
writeln!(output, "<li>{}</li>", escape_html(alias.as_ref()))
|
||||
.expect("should be able to write to string buffer");
|
||||
output
|
||||
});
|
||||
|
||||
let plain = format!("Aliases for {room_id}:\n{plain_list}");
|
||||
let html = format!("Aliases for {room_id}:\n<ul>{html_list}</ul>");
|
||||
Ok(RoomMessageEventContent::text_html(plain, html))
|
||||
} else {
|
||||
let aliases = services
|
||||
.rooms
|
||||
.alias
|
||||
.all_local_aliases()
|
||||
.collect::<Result<Vec<_>, _>>();
|
||||
match aliases {
|
||||
Ok(aliases) => {
|
||||
let server_name = services.globals.server_name();
|
||||
let plain_list = aliases
|
||||
.iter()
|
||||
.fold(String::new(), |mut output, (alias, id)| {
|
||||
writeln!(output, "- `{alias}` -> #{id}:{server_name}")
|
||||
.expect("should be able to write to string buffer");
|
||||
output
|
||||
});
|
||||
.map(|(room_id, localpart)| (room_id.into(), localpart.into()))
|
||||
.collect::<Vec<(OwnedRoomId, String)>>()
|
||||
.await;
|
||||
|
||||
let html_list = aliases
|
||||
.iter()
|
||||
.fold(String::new(), |mut output, (alias, id)| {
|
||||
writeln!(
|
||||
output,
|
||||
"<li><code>{}</code> -> #{}:{}</li>",
|
||||
escape_html(alias.as_ref()),
|
||||
escape_html(id.as_ref()),
|
||||
server_name
|
||||
)
|
||||
.expect("should be able to write to string buffer");
|
||||
output
|
||||
});
|
||||
let server_name = services.globals.server_name();
|
||||
let plain_list = aliases
|
||||
.iter()
|
||||
.fold(String::new(), |mut output, (alias, id)| {
|
||||
writeln!(output, "- `{alias}` -> #{id}:{server_name}")
|
||||
.expect("should be able to write to string buffer");
|
||||
output
|
||||
});
|
||||
|
||||
let plain = format!("Aliases:\n{plain_list}");
|
||||
let html = format!("Aliases:\n<ul>{html_list}</ul>");
|
||||
Ok(RoomMessageEventContent::text_html(plain, html))
|
||||
},
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Unable to list room aliases: {e}"))),
|
||||
}
|
||||
let html_list = aliases
|
||||
.iter()
|
||||
.fold(String::new(), |mut output, (alias, id)| {
|
||||
writeln!(
|
||||
output,
|
||||
"<li><code>{}</code> -> #{}:{}</li>",
|
||||
escape_html(alias.as_ref()),
|
||||
escape_html(id),
|
||||
server_name
|
||||
)
|
||||
.expect("should be able to write to string buffer");
|
||||
output
|
||||
});
|
||||
|
||||
let plain = format!("Aliases:\n{plain_list}");
|
||||
let html = format!("Aliases:\n<ul>{html_list}</ul>");
|
||||
Ok(RoomMessageEventContent::text_html(plain, html))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
+18
-31
@@ -1,5 +1,6 @@
|
||||
use conduit::Result;
|
||||
use ruma::events::room::message::RoomMessageEventContent;
|
||||
use futures::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
|
||||
|
||||
use crate::{admin_command, get_room_info, PAGE_SIZE};
|
||||
|
||||
@@ -14,37 +15,16 @@ pub(super) async fn list_rooms(
|
||||
.rooms
|
||||
.metadata
|
||||
.iter_ids()
|
||||
.filter_map(|room_id| {
|
||||
room_id
|
||||
.ok()
|
||||
.filter(|room_id| {
|
||||
if exclude_disabled
|
||||
&& self
|
||||
.services
|
||||
.rooms
|
||||
.metadata
|
||||
.is_disabled(room_id)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if exclude_banned
|
||||
&& self
|
||||
.services
|
||||
.rooms
|
||||
.metadata
|
||||
.is_banned(room_id)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.map(|room_id| get_room_info(self.services, &room_id))
|
||||
.filter_map(|room_id| async move {
|
||||
(!exclude_disabled || !self.services.rooms.metadata.is_disabled(room_id).await).then_some(room_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.filter_map(|room_id| async move {
|
||||
(!exclude_banned || !self.services.rooms.metadata.is_banned(room_id).await).then_some(room_id)
|
||||
})
|
||||
.then(|room_id| get_room_info(self.services, room_id))
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
rooms.sort_by_key(|r| r.1);
|
||||
rooms.reverse();
|
||||
|
||||
@@ -74,3 +54,10 @@ pub(super) async fn list_rooms(
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(output_plain))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn exists(&self, room_id: OwnedRoomId) -> Result<RoomMessageEventContent> {
|
||||
let result = self.services.rooms.metadata.exists(&room_id).await;
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!("{result}")))
|
||||
}
|
||||
|
||||
+20
-37
@@ -1,10 +1,9 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use clap::Subcommand;
|
||||
use conduit::Result;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomId};
|
||||
use futures::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, RoomId};
|
||||
|
||||
use crate::{escape_html, get_room_info, Command, PAGE_SIZE};
|
||||
use crate::{get_room_info, Command, PAGE_SIZE};
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub(crate) enum RoomDirectoryCommand {
|
||||
@@ -31,67 +30,51 @@ pub(super) async fn process(command: RoomDirectoryCommand, context: &Command<'_>
|
||||
match command {
|
||||
RoomDirectoryCommand::Publish {
|
||||
room_id,
|
||||
} => match services.rooms.directory.set_public(&room_id) {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Room published")),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to update room: {err}"))),
|
||||
} => {
|
||||
services.rooms.directory.set_public(&room_id);
|
||||
Ok(RoomMessageEventContent::notice_plain("Room published"))
|
||||
},
|
||||
RoomDirectoryCommand::Unpublish {
|
||||
room_id,
|
||||
} => match services.rooms.directory.set_not_public(&room_id) {
|
||||
Ok(()) => Ok(RoomMessageEventContent::text_plain("Room unpublished")),
|
||||
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to update room: {err}"))),
|
||||
} => {
|
||||
services.rooms.directory.set_not_public(&room_id);
|
||||
Ok(RoomMessageEventContent::notice_plain("Room unpublished"))
|
||||
},
|
||||
RoomDirectoryCommand::List {
|
||||
page,
|
||||
} => {
|
||||
// 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 = services
|
||||
let mut rooms: Vec<_> = services
|
||||
.rooms
|
||||
.directory
|
||||
.public_rooms()
|
||||
.filter_map(Result::ok)
|
||||
.map(|id: OwnedRoomId| get_room_info(services, &id))
|
||||
.collect::<Vec<_>>();
|
||||
.then(|room_id| get_room_info(services, room_id))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
rooms.sort_by_key(|r| r.1);
|
||||
rooms.reverse();
|
||||
|
||||
let rooms = rooms
|
||||
let rooms: Vec<_> = rooms
|
||||
.into_iter()
|
||||
.skip(page.saturating_sub(1).saturating_mul(PAGE_SIZE))
|
||||
.take(PAGE_SIZE)
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
|
||||
if rooms.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain("No more rooms."));
|
||||
};
|
||||
|
||||
let output_plain = format!(
|
||||
"Rooms:\n{}",
|
||||
let output = format!(
|
||||
"Rooms (page {page}):\n```\n{}\n```",
|
||||
rooms
|
||||
.iter()
|
||||
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
|
||||
.map(|(id, members, name)| format!("{id} | Members: {members} | Name: {name}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
let output_html = format!(
|
||||
"<table><caption>Room directory - page \
|
||||
{page}</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
|
||||
rooms
|
||||
.iter()
|
||||
.fold(String::new(), |mut output, (id, members, name)| {
|
||||
writeln!(
|
||||
output,
|
||||
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
|
||||
escape_html(id.as_ref()),
|
||||
members,
|
||||
escape_html(name.as_ref())
|
||||
)
|
||||
.expect("should be able to write to string buffer");
|
||||
output
|
||||
})
|
||||
);
|
||||
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
|
||||
Ok(RoomMessageEventContent::text_markdown(output))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+24
-28
@@ -1,5 +1,6 @@
|
||||
use clap::Subcommand;
|
||||
use conduit::Result;
|
||||
use conduit::{utils::ReadyExt, Result};
|
||||
use futures::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, RoomId};
|
||||
|
||||
use crate::{admin_command, admin_command_dispatch};
|
||||
@@ -32,46 +33,40 @@ async fn list_joined_members(&self, room_id: Box<RoomId>, local_only: bool) -> R
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_name(&room_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| room_id.to_string());
|
||||
.await
|
||||
.unwrap_or_else(|_| room_id.to_string());
|
||||
|
||||
let members = self
|
||||
let member_info: Vec<_> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&room_id)
|
||||
.filter_map(|member| {
|
||||
if local_only {
|
||||
member
|
||||
.ok()
|
||||
.filter(|user| self.services.globals.user_is_local(user))
|
||||
} else {
|
||||
member.ok()
|
||||
}
|
||||
});
|
||||
|
||||
let member_info = members
|
||||
.into_iter()
|
||||
.map(|user_id| {
|
||||
(
|
||||
user_id.clone(),
|
||||
.ready_filter(|user_id| {
|
||||
local_only
|
||||
.then(|| self.services.globals.user_is_local(user_id))
|
||||
.unwrap_or(true)
|
||||
})
|
||||
.map(ToOwned::to_owned)
|
||||
.filter_map(|user_id| async move {
|
||||
Some((
|
||||
self.services
|
||||
.users
|
||||
.displayname(&user_id)
|
||||
.unwrap_or(None)
|
||||
.unwrap_or_else(|| user_id.to_string()),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|_| user_id.to_string()),
|
||||
user_id,
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let output_plain = format!(
|
||||
"{} Members in Room \"{}\":\n```\n{}\n```",
|
||||
member_info.len(),
|
||||
room_name,
|
||||
member_info
|
||||
.iter()
|
||||
.map(|(mxid, displayname)| format!("{mxid} | {displayname}"))
|
||||
.into_iter()
|
||||
.map(|(displayname, mxid)| format!("{mxid} | {displayname}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
@@ -81,11 +76,12 @@ async fn list_joined_members(&self, room_id: Box<RoomId>, local_only: bool) -> R
|
||||
|
||||
#[admin_command]
|
||||
async fn view_room_topic(&self, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
|
||||
let Some(room_topic) = self
|
||||
let Ok(room_topic) = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_topic(&room_id)?
|
||||
.get_room_topic(&room_id)
|
||||
.await
|
||||
else {
|
||||
return Ok(RoomMessageEventContent::text_plain("Room does not have a room topic set."));
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ mod moderation;
|
||||
|
||||
use clap::Subcommand;
|
||||
use conduit::Result;
|
||||
use ruma::OwnedRoomId;
|
||||
|
||||
use self::{
|
||||
alias::RoomAliasCommand, directory::RoomDirectoryCommand, info::RoomInfoCommand, moderation::RoomModerationCommand,
|
||||
@@ -49,4 +50,9 @@ pub(super) enum RoomCommand {
|
||||
#[command(subcommand)]
|
||||
/// - Manage the room directory
|
||||
Directory(RoomDirectoryCommand),
|
||||
|
||||
/// - Check if we know about a room
|
||||
Exists {
|
||||
room_id: OwnedRoomId,
|
||||
},
|
||||
}
|
||||
|
||||
+163
-180
@@ -1,6 +1,11 @@
|
||||
use api::client::leave_room;
|
||||
use clap::Subcommand;
|
||||
use conduit::{debug, error, info, warn, Result};
|
||||
use conduit::{
|
||||
debug, error, info,
|
||||
utils::{IterStream, ReadyExt},
|
||||
warn, Result,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomAliasId, RoomId, RoomOrAliasId};
|
||||
|
||||
use crate::{admin_command, admin_command_dispatch, get_room_info};
|
||||
@@ -76,7 +81,7 @@ async fn ban_room(
|
||||
|
||||
let admin_room_alias = &self.services.globals.admin_alias;
|
||||
|
||||
if let Some(admin_room_id) = self.services.admin.get_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 Ok(RoomMessageEventContent::text_plain("Not allowed to ban the admin room."));
|
||||
}
|
||||
@@ -95,7 +100,7 @@ async fn ban_room(
|
||||
|
||||
debug!("Room specified is a room ID, banning room ID");
|
||||
|
||||
self.services.rooms.metadata.ban_room(&room_id, true)?;
|
||||
self.services.rooms.metadata.ban_room(&room_id, true);
|
||||
|
||||
room_id
|
||||
} else if room.is_room_alias_id() {
|
||||
@@ -114,7 +119,13 @@ async fn ban_room(
|
||||
get_alias_helper to fetch room ID remotely"
|
||||
);
|
||||
|
||||
let room_id = if let Some(room_id) = self.services.rooms.alias.resolve_local_alias(&room_alias)? {
|
||||
let room_id = if let Ok(room_id) = self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&room_alias)
|
||||
.await
|
||||
{
|
||||
room_id
|
||||
} else {
|
||||
debug!("We don't have this room alias to a room ID locally, attempting to fetch room ID over federation");
|
||||
@@ -138,7 +149,7 @@ async fn ban_room(
|
||||
}
|
||||
};
|
||||
|
||||
self.services.rooms.metadata.ban_room(&room_id, true)?;
|
||||
self.services.rooms.metadata.ban_room(&room_id, true);
|
||||
|
||||
room_id
|
||||
} else {
|
||||
@@ -150,56 +161,40 @@ async fn ban_room(
|
||||
|
||||
debug!("Making all users leave the room {}", &room);
|
||||
if force {
|
||||
for local_user in self
|
||||
let mut users = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&room_id)
|
||||
.filter_map(|user| {
|
||||
user.ok().filter(|local_user| {
|
||||
self.services.globals.user_is_local(local_user)
|
||||
// additional wrapped check here is to avoid adding remote users
|
||||
// who are in the admin room to the list of local users (would
|
||||
// fail auth check)
|
||||
&& (self.services.globals.user_is_local(local_user)
|
||||
// since this is a force operation, assume user is an admin
|
||||
// if somehow this fails
|
||||
&& self.services
|
||||
.users
|
||||
.is_admin(local_user)
|
||||
.unwrap_or(true))
|
||||
})
|
||||
}) {
|
||||
.ready_filter(|user| self.services.globals.user_is_local(user))
|
||||
.boxed();
|
||||
|
||||
while let Some(local_user) = users.next().await {
|
||||
debug!(
|
||||
"Attempting leave for user {} in room {} (forced, ignoring all errors, evicting admins too)",
|
||||
&local_user, &room_id
|
||||
"Attempting leave for user {local_user} in room {room_id} (forced, ignoring all errors, evicting \
|
||||
admins too)",
|
||||
);
|
||||
|
||||
if let Err(e) = leave_room(self.services, &local_user, &room_id, None).await {
|
||||
if let Err(e) = leave_room(self.services, local_user, &room_id, None).await {
|
||||
warn!(%e, "Failed to leave room");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for local_user in self
|
||||
let mut users = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&room_id)
|
||||
.filter_map(|user| {
|
||||
user.ok().filter(|local_user| {
|
||||
local_user.server_name() == self.services.globals.server_name()
|
||||
// additional wrapped check here is to avoid adding remote users
|
||||
// who are in the admin room to the list of local users (would fail auth check)
|
||||
&& (local_user.server_name()
|
||||
== self.services.globals.server_name()
|
||||
&& !self.services
|
||||
.users
|
||||
.is_admin(local_user)
|
||||
.unwrap_or(false))
|
||||
})
|
||||
}) {
|
||||
.ready_filter(|user| self.services.globals.user_is_local(user))
|
||||
.boxed();
|
||||
|
||||
while let Some(local_user) = users.next().await {
|
||||
if self.services.users.is_admin(local_user).await {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Attempting leave for user {} in room {}", &local_user, &room_id);
|
||||
if let Err(e) = leave_room(self.services, &local_user, &room_id, None).await {
|
||||
if let Err(e) = leave_room(self.services, local_user, &room_id, None).await {
|
||||
error!(
|
||||
"Error attempting to make local user {} leave room {} during room banning: {}",
|
||||
&local_user, &room_id, e
|
||||
@@ -214,12 +209,14 @@ async fn ban_room(
|
||||
}
|
||||
|
||||
// remove any local aliases, ignore errors
|
||||
for ref local_alias in self
|
||||
for local_alias in &self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&room_id)
|
||||
.filter_map(Result::ok)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
{
|
||||
_ = self
|
||||
.services
|
||||
@@ -230,10 +227,10 @@ async fn ban_room(
|
||||
}
|
||||
|
||||
// unpublish from room directory, ignore errors
|
||||
_ = self.services.rooms.directory.set_not_public(&room_id);
|
||||
self.services.rooms.directory.set_not_public(&room_id);
|
||||
|
||||
if disable_federation {
|
||||
self.services.rooms.metadata.disable_room(&room_id, true)?;
|
||||
self.services.rooms.metadata.disable_room(&room_id, true);
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Room banned, removed all our local users, and disabled incoming federation with room.",
|
||||
));
|
||||
@@ -268,7 +265,7 @@ async fn ban_list_of_rooms(&self, force: bool, disable_federation: bool) -> Resu
|
||||
for &room in &rooms_s {
|
||||
match <&RoomOrAliasId>::try_from(room) {
|
||||
Ok(room_alias_or_id) => {
|
||||
if let Some(admin_room_id) = self.services.admin.get_admin_room()? {
|
||||
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) {
|
||||
info!("User specified admin room in bulk ban list, ignoring");
|
||||
continue;
|
||||
@@ -300,43 +297,48 @@ async fn ban_list_of_rooms(&self, force: bool, disable_federation: bool) -> Resu
|
||||
if room_alias_or_id.is_room_alias_id() {
|
||||
match RoomAliasId::parse(room_alias_or_id) {
|
||||
Ok(room_alias) => {
|
||||
let room_id =
|
||||
if let Some(room_id) = self.services.rooms.alias.resolve_local_alias(&room_alias)? {
|
||||
room_id
|
||||
} else {
|
||||
debug!(
|
||||
"We don't have this room alias to a room ID locally, attempting to fetch room \
|
||||
ID over federation"
|
||||
);
|
||||
let room_id = if let Ok(room_id) = self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&room_alias)
|
||||
.await
|
||||
{
|
||||
room_id
|
||||
} else {
|
||||
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, None)
|
||||
.await
|
||||
{
|
||||
Ok((room_id, servers)) => {
|
||||
debug!(
|
||||
?room_id,
|
||||
?servers,
|
||||
"Got federation response fetching room ID for {room}",
|
||||
);
|
||||
room_id
|
||||
},
|
||||
Err(e) => {
|
||||
// don't fail if force blocking
|
||||
if force {
|
||||
warn!("Failed to resolve room alias {room} to a room ID: {e}");
|
||||
continue;
|
||||
}
|
||||
match self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_alias(&room_alias, None)
|
||||
.await
|
||||
{
|
||||
Ok((room_id, servers)) => {
|
||||
debug!(
|
||||
?room_id,
|
||||
?servers,
|
||||
"Got federation response fetching room ID for {room}",
|
||||
);
|
||||
room_id
|
||||
},
|
||||
Err(e) => {
|
||||
// don't fail if force blocking
|
||||
if force {
|
||||
warn!("Failed to resolve room alias {room} to a room ID: {e}");
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to resolve room alias {room} to a room ID: {e}"
|
||||
)));
|
||||
},
|
||||
}
|
||||
};
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to resolve room alias {room} to a room ID: {e}"
|
||||
)));
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
room_ids.push(room_id);
|
||||
},
|
||||
@@ -374,74 +376,52 @@ async fn ban_list_of_rooms(&self, force: bool, disable_federation: bool) -> Resu
|
||||
}
|
||||
|
||||
for room_id in room_ids {
|
||||
if self
|
||||
.services
|
||||
.rooms
|
||||
.metadata
|
||||
.ban_room(&room_id, true)
|
||||
.is_ok()
|
||||
{
|
||||
debug!("Banned {room_id} successfully");
|
||||
room_ban_count = room_ban_count.saturating_add(1);
|
||||
}
|
||||
self.services.rooms.metadata.ban_room(&room_id, true);
|
||||
|
||||
debug!("Banned {room_id} successfully");
|
||||
room_ban_count = room_ban_count.saturating_add(1);
|
||||
|
||||
debug!("Making all users leave the room {}", &room_id);
|
||||
if force {
|
||||
for local_user in self
|
||||
let mut users = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&room_id)
|
||||
.filter_map(|user| {
|
||||
user.ok().filter(|local_user| {
|
||||
local_user.server_name() == self.services.globals.server_name()
|
||||
// additional wrapped check here is to avoid adding remote
|
||||
// users who are in the admin room to the list of local
|
||||
// users (would fail auth check)
|
||||
&& (local_user.server_name()
|
||||
== self.services.globals.server_name()
|
||||
// since this is a force operation, assume user is an
|
||||
// admin if somehow this fails
|
||||
&& self.services
|
||||
.users
|
||||
.is_admin(local_user)
|
||||
.unwrap_or(true))
|
||||
})
|
||||
}) {
|
||||
.ready_filter(|user| self.services.globals.user_is_local(user))
|
||||
.boxed();
|
||||
|
||||
while let Some(local_user) = users.next().await {
|
||||
debug!(
|
||||
"Attempting leave for user {} in room {} (forced, ignoring all errors, evicting admins too)",
|
||||
&local_user, room_id
|
||||
"Attempting leave for user {local_user} in room {room_id} (forced, ignoring all errors, evicting \
|
||||
admins too)",
|
||||
);
|
||||
if let Err(e) = leave_room(self.services, &local_user, &room_id, None).await {
|
||||
|
||||
if let Err(e) = leave_room(self.services, local_user, &room_id, None).await {
|
||||
warn!(%e, "Failed to leave room");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for local_user in self
|
||||
let mut users = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&room_id)
|
||||
.filter_map(|user| {
|
||||
user.ok().filter(|local_user| {
|
||||
local_user.server_name() == self.services.globals.server_name()
|
||||
// additional wrapped check here is to avoid adding remote
|
||||
// users who are in the admin room to the list of local
|
||||
// users (would fail auth check)
|
||||
&& (local_user.server_name()
|
||||
== self.services.globals.server_name()
|
||||
&& !self.services
|
||||
.users
|
||||
.is_admin(local_user)
|
||||
.unwrap_or(false))
|
||||
})
|
||||
}) {
|
||||
debug!("Attempting leave for user {} in room {}", &local_user, &room_id);
|
||||
if let Err(e) = leave_room(self.services, &local_user, &room_id, None).await {
|
||||
.ready_filter(|user| self.services.globals.user_is_local(user))
|
||||
.boxed();
|
||||
|
||||
while let Some(local_user) = users.next().await {
|
||||
if self.services.users.is_admin(local_user).await {
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Attempting leave for user {local_user} in room {room_id}");
|
||||
if let Err(e) = leave_room(self.services, local_user, &room_id, None).await {
|
||||
error!(
|
||||
"Error attempting to make local user {} leave room {} during bulk room banning: {}",
|
||||
&local_user, &room_id, e
|
||||
"Error attempting to make local user {local_user} leave room {room_id} during bulk room \
|
||||
banning: {e}",
|
||||
);
|
||||
|
||||
return Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Error attempting to make local user {} leave room {} during room banning (room is still \
|
||||
banned but not removing any more users and not banning any more rooms): {}\nIf you would \
|
||||
@@ -453,26 +433,26 @@ async fn ban_list_of_rooms(&self, force: bool, disable_federation: bool) -> Resu
|
||||
}
|
||||
|
||||
// remove any local aliases, ignore errors
|
||||
for ref local_alias in self
|
||||
.services
|
||||
self.services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&room_id)
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
_ = self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.remove_alias(local_alias, &self.services.globals.server_user)
|
||||
.await;
|
||||
}
|
||||
.map(ToOwned::to_owned)
|
||||
.for_each(|local_alias| async move {
|
||||
self.services
|
||||
.rooms
|
||||
.alias
|
||||
.remove_alias(&local_alias, &self.services.globals.server_user)
|
||||
.await
|
||||
.ok();
|
||||
})
|
||||
.await;
|
||||
|
||||
// unpublish from room directory, ignore errors
|
||||
_ = self.services.rooms.directory.set_not_public(&room_id);
|
||||
self.services.rooms.directory.set_not_public(&room_id);
|
||||
|
||||
if disable_federation {
|
||||
self.services.rooms.metadata.disable_room(&room_id, true)?;
|
||||
self.services.rooms.metadata.disable_room(&room_id, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,7 +483,7 @@ async fn unban_room(&self, enable_federation: bool, room: Box<RoomOrAliasId>) ->
|
||||
|
||||
debug!("Room specified is a room ID, unbanning room ID");
|
||||
|
||||
self.services.rooms.metadata.ban_room(&room_id, false)?;
|
||||
self.services.rooms.metadata.ban_room(&room_id, false);
|
||||
|
||||
room_id
|
||||
} else if room.is_room_alias_id() {
|
||||
@@ -522,7 +502,13 @@ async fn unban_room(&self, enable_federation: bool, room: Box<RoomOrAliasId>) ->
|
||||
get_alias_helper to fetch room ID remotely"
|
||||
);
|
||||
|
||||
let room_id = if let Some(room_id) = self.services.rooms.alias.resolve_local_alias(&room_alias)? {
|
||||
let room_id = if let Ok(room_id) = self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&room_alias)
|
||||
.await
|
||||
{
|
||||
room_id
|
||||
} else {
|
||||
debug!("We don't have this room alias to a room ID locally, attempting to fetch room ID over federation");
|
||||
@@ -546,7 +532,7 @@ async fn unban_room(&self, enable_federation: bool, room: Box<RoomOrAliasId>) ->
|
||||
}
|
||||
};
|
||||
|
||||
self.services.rooms.metadata.ban_room(&room_id, false)?;
|
||||
self.services.rooms.metadata.ban_room(&room_id, false);
|
||||
|
||||
room_id
|
||||
} else {
|
||||
@@ -557,7 +543,7 @@ async fn unban_room(&self, enable_federation: bool, room: Box<RoomOrAliasId>) ->
|
||||
};
|
||||
|
||||
if enable_federation {
|
||||
self.services.rooms.metadata.disable_room(&room_id, false)?;
|
||||
self.services.rooms.metadata.disable_room(&room_id, false);
|
||||
return Ok(RoomMessageEventContent::text_plain("Room unbanned."));
|
||||
}
|
||||
|
||||
@@ -569,45 +555,42 @@ async fn unban_room(&self, enable_federation: bool, room: Box<RoomOrAliasId>) ->
|
||||
|
||||
#[admin_command]
|
||||
async fn list_banned_rooms(&self, no_details: bool) -> Result<RoomMessageEventContent> {
|
||||
let rooms = self
|
||||
let room_ids: Vec<OwnedRoomId> = self
|
||||
.services
|
||||
.rooms
|
||||
.metadata
|
||||
.list_banned_rooms()
|
||||
.collect::<Result<Vec<_>, _>>();
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
match rooms {
|
||||
Ok(room_ids) => {
|
||||
if room_ids.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain("No rooms are banned."));
|
||||
}
|
||||
|
||||
let mut rooms = room_ids
|
||||
.into_iter()
|
||||
.map(|room_id| get_room_info(self.services, &room_id))
|
||||
.collect::<Vec<_>>();
|
||||
rooms.sort_by_key(|r| r.1);
|
||||
rooms.reverse();
|
||||
|
||||
let output_plain = format!(
|
||||
"Rooms Banned ({}):\n```\n{}\n```",
|
||||
rooms.len(),
|
||||
rooms
|
||||
.iter()
|
||||
.map(|(id, members, name)| if no_details {
|
||||
format!("{id}")
|
||||
} else {
|
||||
format!("{id}\tMembers: {members}\tName: {name}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(output_plain))
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Failed to list banned rooms: {e}");
|
||||
Ok(RoomMessageEventContent::text_plain(format!("Unable to list banned rooms: {e}")))
|
||||
},
|
||||
if room_ids.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain("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 output_plain = format!(
|
||||
"Rooms Banned ({}):\n```\n{}\n```",
|
||||
rooms.len(),
|
||||
rooms
|
||||
.iter()
|
||||
.map(|(id, members, name)| if no_details {
|
||||
format!("{id}")
|
||||
} else {
|
||||
format!("{id}\tMembers: {members}\tName: {name}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(output_plain))
|
||||
}
|
||||
|
||||
@@ -21,7 +21,10 @@ pub(super) async fn uptime(&self) -> Result<RoomMessageEventContent> {
|
||||
#[admin_command]
|
||||
pub(super) async fn show_config(&self) -> Result<RoomMessageEventContent> {
|
||||
// Construct and send the response
|
||||
Ok(RoomMessageEventContent::text_plain(format!("{}", self.services.globals.config)))
|
||||
Ok(RoomMessageEventContent::text_markdown(format!(
|
||||
"```\n{}\n```",
|
||||
self.services.globals.config
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
@@ -104,7 +107,7 @@ pub(super) async fn backup_database(&self) -> Result<RoomMessageEventContent> {
|
||||
.runtime()
|
||||
.spawn_blocking(move || match globals.db.backup() {
|
||||
Ok(()) => String::new(),
|
||||
Err(e) => (*e).to_string(),
|
||||
Err(e) => e.to_string(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
||||
+376
-119
@@ -1,7 +1,13 @@
|
||||
use std::{collections::BTreeMap, fmt::Write as _};
|
||||
|
||||
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room};
|
||||
use conduit::{error, info, utils, warn, PduBuilder, Result};
|
||||
use conduit::{
|
||||
debug_warn, error, info, is_equal_to,
|
||||
utils::{self, ReadyExt},
|
||||
warn, PduBuilder, Result,
|
||||
};
|
||||
use conduit_api::client::{leave_all_rooms, update_avatar_url, update_displayname};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
events::{
|
||||
room::{
|
||||
@@ -10,11 +16,10 @@ use ruma::{
|
||||
redaction::RoomRedactionEventContent,
|
||||
},
|
||||
tag::{TagEvent, TagEventContent, TagInfo},
|
||||
RoomAccountDataEventType, StateEventType, TimelineEventType,
|
||||
RoomAccountDataEventType, StateEventType,
|
||||
},
|
||||
EventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, RoomId,
|
||||
EventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
|
||||
use crate::{
|
||||
admin_command, get_room_info,
|
||||
@@ -22,19 +27,23 @@ use crate::{
|
||||
};
|
||||
|
||||
const AUTO_GEN_PASSWORD_LENGTH: usize = 25;
|
||||
const BULK_JOIN_REASON: &str = "Bulk force joining this room as initiated by the server admin.";
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn list_users(&self) -> Result<RoomMessageEventContent> {
|
||||
match self.services.users.list_local_users() {
|
||||
Ok(users) => {
|
||||
let mut plain_msg = format!("Found {} local user account(s):\n```\n", users.len());
|
||||
plain_msg += users.join("\n").as_str();
|
||||
plain_msg += "\n```";
|
||||
let users = self
|
||||
.services
|
||||
.users
|
||||
.list_local_users()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(plain_msg))
|
||||
},
|
||||
Err(e) => Ok(RoomMessageEventContent::text_plain(e.to_string())),
|
||||
}
|
||||
let mut plain_msg = format!("Found {} local user account(s):\n```\n", users.len());
|
||||
plain_msg += users.join("\n").as_str();
|
||||
plain_msg += "\n```";
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(plain_msg))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
@@ -42,7 +51,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
|
||||
// Validate user id
|
||||
let user_id = parse_local_user_id(self.services, &username)?;
|
||||
|
||||
if self.services.users.exists(&user_id)? {
|
||||
if self.services.users.exists(&user_id).await {
|
||||
return Ok(RoomMessageEventContent::text_plain(format!("Userid {user_id} already exists")));
|
||||
}
|
||||
|
||||
@@ -77,43 +86,51 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
|
||||
|
||||
self.services
|
||||
.users
|
||||
.set_displayname(&user_id, Some(displayname))
|
||||
.await?;
|
||||
.set_displayname(&user_id, Some(displayname));
|
||||
|
||||
// Initial account data
|
||||
self.services.account_data.update(
|
||||
None,
|
||||
&user_id,
|
||||
ruma::events::GlobalAccountDataEventType::PushRules
|
||||
.to_string()
|
||||
.into(),
|
||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
||||
content: ruma::events::push_rules::PushRulesEventContent {
|
||||
global: ruma::push::Ruleset::server_default(&user_id),
|
||||
},
|
||||
})
|
||||
.expect("to json value always works"),
|
||||
)?;
|
||||
self.services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
&user_id,
|
||||
ruma::events::GlobalAccountDataEventType::PushRules
|
||||
.to_string()
|
||||
.into(),
|
||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
||||
content: ruma::events::push_rules::PushRulesEventContent {
|
||||
global: ruma::push::Ruleset::server_default(&user_id),
|
||||
},
|
||||
})
|
||||
.expect("to json value always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !self.services.globals.config.auto_join_rooms.is_empty() {
|
||||
for room in &self.services.globals.config.auto_join_rooms {
|
||||
let Ok(room_id) = self.services.rooms.alias.resolve(room).await else {
|
||||
error!(%user_id, "Failed to resolve room alias to room ID when attempting to auto join {room}, skipping");
|
||||
continue;
|
||||
};
|
||||
|
||||
if !self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(self.services.globals.server_name(), room)?
|
||||
.server_in_room(self.services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
warn!("Skipping room {room} to automatically join as we have never joined before.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_id_server_name) = room.server_name() {
|
||||
if let Some(room_server_name) = room.server_name() {
|
||||
match join_room_by_id_helper(
|
||||
self.services,
|
||||
&user_id,
|
||||
room,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[room_id_server_name.to_owned(), self.services.globals.server_name().to_owned()],
|
||||
&[self.services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||
None,
|
||||
&None,
|
||||
)
|
||||
@@ -123,6 +140,13 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
|
||||
info!("Automatically joined room {room} for user {user_id}");
|
||||
},
|
||||
Err(e) => {
|
||||
self.services
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::text_plain(format!(
|
||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
// don't return this error so we don't fail registrations
|
||||
error!("Failed to automatically join room {room} for user {user_id}: {e}");
|
||||
},
|
||||
@@ -135,13 +159,14 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
|
||||
|
||||
// if this account creation is from the CLI / --execute, invite the first user
|
||||
// to admin room
|
||||
if let Some(admin_room) = self.services.admin.get_admin_room()? {
|
||||
if let Ok(admin_room) = self.services.admin.get_admin_room().await {
|
||||
if self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(&admin_room)?
|
||||
== Some(1)
|
||||
.room_joined_count(&admin_room)
|
||||
.await
|
||||
.is_ok_and(is_equal_to!(1))
|
||||
{
|
||||
self.services.admin.make_user_admin(&user_id).await?;
|
||||
|
||||
@@ -167,7 +192,7 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
|
||||
));
|
||||
}
|
||||
|
||||
self.services.users.deactivate_account(&user_id)?;
|
||||
self.services.users.deactivate_account(&user_id).await?;
|
||||
|
||||
if !no_leave_rooms {
|
||||
self.services
|
||||
@@ -175,17 +200,22 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
|
||||
.send_message(RoomMessageEventContent::text_plain(format!(
|
||||
"Making {user_id} leave all rooms after deactivation..."
|
||||
)))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let all_joined_rooms: Vec<OwnedRoomId> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&user_id)
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
full_user_deactivate(self.services, &user_id, all_joined_rooms).await?;
|
||||
full_user_deactivate(self.services, &user_id, &all_joined_rooms).await?;
|
||||
update_displayname(self.services, &user_id, None, &all_joined_rooms).await?;
|
||||
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms).await?;
|
||||
leave_all_rooms(self.services, &user_id).await;
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
@@ -238,15 +268,16 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
|
||||
let mut admins = Vec::new();
|
||||
|
||||
for username in usernames {
|
||||
match parse_active_local_user_id(self.services, username) {
|
||||
match parse_active_local_user_id(self.services, username).await {
|
||||
Ok(user_id) => {
|
||||
if self.services.users.is_admin(&user_id)? && !force {
|
||||
if self.services.users.is_admin(&user_id).await && !force {
|
||||
self.services
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::text_plain(format!(
|
||||
"{username} is an admin and --force is not set, skipping over"
|
||||
)))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
admins.push(username);
|
||||
continue;
|
||||
}
|
||||
@@ -258,7 +289,8 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
|
||||
.send_message(RoomMessageEventContent::text_plain(format!(
|
||||
"{username} is the server service account, skipping over"
|
||||
)))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -270,7 +302,8 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
|
||||
.send_message(RoomMessageEventContent::text_plain(format!(
|
||||
"{username} is not a valid username, skipping over: {e}"
|
||||
)))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
continue;
|
||||
},
|
||||
}
|
||||
@@ -279,7 +312,7 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
|
||||
let mut deactivation_count: usize = 0;
|
||||
|
||||
for user_id in user_ids {
|
||||
match self.services.users.deactivate_account(&user_id) {
|
||||
match self.services.users.deactivate_account(&user_id).await {
|
||||
Ok(()) => {
|
||||
deactivation_count = deactivation_count.saturating_add(1);
|
||||
if !no_leave_rooms {
|
||||
@@ -289,16 +322,26 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&user_id)
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
full_user_deactivate(self.services, &user_id, all_joined_rooms).await?;
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
full_user_deactivate(self.services, &user_id, &all_joined_rooms).await?;
|
||||
update_displayname(self.services, &user_id, None, &all_joined_rooms)
|
||||
.await
|
||||
.ok();
|
||||
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms)
|
||||
.await
|
||||
.ok();
|
||||
leave_all_rooms(self.services, &user_id).await;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
self.services
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::text_plain(format!("Failed deactivating user: {e}")))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -326,9 +369,9 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result<RoomMess
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&user_id)
|
||||
.filter_map(Result::ok)
|
||||
.map(|room_id| get_room_info(self.services, &room_id))
|
||||
.collect();
|
||||
.then(|room_id| get_room_info(self.services, room_id))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
if rooms.is_empty() {
|
||||
return Ok(RoomMessageEventContent::text_plain("User is not in any rooms."));
|
||||
@@ -350,18 +393,247 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result<RoomMess
|
||||
Ok(RoomMessageEventContent::notice_markdown(output_plain))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn force_join_list_of_local_users(
|
||||
&self, room_id: OwnedRoomOrAliasId, yes_i_want_to_do_this: bool,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
if self.body.len() < 2 || !self.body[0].trim().starts_with("```") || self.body.last().unwrap_or(&"").trim() != "```"
|
||||
{
|
||||
return Ok(RoomMessageEventContent::text_plain(
|
||||
"Expected code block in command body. Add --help for details.",
|
||||
));
|
||||
}
|
||||
|
||||
if !yes_i_want_to_do_this {
|
||||
return Ok(RoomMessageEventContent::notice_markdown(
|
||||
"You must pass the --yes-i-want-to-do-this-flag to ensure you really want to force bulk join all \
|
||||
specified local users.",
|
||||
));
|
||||
}
|
||||
|
||||
let Ok(admin_room) = self.services.admin.get_admin_room().await else {
|
||||
return Ok(RoomMessageEventContent::notice_markdown(
|
||||
"There is not an admin room to check for server admins.",
|
||||
));
|
||||
};
|
||||
|
||||
let (room_id, servers) = self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_with_servers(&room_id, None)
|
||||
.await?;
|
||||
|
||||
if !self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(self.services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
return Ok(RoomMessageEventContent::notice_markdown("We are not joined in this room."));
|
||||
}
|
||||
|
||||
let server_admins: Vec<_> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.active_local_users_in_room(&admin_room)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
if !self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&room_id)
|
||||
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
|
||||
.await
|
||||
{
|
||||
return Ok(RoomMessageEventContent::notice_markdown(
|
||||
"There is not a single server admin in the room.",
|
||||
));
|
||||
}
|
||||
|
||||
let usernames = self
|
||||
.body
|
||||
.to_vec()
|
||||
.drain(1..self.body.len().saturating_sub(1))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut user_ids: Vec<OwnedUserId> = Vec::with_capacity(usernames.len());
|
||||
|
||||
for username in usernames {
|
||||
match parse_active_local_user_id(self.services, username).await {
|
||||
Ok(user_id) => {
|
||||
// don't make the server service account join
|
||||
if user_id == self.services.globals.server_user {
|
||||
self.services
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::text_plain(format!(
|
||||
"{username} is the server service account, skipping over"
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
continue;
|
||||
}
|
||||
|
||||
user_ids.push(user_id);
|
||||
},
|
||||
Err(e) => {
|
||||
self.services
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::text_plain(format!(
|
||||
"{username} is not a valid username, skipping over: {e}"
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
continue;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let mut failed_joins: usize = 0;
|
||||
let mut successful_joins: usize = 0;
|
||||
|
||||
for user_id in user_ids {
|
||||
match join_room_by_id_helper(
|
||||
self.services,
|
||||
&user_id,
|
||||
&room_id,
|
||||
Some(String::from(BULK_JOIN_REASON)),
|
||||
&servers,
|
||||
None,
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_res) => {
|
||||
successful_joins = successful_joins.saturating_add(1);
|
||||
},
|
||||
Err(e) => {
|
||||
debug_warn!("Failed force joining {user_id} to {room_id} during bulk join: {e}");
|
||||
failed_joins = failed_joins.saturating_add(1);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"{successful_joins} local users have been joined to {room_id}. {failed_joins} joins failed.",
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn force_join_all_local_users(
|
||||
&self, room_id: OwnedRoomOrAliasId, yes_i_want_to_do_this: bool,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
if !yes_i_want_to_do_this {
|
||||
return Ok(RoomMessageEventContent::notice_markdown(
|
||||
"You must pass the --yes-i-want-to-do-this-flag to ensure you really want to force bulk join all local \
|
||||
users.",
|
||||
));
|
||||
}
|
||||
|
||||
let Ok(admin_room) = self.services.admin.get_admin_room().await else {
|
||||
return Ok(RoomMessageEventContent::notice_markdown(
|
||||
"There is not an admin room to check for server admins.",
|
||||
));
|
||||
};
|
||||
|
||||
let (room_id, servers) = self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_with_servers(&room_id, None)
|
||||
.await?;
|
||||
|
||||
if !self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(self.services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
return Ok(RoomMessageEventContent::notice_markdown("We are not joined in this room."));
|
||||
}
|
||||
|
||||
let server_admins: Vec<_> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.active_local_users_in_room(&admin_room)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
if !self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&room_id)
|
||||
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
|
||||
.await
|
||||
{
|
||||
return Ok(RoomMessageEventContent::notice_markdown(
|
||||
"There is not a single server admin in the room.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut failed_joins: usize = 0;
|
||||
let mut successful_joins: usize = 0;
|
||||
|
||||
for user_id in &self
|
||||
.services
|
||||
.users
|
||||
.list_local_users()
|
||||
.map(UserId::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
{
|
||||
match join_room_by_id_helper(
|
||||
self.services,
|
||||
user_id,
|
||||
&room_id,
|
||||
Some(String::from(BULK_JOIN_REASON)),
|
||||
&servers,
|
||||
None,
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_res) => {
|
||||
successful_joins = successful_joins.saturating_add(1);
|
||||
},
|
||||
Err(e) => {
|
||||
debug_warn!("Failed force joining {user_id} to {room_id} during bulk join: {e}");
|
||||
failed_joins = failed_joins.saturating_add(1);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"{successful_joins} local users have been joined to {room_id}. {failed_joins} joins failed.",
|
||||
)))
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn force_join_room(
|
||||
&self, user_id: String, room_id: OwnedRoomOrAliasId,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
let room_id = self.services.rooms.alias.resolve(&room_id).await?;
|
||||
let (room_id, servers) = self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_with_servers(&room_id, None)
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
join_room_by_id_helper(self.services, &user_id, &room_id, None, &[], None, &None).await?;
|
||||
join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers, None, &None).await?;
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"{user_id} has been joined to {room_id}.",
|
||||
@@ -404,10 +676,9 @@ pub(super) async fn force_demote(
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomPowerLevels, "")?
|
||||
.as_ref()
|
||||
.and_then(|event| serde_json::from_str(event.content.get()).ok()?)
|
||||
.and_then(|content: RoomPowerLevelsEventContent| content.into());
|
||||
.room_state_get_content::<RoomPowerLevelsEventContent>(&room_id, &StateEventType::RoomPowerLevels, "")
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let user_can_demote_self = room_power_levels
|
||||
.as_ref()
|
||||
@@ -417,9 +688,9 @@ pub(super) async fn force_demote(
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomCreate, "")?
|
||||
.as_ref()
|
||||
.is_some_and(|event| event.sender == user_id);
|
||||
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.is_ok_and(|event| event.sender == user_id);
|
||||
|
||||
if !user_can_demote_self {
|
||||
return Ok(RoomMessageEventContent::notice_markdown(
|
||||
@@ -435,14 +706,7 @@ pub(super) async fn force_demote(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomPowerLevels,
|
||||
content: to_raw_value(&power_levels_content).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
PduBuilder::state(String::new(), &power_levels_content),
|
||||
&user_id,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
@@ -473,33 +737,33 @@ pub(super) async fn make_user_admin(&self, user_id: String) -> Result<RoomMessag
|
||||
pub(super) async fn put_room_tag(
|
||||
&self, user_id: String, room_id: Box<RoomId>, tag: String,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id)?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
let event = self
|
||||
let mut tags_event = self
|
||||
.services
|
||||
.account_data
|
||||
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
|
||||
|
||||
let mut tags_event = event.map_or_else(
|
||||
|| TagEvent {
|
||||
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
|
||||
.await
|
||||
.unwrap_or(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
},
|
||||
|e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"),
|
||||
);
|
||||
});
|
||||
|
||||
tags_event
|
||||
.content
|
||||
.tags
|
||||
.insert(tag.clone().into(), TagInfo::new());
|
||||
|
||||
self.services.account_data.update(
|
||||
Some(&room_id),
|
||||
&user_id,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)?;
|
||||
self.services
|
||||
.account_data
|
||||
.update(
|
||||
Some(&room_id),
|
||||
&user_id,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Successfully updated room account data for {user_id} and room {room_id} with tag {tag}"
|
||||
@@ -510,30 +774,30 @@ pub(super) async fn put_room_tag(
|
||||
pub(super) async fn delete_room_tag(
|
||||
&self, user_id: String, room_id: Box<RoomId>, tag: String,
|
||||
) -> Result<RoomMessageEventContent> {
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id)?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
let event = self
|
||||
let mut tags_event = self
|
||||
.services
|
||||
.account_data
|
||||
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
|
||||
|
||||
let mut tags_event = event.map_or_else(
|
||||
|| TagEvent {
|
||||
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
|
||||
.await
|
||||
.unwrap_or(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
},
|
||||
|e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"),
|
||||
);
|
||||
});
|
||||
|
||||
tags_event.content.tags.remove(&tag.clone().into());
|
||||
|
||||
self.services.account_data.update(
|
||||
Some(&room_id),
|
||||
&user_id,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)?;
|
||||
self.services
|
||||
.account_data
|
||||
.update(
|
||||
Some(&room_id),
|
||||
&user_id,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(RoomMessageEventContent::text_plain(format!(
|
||||
"Successfully updated room account data for {user_id} and room {room_id}, deleting room tag {tag}"
|
||||
@@ -542,21 +806,18 @@ pub(super) async fn delete_room_tag(
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn get_room_tags(&self, user_id: String, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id)?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
let event = self
|
||||
let tags_event = self
|
||||
.services
|
||||
.account_data
|
||||
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
|
||||
|
||||
let tags_event = event.map_or_else(
|
||||
|| TagEvent {
|
||||
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
|
||||
.await
|
||||
.unwrap_or(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
},
|
||||
|e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"),
|
||||
);
|
||||
});
|
||||
|
||||
Ok(RoomMessageEventContent::notice_markdown(format!(
|
||||
"```\n{:#?}\n```",
|
||||
@@ -566,11 +827,12 @@ pub(super) async fn get_room_tags(&self, user_id: String, room_id: Box<RoomId>)
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn redact_event(&self, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
|
||||
let Some(event) = self
|
||||
let Ok(event) = self
|
||||
.services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_non_outlier_pdu(&event_id)?
|
||||
.get_non_outlier_pdu(&event_id)
|
||||
.await
|
||||
else {
|
||||
return Ok(RoomMessageEventContent::text_plain("Event does not exist in our database."));
|
||||
};
|
||||
@@ -599,16 +861,11 @@ pub(super) async fn redact_event(&self, event_id: Box<EventId>) -> Result<RoomMe
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomRedaction,
|
||||
content: to_raw_value(&RoomRedactionEventContent {
|
||||
redacts: Some(event.event_id.clone()),
|
||||
..PduBuilder::timeline(&RoomRedactionEventContent {
|
||||
redacts: Some(event.event_id.clone().into()),
|
||||
reason: Some(reason),
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: None,
|
||||
redacts: Some(event.event_id),
|
||||
timestamp: None,
|
||||
},
|
||||
&sender_user,
|
||||
&room_id,
|
||||
|
||||
@@ -124,4 +124,31 @@ pub(super) enum UserCommand {
|
||||
RedactEvent {
|
||||
event_id: Box<EventId>,
|
||||
},
|
||||
|
||||
/// - Force joins a specified list of local users to join the specified
|
||||
/// room.
|
||||
///
|
||||
/// Specify a codeblock of usernames.
|
||||
///
|
||||
/// At least 1 server admin must be in the room to reduce abuse.
|
||||
///
|
||||
/// Requires the `--yes-i-want-to-do-this` flag.
|
||||
ForceJoinListOfLocalUsers {
|
||||
room_id: OwnedRoomOrAliasId,
|
||||
|
||||
#[arg(long)]
|
||||
yes_i_want_to_do_this: bool,
|
||||
},
|
||||
|
||||
/// - Force joins all local users to the specified room.
|
||||
///
|
||||
/// At least 1 server admin must be in the room to reduce abuse.
|
||||
///
|
||||
/// Requires the `--yes-i-want-to-do-this` flag.
|
||||
ForceJoinAllLocalUsers {
|
||||
room_id: OwnedRoomOrAliasId,
|
||||
|
||||
#[arg(long)]
|
||||
yes_i_want_to_do_this: bool,
|
||||
},
|
||||
}
|
||||
|
||||
+10
-12
@@ -8,23 +8,21 @@ pub(crate) fn escape_html(s: &str) -> String {
|
||||
.replace('>', ">")
|
||||
}
|
||||
|
||||
pub(crate) fn get_room_info(services: &Services, id: &RoomId) -> (OwnedRoomId, u64, String) {
|
||||
pub(crate) async fn get_room_info(services: &Services, room_id: &RoomId) -> (OwnedRoomId, u64, String) {
|
||||
(
|
||||
id.into(),
|
||||
room_id.into(),
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.room_joined_count(room_id)
|
||||
.await
|
||||
.unwrap_or(0),
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_name(id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| id.to_string()),
|
||||
.get_name(room_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| room_id.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,14 +44,14 @@ pub(crate) fn parse_local_user_id(services: &Services, user_id: &str) -> Result<
|
||||
}
|
||||
|
||||
/// Parses user ID that is an active (not guest or deactivated) local user
|
||||
pub(crate) fn parse_active_local_user_id(services: &Services, user_id: &str) -> Result<OwnedUserId> {
|
||||
pub(crate) async fn parse_active_local_user_id(services: &Services, user_id: &str) -> Result<OwnedUserId> {
|
||||
let user_id = parse_local_user_id(services, user_id)?;
|
||||
|
||||
if !services.users.exists(&user_id)? {
|
||||
if !services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id:?} does not exist on this server.");
|
||||
}
|
||||
|
||||
if services.users.is_deactivated(&user_id)? {
|
||||
if services.users.is_deactivated(&user_id).await? {
|
||||
return Err!("User {user_id:?} is deactivated.");
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -45,7 +45,7 @@ conduit-core.workspace = true
|
||||
conduit-database.workspace = true
|
||||
conduit-service.workspace = true
|
||||
const-str.workspace = true
|
||||
futures-util.workspace = true
|
||||
futures.workspace = true
|
||||
hmac.workspace = true
|
||||
http.workspace = true
|
||||
http-body-util.workspace = true
|
||||
@@ -59,7 +59,7 @@ ruma.workspace = true
|
||||
serde_html_form.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
sha-1.workspace = true
|
||||
sha1.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
|
||||
+164
-110
@@ -2,7 +2,8 @@ use std::fmt::Write;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduit::{debug_info, error, info, utils, warn, Error, PduBuilder, Result};
|
||||
use conduit::{debug_info, error, info, is_equal_to, utils, utils::ReadyExt, warn, Error, PduBuilder, Result};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use register::RegistrationKind;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
@@ -20,11 +21,10 @@ use ruma::{
|
||||
message::RoomMessageEventContent,
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
GlobalAccountDataEventType, StateEventType, TimelineEventType,
|
||||
GlobalAccountDataEventType, StateEventType,
|
||||
},
|
||||
push, OwnedRoomId, UserId,
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use service::Services;
|
||||
|
||||
use super::{join_room_by_id_helper, DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
|
||||
@@ -48,14 +48,30 @@ pub(crate) async fn get_register_available_route(
|
||||
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_username_availability::v3::Request>,
|
||||
) -> Result<get_username_availability::v3::Response> {
|
||||
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
|
||||
let is_matrix_appservice_irc = body.appservice_info.as_ref().is_some_and(|appservice| {
|
||||
appservice.registration.id == "irc"
|
||||
|| appservice.registration.id.contains("matrix-appservice-irc")
|
||||
|| appservice.registration.id.contains("matrix_appservice_irc")
|
||||
});
|
||||
|
||||
// don't force the username lowercase if it's from matrix-appservice-irc
|
||||
let body_username = if is_matrix_appservice_irc {
|
||||
body.username.clone()
|
||||
} else {
|
||||
body.username.to_lowercase()
|
||||
};
|
||||
|
||||
// Validate user id
|
||||
let user_id = UserId::parse_with_server_name(body.username.to_lowercase(), services.globals.server_name())
|
||||
let user_id = UserId::parse_with_server_name(body_username, services.globals.server_name())
|
||||
.ok()
|
||||
.filter(|user_id| !user_id.is_historical() && services.globals.user_is_local(user_id))
|
||||
.filter(|user_id| {
|
||||
(!user_id.is_historical() || is_matrix_appservice_irc) && services.globals.user_is_local(user_id)
|
||||
})
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
|
||||
|
||||
// Check if username is creative enough
|
||||
if services.users.exists(&user_id)? {
|
||||
if services.users.exists(&user_id).await {
|
||||
return Err(Error::BadRequest(ErrorKind::UserInUse, "Desired user ID is already taken."));
|
||||
}
|
||||
|
||||
@@ -100,8 +116,8 @@ pub(crate) async fn register_route(
|
||||
if !services.globals.allow_registration() && body.appservice_info.is_none() {
|
||||
info!(
|
||||
"Registration disabled and request not from known appservice, rejecting registration attempt for username \
|
||||
{:?}",
|
||||
body.username
|
||||
\"{}\"",
|
||||
body.username.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Registration has been disabled."));
|
||||
}
|
||||
@@ -110,12 +126,12 @@ pub(crate) async fn register_route(
|
||||
|
||||
if is_guest
|
||||
&& (!services.globals.allow_guest_registration()
|
||||
|| (services.globals.allow_registration() && services.globals.config.registration_token.is_some()))
|
||||
|| (services.globals.allow_registration() && services.globals.registration_token.is_some()))
|
||||
{
|
||||
info!(
|
||||
"Guest registration disabled / registration enabled with token configured, rejecting guest registration \
|
||||
attempt, initial device name: {:?}",
|
||||
body.initial_device_display_name
|
||||
attempt, initial device name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::GuestAccessForbidden,
|
||||
@@ -125,24 +141,39 @@ pub(crate) async fn register_route(
|
||||
|
||||
// forbid guests from registering if there is not a real admin user yet. give
|
||||
// generic user error.
|
||||
if is_guest && services.users.count()? < 2 {
|
||||
if is_guest && services.users.count().await < 2 {
|
||||
warn!(
|
||||
"Guest account attempted to register before a real admin user has been registered, rejecting \
|
||||
registration. Guest's initial device name: {:?}",
|
||||
body.initial_device_display_name
|
||||
registration. Guest's initial device name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Registration temporarily disabled."));
|
||||
}
|
||||
|
||||
let user_id = match (&body.username, is_guest) {
|
||||
(Some(username), false) => {
|
||||
let proposed_user_id =
|
||||
UserId::parse_with_server_name(username.to_lowercase(), services.globals.server_name())
|
||||
.ok()
|
||||
.filter(|user_id| !user_id.is_historical() && services.globals.user_is_local(user_id))
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
|
||||
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
|
||||
let is_matrix_appservice_irc = body.appservice_info.as_ref().is_some_and(|appservice| {
|
||||
appservice.registration.id == "irc"
|
||||
|| appservice.registration.id.contains("matrix-appservice-irc")
|
||||
|| appservice.registration.id.contains("matrix_appservice_irc")
|
||||
});
|
||||
|
||||
if services.users.exists(&proposed_user_id)? {
|
||||
// don't force the username lowercase if it's from matrix-appservice-irc
|
||||
let body_username = if is_matrix_appservice_irc {
|
||||
username.clone()
|
||||
} else {
|
||||
username.to_lowercase()
|
||||
};
|
||||
|
||||
let proposed_user_id = UserId::parse_with_server_name(body_username, services.globals.server_name())
|
||||
.ok()
|
||||
.filter(|user_id| {
|
||||
(!user_id.is_historical() || is_matrix_appservice_irc) && services.globals.user_is_local(user_id)
|
||||
})
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
|
||||
|
||||
if services.users.exists(&proposed_user_id).await {
|
||||
return Err(Error::BadRequest(ErrorKind::UserInUse, "Desired user ID is already taken."));
|
||||
}
|
||||
|
||||
@@ -162,7 +193,7 @@ pub(crate) async fn register_route(
|
||||
services.globals.server_name(),
|
||||
)
|
||||
.unwrap();
|
||||
if !services.users.exists(&proposed_user_id)? {
|
||||
if !services.users.exists(&proposed_user_id).await {
|
||||
break proposed_user_id;
|
||||
}
|
||||
},
|
||||
@@ -182,7 +213,7 @@ pub(crate) async fn register_route(
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo;
|
||||
let skip_auth = if services.globals.config.registration_token.is_some() {
|
||||
let skip_auth = if services.globals.registration_token.is_some() {
|
||||
// Registration token required
|
||||
uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow {
|
||||
@@ -210,12 +241,15 @@ pub(crate) async fn register_route(
|
||||
|
||||
if !skip_auth {
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services.uiaa.try_auth(
|
||||
&UserId::parse_with_server_name("", services.globals.server_name()).expect("we know this is valid"),
|
||||
"".into(),
|
||||
auth,
|
||||
&uiaainfo,
|
||||
)?;
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(
|
||||
&UserId::parse_with_server_name("", services.globals.server_name()).expect("we know this is valid"),
|
||||
"".into(),
|
||||
auth,
|
||||
&uiaainfo,
|
||||
)
|
||||
.await?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
@@ -227,7 +261,7 @@ pub(crate) async fn register_route(
|
||||
"".into(),
|
||||
&uiaainfo,
|
||||
&json,
|
||||
)?;
|
||||
);
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
@@ -255,21 +289,23 @@ pub(crate) async fn register_route(
|
||||
|
||||
services
|
||||
.users
|
||||
.set_displayname(&user_id, Some(displayname.clone()))
|
||||
.await?;
|
||||
.set_displayname(&user_id, Some(displayname.clone()));
|
||||
|
||||
// Initial account data
|
||||
services.account_data.update(
|
||||
None,
|
||||
&user_id,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
||||
content: ruma::events::push_rules::PushRulesEventContent {
|
||||
global: push::Ruleset::server_default(&user_id),
|
||||
},
|
||||
})
|
||||
.expect("to json always works"),
|
||||
)?;
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
&user_id,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
||||
content: ruma::events::push_rules::PushRulesEventContent {
|
||||
global: push::Ruleset::server_default(&user_id),
|
||||
},
|
||||
})
|
||||
.expect("to json always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Inhibit login does not work for guests
|
||||
if !is_guest && body.inhibit_login {
|
||||
@@ -294,22 +330,27 @@ pub(crate) async fn register_route(
|
||||
let token = utils::random_string(TOKEN_LENGTH);
|
||||
|
||||
// Create device for this account
|
||||
services.users.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&token,
|
||||
body.initial_device_display_name.clone(),
|
||||
Some(client.to_string()),
|
||||
)?;
|
||||
services
|
||||
.users
|
||||
.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&token,
|
||||
body.initial_device_display_name.clone(),
|
||||
Some(client.to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
debug_info!(%user_id, %device_id, "User account was created");
|
||||
|
||||
let device_display_name = body.initial_device_display_name.clone().unwrap_or_default();
|
||||
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
|
||||
|
||||
// log in conduit admin channel if a non-guest user registered
|
||||
if body.appservice_info.is_none() && !is_guest {
|
||||
if !device_display_name.is_empty() {
|
||||
info!("New user \"{user_id}\" registered on this server with device display name: {device_display_name}");
|
||||
info!(
|
||||
"New user \"{user_id}\" registered on this server with device display name: \"{device_display_name}\""
|
||||
);
|
||||
|
||||
if services.globals.config.admin_room_notices {
|
||||
services
|
||||
@@ -318,7 +359,8 @@ pub(crate) async fn register_route(
|
||||
"New user \"{user_id}\" registered on this server from IP {client} and device display name \
|
||||
\"{device_display_name}\""
|
||||
)))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
} else {
|
||||
info!("New user \"{user_id}\" registered on this server.");
|
||||
@@ -329,7 +371,8 @@ pub(crate) async fn register_route(
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"New user \"{user_id}\" registered on this server from IP {client}"
|
||||
)))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,7 +389,8 @@ pub(crate) async fn register_route(
|
||||
"Guest user \"{user_id}\" with device display name \"{device_display_name}\" registered on \
|
||||
this server from IP {client}"
|
||||
)))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
} else {
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
@@ -357,7 +401,8 @@ pub(crate) async fn register_route(
|
||||
"Guest user \"{user_id}\" with no device display name registered on this server from IP \
|
||||
{client}",
|
||||
)))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,10 +410,15 @@ pub(crate) async fn register_route(
|
||||
// If this is the first real user, grant them admin privileges except for guest
|
||||
// users Note: the server user, @conduit:servername, is generated first
|
||||
if !is_guest {
|
||||
if let Some(admin_room) = services.admin.get_admin_room()? {
|
||||
if services.rooms.state_cache.room_joined_count(&admin_room)? == Some(1) {
|
||||
if let Ok(admin_room) = services.admin.get_admin_room().await {
|
||||
if services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(&admin_room)
|
||||
.await
|
||||
.is_ok_and(is_equal_to!(1))
|
||||
{
|
||||
services.admin.make_user_admin(&user_id).await?;
|
||||
|
||||
warn!("Granting {user_id} admin privileges as the first user");
|
||||
}
|
||||
}
|
||||
@@ -379,25 +429,32 @@ pub(crate) async fn register_route(
|
||||
&& (services.globals.allow_guests_auto_join_rooms() || !is_guest)
|
||||
{
|
||||
for room in &services.globals.config.auto_join_rooms {
|
||||
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
||||
error!("Failed to resolve room alias to room ID when attempting to auto join {room}, skipping");
|
||||
continue;
|
||||
};
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), room)?
|
||||
.server_in_room(services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
warn!("Skipping room {room} to automatically join as we have never joined before.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_id_server_name) = room.server_name() {
|
||||
if let Some(room_server_name) = room.server_name() {
|
||||
if let Err(e) = join_room_by_id_helper(
|
||||
&services,
|
||||
&user_id,
|
||||
room,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[room_id_server_name.to_owned(), services.globals.server_name().to_owned()],
|
||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||
None,
|
||||
&body.appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
// don't return this error so we don't fail registrations
|
||||
@@ -461,16 +518,20 @@ pub(crate) async fn change_password_route(
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
.create(sender_user, sender_device, &uiaainfo, &json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
@@ -482,14 +543,12 @@ pub(crate) async fn change_password_route(
|
||||
|
||||
if body.logout_devices {
|
||||
// Logout all devices except the current one
|
||||
for id in services
|
||||
services
|
||||
.users
|
||||
.all_device_ids(sender_user)
|
||||
.filter_map(Result::ok)
|
||||
.filter(|id| id != sender_device)
|
||||
{
|
||||
services.users.remove_device(sender_user, &id)?;
|
||||
}
|
||||
.ready_filter(|id| id != sender_device)
|
||||
.for_each(|id| services.users.remove_device(sender_user, id))
|
||||
.await;
|
||||
}
|
||||
|
||||
info!("User {sender_user} changed their password.");
|
||||
@@ -500,7 +559,8 @@ pub(crate) async fn change_password_route(
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"User {sender_user} changed their password."
|
||||
)))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(change_password::v3::Response {})
|
||||
@@ -520,7 +580,7 @@ pub(crate) async fn whoami_route(
|
||||
Ok(whoami::v3::Response {
|
||||
user_id: sender_user.clone(),
|
||||
device_id,
|
||||
is_guest: services.users.is_deactivated(sender_user)? && body.appservice_info.is_none(),
|
||||
is_guest: services.users.is_deactivated(sender_user).await? && body.appservice_info.is_none(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -561,7 +621,9 @@ pub(crate) async fn deactivate_route(
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
@@ -570,7 +632,8 @@ pub(crate) async fn deactivate_route(
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
.create(sender_user, sender_device, &uiaainfo, &json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
@@ -581,10 +644,14 @@ pub(crate) async fn deactivate_route(
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
full_user_deactivate(&services, sender_user, all_joined_rooms).await?;
|
||||
super::update_displayname(&services, sender_user, None, &all_joined_rooms).await?;
|
||||
super::update_avatar_url(&services, sender_user, None, None, &all_joined_rooms).await?;
|
||||
|
||||
full_user_deactivate(&services, sender_user, &all_joined_rooms).await?;
|
||||
|
||||
info!("User {sender_user} deactivated their account.");
|
||||
|
||||
@@ -594,7 +661,8 @@ pub(crate) async fn deactivate_route(
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"User {sender_user} deactivated their account."
|
||||
)))
|
||||
.await;
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(deactivate::v3::Response {
|
||||
@@ -654,7 +722,7 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
||||
pub(crate) async fn check_registration_token_validity(
|
||||
State(services): State<crate::State>, body: Ruma<check_registration_token_validity::v1::Request>,
|
||||
) -> Result<check_registration_token_validity::v1::Response> {
|
||||
let Some(reg_token) = services.globals.config.registration_token.clone() else {
|
||||
let Some(reg_token) = services.globals.registration_token.clone() else {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"Server does not allow token registration.",
|
||||
@@ -674,34 +742,27 @@ pub(crate) async fn check_registration_token_validity(
|
||||
/// - Removing all profile data
|
||||
/// - Leaving all rooms (and forgets all of them)
|
||||
pub async fn full_user_deactivate(
|
||||
services: &Services, user_id: &UserId, all_joined_rooms: Vec<OwnedRoomId>,
|
||||
services: &Services, user_id: &UserId, all_joined_rooms: &[OwnedRoomId],
|
||||
) -> Result<()> {
|
||||
services.users.deactivate_account(user_id)?;
|
||||
services.users.deactivate_account(user_id).await?;
|
||||
super::update_displayname(services, user_id, None, all_joined_rooms).await?;
|
||||
super::update_avatar_url(services, user_id, None, None, all_joined_rooms).await?;
|
||||
|
||||
super::update_displayname(services, user_id, None, all_joined_rooms.clone()).await?;
|
||||
super::update_avatar_url(services, user_id, None, None, all_joined_rooms.clone()).await?;
|
||||
|
||||
let all_profile_keys = services
|
||||
services
|
||||
.users
|
||||
.all_profile_keys(user_id)
|
||||
.filter_map(Result::ok);
|
||||
|
||||
for (profile_key, _profile_value) in all_profile_keys {
|
||||
if let Err(e) = services.users.set_profile_key(user_id, &profile_key, None) {
|
||||
warn!("Failed removing {user_id} profile key {profile_key}: {e}");
|
||||
}
|
||||
}
|
||||
.ready_for_each(|(profile_key, _)| services.users.set_profile_key(user_id, &profile_key, None))
|
||||
.await;
|
||||
|
||||
for room_id in all_joined_rooms {
|
||||
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).await;
|
||||
|
||||
let room_power_levels = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomPowerLevels, "")?
|
||||
.as_ref()
|
||||
.and_then(|event| serde_json::from_str(event.content.get()).ok()?)
|
||||
.and_then(|content: RoomPowerLevelsEventContent| content.into());
|
||||
.room_state_get_content::<RoomPowerLevelsEventContent>(room_id, &StateEventType::RoomPowerLevels, "")
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let user_can_demote_self = room_power_levels
|
||||
.as_ref()
|
||||
@@ -710,9 +771,9 @@ pub async fn full_user_deactivate(
|
||||
}) || services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomCreate, "")?
|
||||
.as_ref()
|
||||
.is_some_and(|event| event.sender == user_id);
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.is_ok_and(|event| event.sender == user_id);
|
||||
|
||||
if user_can_demote_self {
|
||||
let mut power_levels_content = room_power_levels.unwrap_or_default();
|
||||
@@ -723,16 +784,9 @@ pub async fn full_user_deactivate(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomPowerLevels,
|
||||
content: to_raw_value(&power_levels_content).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
PduBuilder::state(String::new(), &power_levels_content),
|
||||
user_id,
|
||||
&room_id,
|
||||
room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await
|
||||
|
||||
+17
-25
@@ -1,11 +1,9 @@
|
||||
use axum::extract::State;
|
||||
use conduit::{debug, Error, Result};
|
||||
use conduit::{debug, Err, Result};
|
||||
use futures::StreamExt;
|
||||
use rand::seq::SliceRandom;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
alias::{create_alias, delete_alias, get_alias},
|
||||
error::ErrorKind,
|
||||
},
|
||||
api::client::alias::{create_alias, delete_alias, get_alias},
|
||||
OwnedServerName, RoomAliasId, RoomId,
|
||||
};
|
||||
use service::Services;
|
||||
@@ -33,16 +31,17 @@ pub(crate) async fn create_alias_route(
|
||||
.forbidden_alias_names()
|
||||
.is_match(body.room_alias.alias())
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Room alias is forbidden."));
|
||||
return Err!(Request(Forbidden("Room alias is forbidden.")));
|
||||
}
|
||||
|
||||
if services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&body.room_alias)?
|
||||
.is_some()
|
||||
.resolve_local_alias(&body.room_alias)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(Error::Conflict("Alias already exists."));
|
||||
return Err!(Conflict("Alias already exists."));
|
||||
}
|
||||
|
||||
services
|
||||
@@ -87,39 +86,32 @@ pub(crate) async fn get_alias_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_alias::v3::Request>,
|
||||
) -> Result<get_alias::v3::Response> {
|
||||
let room_alias = body.body.room_alias;
|
||||
let servers = None;
|
||||
|
||||
let Ok((room_id, pre_servers)) = services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_alias(&room_alias, servers.as_ref())
|
||||
.await
|
||||
else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Room with alias not found."));
|
||||
let Ok((room_id, servers)) = services.rooms.alias.resolve_alias(&room_alias, None).await else {
|
||||
return Err!(Request(NotFound("Room with alias not found.")));
|
||||
};
|
||||
|
||||
let servers = room_available_servers(&services, &room_id, &room_alias, &pre_servers);
|
||||
let servers = room_available_servers(&services, &room_id, &room_alias, servers).await;
|
||||
debug!(?room_alias, ?room_id, "available servers: {servers:?}");
|
||||
|
||||
Ok(get_alias::v3::Response::new(room_id, servers))
|
||||
}
|
||||
|
||||
fn room_available_servers(
|
||||
services: &Services, room_id: &RoomId, room_alias: &RoomAliasId, pre_servers: &Option<Vec<OwnedServerName>>,
|
||||
async fn room_available_servers(
|
||||
services: &Services, room_id: &RoomId, room_alias: &RoomAliasId, pre_servers: Vec<OwnedServerName>,
|
||||
) -> Vec<OwnedServerName> {
|
||||
// find active servers in room state cache to suggest
|
||||
let mut servers: Vec<OwnedServerName> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_servers(room_id)
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
// push any servers we want in the list already (e.g. responded remote alias
|
||||
// servers, room alias server itself)
|
||||
if let Some(pre_servers) = pre_servers {
|
||||
servers.extend(pre_servers.clone());
|
||||
};
|
||||
servers.extend(pre_servers);
|
||||
|
||||
servers.sort_unstable();
|
||||
servers.dedup();
|
||||
|
||||
+143
-130
@@ -1,18 +1,16 @@
|
||||
use axum::extract::State;
|
||||
use conduit::{err, Err};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
backup::{
|
||||
add_backup_keys, add_backup_keys_for_room, add_backup_keys_for_session, create_backup_version,
|
||||
delete_backup_keys, delete_backup_keys_for_room, delete_backup_keys_for_session, delete_backup_version,
|
||||
get_backup_info, get_backup_keys, get_backup_keys_for_room, get_backup_keys_for_session,
|
||||
get_latest_backup_info, update_backup_version,
|
||||
},
|
||||
error::ErrorKind,
|
||||
api::client::backup::{
|
||||
add_backup_keys, add_backup_keys_for_room, add_backup_keys_for_session, create_backup_version,
|
||||
delete_backup_keys, delete_backup_keys_for_room, delete_backup_keys_for_session, delete_backup_version,
|
||||
get_backup_info, get_backup_keys, get_backup_keys_for_room, get_backup_keys_for_session,
|
||||
get_latest_backup_info, update_backup_version,
|
||||
},
|
||||
UInt,
|
||||
};
|
||||
|
||||
use crate::{Error, Result, Ruma};
|
||||
use crate::{Result, Ruma};
|
||||
|
||||
/// # `POST /_matrix/client/r0/room_keys/version`
|
||||
///
|
||||
@@ -20,10 +18,9 @@ use crate::{Error, Result, Ruma};
|
||||
pub(crate) async fn create_backup_version_route(
|
||||
State(services): State<crate::State>, body: Ruma<create_backup_version::v3::Request>,
|
||||
) -> Result<create_backup_version::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let version = services
|
||||
.key_backups
|
||||
.create_backup(sender_user, &body.algorithm)?;
|
||||
.create_backup(body.sender_user(), &body.algorithm)?;
|
||||
|
||||
Ok(create_backup_version::v3::Response {
|
||||
version,
|
||||
@@ -37,10 +34,10 @@ pub(crate) async fn create_backup_version_route(
|
||||
pub(crate) async fn update_backup_version_route(
|
||||
State(services): State<crate::State>, body: Ruma<update_backup_version::v3::Request>,
|
||||
) -> Result<update_backup_version::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
services
|
||||
.key_backups
|
||||
.update_backup(sender_user, &body.version, &body.algorithm)?;
|
||||
.update_backup(body.sender_user(), &body.version, &body.algorithm)
|
||||
.await?;
|
||||
|
||||
Ok(update_backup_version::v3::Response {})
|
||||
}
|
||||
@@ -51,18 +48,25 @@ pub(crate) async fn update_backup_version_route(
|
||||
pub(crate) async fn get_latest_backup_info_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_latest_backup_info::v3::Request>,
|
||||
) -> Result<get_latest_backup_info::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let (version, algorithm) = services
|
||||
.key_backups
|
||||
.get_latest_backup(sender_user)?
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Key backup does not exist."))?;
|
||||
.get_latest_backup(body.sender_user())
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("Key backup does not exist."))))?;
|
||||
|
||||
Ok(get_latest_backup_info::v3::Response {
|
||||
algorithm,
|
||||
count: (UInt::try_from(services.key_backups.count_keys(sender_user, &version)?)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services.key_backups.get_etag(sender_user, &version)?,
|
||||
count: (UInt::try_from(
|
||||
services
|
||||
.key_backups
|
||||
.count_keys(body.sender_user(), &version)
|
||||
.await,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services
|
||||
.key_backups
|
||||
.get_etag(body.sender_user(), &version)
|
||||
.await,
|
||||
version,
|
||||
})
|
||||
}
|
||||
@@ -73,21 +77,23 @@ pub(crate) async fn get_latest_backup_info_route(
|
||||
pub(crate) async fn get_backup_info_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_backup_info::v3::Request>,
|
||||
) -> Result<get_backup_info::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let algorithm = services
|
||||
.key_backups
|
||||
.get_backup(sender_user, &body.version)?
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Key backup does not exist."))?;
|
||||
.get_backup(body.sender_user(), &body.version)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("Key backup does not exist at version {:?}", body.version))))?;
|
||||
|
||||
Ok(get_backup_info::v3::Response {
|
||||
algorithm,
|
||||
count: (UInt::try_from(
|
||||
services
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services.key_backups.get_etag(sender_user, &body.version)?,
|
||||
count: services
|
||||
.key_backups
|
||||
.count_keys(body.sender_user(), &body.version)
|
||||
.await
|
||||
.try_into()?,
|
||||
etag: services
|
||||
.key_backups
|
||||
.get_etag(body.sender_user(), &body.version)
|
||||
.await,
|
||||
version: body.version.clone(),
|
||||
})
|
||||
}
|
||||
@@ -101,11 +107,10 @@ pub(crate) async fn get_backup_info_route(
|
||||
pub(crate) async fn delete_backup_version_route(
|
||||
State(services): State<crate::State>, body: Ruma<delete_backup_version::v3::Request>,
|
||||
) -> Result<delete_backup_version::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services
|
||||
.key_backups
|
||||
.delete_backup(sender_user, &body.version)?;
|
||||
.delete_backup(body.sender_user(), &body.version)
|
||||
.await;
|
||||
|
||||
Ok(delete_backup_version::v3::Response {})
|
||||
}
|
||||
@@ -121,36 +126,36 @@ pub(crate) async fn delete_backup_version_route(
|
||||
pub(crate) async fn add_backup_keys_route(
|
||||
State(services): State<crate::State>, body: Ruma<add_backup_keys::v3::Request>,
|
||||
) -> Result<add_backup_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= services
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
if services
|
||||
.key_backups
|
||||
.get_latest_backup_version(body.sender_user())
|
||||
.await
|
||||
.is_ok_and(|version| version != body.version)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
return Err!(Request(InvalidParam(
|
||||
"You may only manipulate the most recently created version of the backup."
|
||||
)));
|
||||
}
|
||||
|
||||
for (room_id, room) in &body.rooms {
|
||||
for (session_id, key_data) in &room.sessions {
|
||||
services
|
||||
.key_backups
|
||||
.add_key(sender_user, &body.version, room_id, session_id, key_data)?;
|
||||
.add_key(body.sender_user(), &body.version, room_id, session_id, key_data)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(add_backup_keys::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services.key_backups.get_etag(sender_user, &body.version)?,
|
||||
count: services
|
||||
.key_backups
|
||||
.count_keys(body.sender_user(), &body.version)
|
||||
.await
|
||||
.try_into()?,
|
||||
etag: services
|
||||
.key_backups
|
||||
.get_etag(body.sender_user(), &body.version)
|
||||
.await,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -165,34 +170,34 @@ pub(crate) async fn add_backup_keys_route(
|
||||
pub(crate) async fn add_backup_keys_for_room_route(
|
||||
State(services): State<crate::State>, body: Ruma<add_backup_keys_for_room::v3::Request>,
|
||||
) -> Result<add_backup_keys_for_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= services
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
if services
|
||||
.key_backups
|
||||
.get_latest_backup_version(body.sender_user())
|
||||
.await
|
||||
.is_ok_and(|version| version != body.version)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
return Err!(Request(InvalidParam(
|
||||
"You may only manipulate the most recently created version of the backup."
|
||||
)));
|
||||
}
|
||||
|
||||
for (session_id, key_data) in &body.sessions {
|
||||
services
|
||||
.key_backups
|
||||
.add_key(sender_user, &body.version, &body.room_id, session_id, key_data)?;
|
||||
.add_key(body.sender_user(), &body.version, &body.room_id, session_id, key_data)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(add_backup_keys_for_room::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services.key_backups.get_etag(sender_user, &body.version)?,
|
||||
count: services
|
||||
.key_backups
|
||||
.count_keys(body.sender_user(), &body.version)
|
||||
.await
|
||||
.try_into()?,
|
||||
etag: services
|
||||
.key_backups
|
||||
.get_etag(body.sender_user(), &body.version)
|
||||
.await,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -207,32 +212,38 @@ pub(crate) async fn add_backup_keys_for_room_route(
|
||||
pub(crate) async fn add_backup_keys_for_session_route(
|
||||
State(services): State<crate::State>, body: Ruma<add_backup_keys_for_session::v3::Request>,
|
||||
) -> Result<add_backup_keys_for_session::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= services
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
if services
|
||||
.key_backups
|
||||
.get_latest_backup_version(body.sender_user())
|
||||
.await
|
||||
.is_ok_and(|version| version != body.version)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
return Err!(Request(InvalidParam(
|
||||
"You may only manipulate the most recently created version of the backup."
|
||||
)));
|
||||
}
|
||||
|
||||
services
|
||||
.key_backups
|
||||
.add_key(sender_user, &body.version, &body.room_id, &body.session_id, &body.session_data)?;
|
||||
.add_key(
|
||||
body.sender_user(),
|
||||
&body.version,
|
||||
&body.room_id,
|
||||
&body.session_id,
|
||||
&body.session_data,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(add_backup_keys_for_session::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services.key_backups.get_etag(sender_user, &body.version)?,
|
||||
count: services
|
||||
.key_backups
|
||||
.count_keys(body.sender_user(), &body.version)
|
||||
.await
|
||||
.try_into()?,
|
||||
etag: services
|
||||
.key_backups
|
||||
.get_etag(body.sender_user(), &body.version)
|
||||
.await,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -242,9 +253,10 @@ pub(crate) async fn add_backup_keys_for_session_route(
|
||||
pub(crate) async fn get_backup_keys_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_backup_keys::v3::Request>,
|
||||
) -> Result<get_backup_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let rooms = services.key_backups.get_all(sender_user, &body.version)?;
|
||||
let rooms = services
|
||||
.key_backups
|
||||
.get_all(body.sender_user(), &body.version)
|
||||
.await;
|
||||
|
||||
Ok(get_backup_keys::v3::Response {
|
||||
rooms,
|
||||
@@ -257,11 +269,10 @@ pub(crate) async fn get_backup_keys_route(
|
||||
pub(crate) async fn get_backup_keys_for_room_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_backup_keys_for_room::v3::Request>,
|
||||
) -> Result<get_backup_keys_for_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let sessions = services
|
||||
.key_backups
|
||||
.get_room(sender_user, &body.version, &body.room_id)?;
|
||||
.get_room(body.sender_user(), &body.version, &body.room_id)
|
||||
.await;
|
||||
|
||||
Ok(get_backup_keys_for_room::v3::Response {
|
||||
sessions,
|
||||
@@ -274,12 +285,11 @@ pub(crate) async fn get_backup_keys_for_room_route(
|
||||
pub(crate) async fn get_backup_keys_for_session_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_backup_keys_for_session::v3::Request>,
|
||||
) -> Result<get_backup_keys_for_session::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let key_data = services
|
||||
.key_backups
|
||||
.get_session(sender_user, &body.version, &body.room_id, &body.session_id)?
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Backup key not found for this user's session."))?;
|
||||
.get_session(body.sender_user(), &body.version, &body.room_id, &body.session_id)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound(debug_error!("Backup key not found for this user's session.")))))?;
|
||||
|
||||
Ok(get_backup_keys_for_session::v3::Response {
|
||||
key_data,
|
||||
@@ -292,20 +302,21 @@ pub(crate) async fn get_backup_keys_for_session_route(
|
||||
pub(crate) async fn delete_backup_keys_route(
|
||||
State(services): State<crate::State>, body: Ruma<delete_backup_keys::v3::Request>,
|
||||
) -> Result<delete_backup_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services
|
||||
.key_backups
|
||||
.delete_all_keys(sender_user, &body.version)?;
|
||||
.delete_all_keys(body.sender_user(), &body.version)
|
||||
.await;
|
||||
|
||||
Ok(delete_backup_keys::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services.key_backups.get_etag(sender_user, &body.version)?,
|
||||
count: services
|
||||
.key_backups
|
||||
.count_keys(body.sender_user(), &body.version)
|
||||
.await
|
||||
.try_into()?,
|
||||
etag: services
|
||||
.key_backups
|
||||
.get_etag(body.sender_user(), &body.version)
|
||||
.await,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -315,20 +326,21 @@ pub(crate) async fn delete_backup_keys_route(
|
||||
pub(crate) async fn delete_backup_keys_for_room_route(
|
||||
State(services): State<crate::State>, body: Ruma<delete_backup_keys_for_room::v3::Request>,
|
||||
) -> Result<delete_backup_keys_for_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services
|
||||
.key_backups
|
||||
.delete_room_keys(sender_user, &body.version, &body.room_id)?;
|
||||
.delete_room_keys(body.sender_user(), &body.version, &body.room_id)
|
||||
.await;
|
||||
|
||||
Ok(delete_backup_keys_for_room::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services.key_backups.get_etag(sender_user, &body.version)?,
|
||||
count: services
|
||||
.key_backups
|
||||
.count_keys(body.sender_user(), &body.version)
|
||||
.await
|
||||
.try_into()?,
|
||||
etag: services
|
||||
.key_backups
|
||||
.get_etag(body.sender_user(), &body.version)
|
||||
.await,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -338,19 +350,20 @@ pub(crate) async fn delete_backup_keys_for_room_route(
|
||||
pub(crate) async fn delete_backup_keys_for_session_route(
|
||||
State(services): State<crate::State>, body: Ruma<delete_backup_keys_for_session::v3::Request>,
|
||||
) -> Result<delete_backup_keys_for_session::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services
|
||||
.key_backups
|
||||
.delete_room_key(sender_user, &body.version, &body.room_id, &body.session_id)?;
|
||||
.delete_room_key(body.sender_user(), &body.version, &body.room_id, &body.session_id)
|
||||
.await;
|
||||
|
||||
Ok(delete_backup_keys_for_session::v3::Response {
|
||||
count: (UInt::try_from(
|
||||
services
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)?,
|
||||
)
|
||||
.expect("user backup keys count should not be that high")),
|
||||
etag: services.key_backups.get_etag(sender_user, &body.version)?,
|
||||
count: services
|
||||
.key_backups
|
||||
.count_keys(body.sender_user(), &body.version)
|
||||
.await
|
||||
.try_into()?,
|
||||
etag: services
|
||||
.key_backups
|
||||
.get_etag(body.sender_user(), &body.version)
|
||||
.await,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ use std::collections::BTreeMap;
|
||||
use axum::extract::State;
|
||||
use ruma::{
|
||||
api::client::discovery::get_capabilities::{
|
||||
self, Capabilities, RoomVersionStability, RoomVersionsCapability, ThirdPartyIdChangesCapability,
|
||||
self, Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability,
|
||||
ThirdPartyIdChangesCapability,
|
||||
},
|
||||
RoomVersionId,
|
||||
};
|
||||
@@ -43,6 +44,11 @@ pub(crate) async fn get_capabilities_route(
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
// we dont support generating tokens yet
|
||||
capabilities.get_login_token = GetLoginTokenCapability {
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
// MSC4133 capability
|
||||
capabilities
|
||||
.set("uk.tcpip.msc4133.profile_fields", json!({"enabled": true}))
|
||||
|
||||
+31
-31
@@ -1,4 +1,5 @@
|
||||
use axum::extract::State;
|
||||
use conduit::err;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
config::{get_global_account_data, get_room_account_data, set_global_account_data, set_room_account_data},
|
||||
@@ -22,10 +23,11 @@ pub(crate) async fn set_global_account_data_route(
|
||||
set_account_data(
|
||||
&services,
|
||||
None,
|
||||
&body.sender_user,
|
||||
body.sender_user.as_ref(),
|
||||
&body.event_type.to_string(),
|
||||
body.data.json(),
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(set_global_account_data::v3::Response {})
|
||||
}
|
||||
@@ -39,10 +41,11 @@ pub(crate) async fn set_room_account_data_route(
|
||||
set_account_data(
|
||||
&services,
|
||||
Some(&body.room_id),
|
||||
&body.sender_user,
|
||||
body.sender_user.as_ref(),
|
||||
&body.event_type.to_string(),
|
||||
body.data.json(),
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(set_room_account_data::v3::Response {})
|
||||
}
|
||||
@@ -55,17 +58,14 @@ pub(crate) async fn get_global_account_data_route(
|
||||
) -> Result<get_global_account_data::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event: Box<RawJsonValue> = services
|
||||
let account_data: ExtractGlobalEventContent = services
|
||||
.account_data
|
||||
.get(None, sender_user, body.event_type.to_string().into())?
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Data not found."))?;
|
||||
|
||||
let account_data = serde_json::from_str::<ExtractGlobalEventContent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
.get_global(sender_user, body.event_type.clone())
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("Data not found."))))?;
|
||||
|
||||
Ok(get_global_account_data::v3::Response {
|
||||
account_data,
|
||||
account_data: account_data.content,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,22 +77,19 @@ pub(crate) async fn get_room_account_data_route(
|
||||
) -> Result<get_room_account_data::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event: Box<RawJsonValue> = services
|
||||
let account_data: ExtractRoomEventContent = services
|
||||
.account_data
|
||||
.get(Some(&body.room_id), sender_user, body.event_type.clone())?
|
||||
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Data not found."))?;
|
||||
|
||||
let account_data = serde_json::from_str::<ExtractRoomEventContent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
.get_room(&body.room_id, sender_user, body.event_type.clone())
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("Data not found."))))?;
|
||||
|
||||
Ok(get_room_account_data::v3::Response {
|
||||
account_data,
|
||||
account_data: account_data.content,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_account_data(
|
||||
services: &Services, room_id: Option<&RoomId>, sender_user: &Option<OwnedUserId>, event_type: &str,
|
||||
async fn set_account_data(
|
||||
services: &Services, room_id: Option<&RoomId>, sender_user: Option<&OwnedUserId>, event_type: &str,
|
||||
data: &RawJsonValue,
|
||||
) -> Result<()> {
|
||||
let sender_user = sender_user.as_ref().expect("user is authenticated");
|
||||
@@ -100,15 +97,18 @@ fn set_account_data(
|
||||
let data: serde_json::Value =
|
||||
serde_json::from_str(data.get()).map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Data is invalid."))?;
|
||||
|
||||
services.account_data.update(
|
||||
room_id,
|
||||
sender_user,
|
||||
event_type.into(),
|
||||
&json!({
|
||||
"type": event_type,
|
||||
"content": data,
|
||||
}),
|
||||
)?;
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
room_id,
|
||||
sender_user,
|
||||
event_type.into(),
|
||||
&json!({
|
||||
"type": event_type,
|
||||
"content": data,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+152
-150
@@ -1,15 +1,31 @@
|
||||
use std::collections::HashSet;
|
||||
use std::iter::once;
|
||||
|
||||
use axum::extract::State;
|
||||
use ruma::{
|
||||
api::client::{context::get_context, error::ErrorKind, filter::LazyLoadOptions},
|
||||
events::StateEventType,
|
||||
use conduit::{
|
||||
at, err, ref_at,
|
||||
utils::{
|
||||
future::TryExtExt,
|
||||
stream::{BroadbandExt, ReadyExt, WidebandExt},
|
||||
IterStream,
|
||||
},
|
||||
Err, Result,
|
||||
};
|
||||
use futures::{join, try_join, FutureExt, StreamExt, TryFutureExt};
|
||||
use ruma::{
|
||||
api::client::{context::get_context, filter::LazyLoadOptions},
|
||||
events::StateEventType,
|
||||
OwnedEventId, UserId,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{Error, Result, Ruma};
|
||||
use crate::{
|
||||
client::message::{event_filter, ignored_filter, update_lazy, visibility_filter, LazySet},
|
||||
Ruma,
|
||||
};
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/context`
|
||||
const LIMIT_MAX: usize = 100;
|
||||
const LIMIT_DEFAULT: usize = 10;
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/context/{eventId}`
|
||||
///
|
||||
/// Allows loading room history around an event.
|
||||
///
|
||||
@@ -18,186 +34,172 @@ use crate::{Error, Result, Ruma};
|
||||
pub(crate) async fn get_context_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_context::v3::Request>,
|
||||
) -> Result<get_context::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
let filter = &body.filter;
|
||||
let sender = body.sender();
|
||||
let (sender_user, _) = sender;
|
||||
let room_id = &body.room_id;
|
||||
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit: usize = body
|
||||
.limit
|
||||
.try_into()
|
||||
.unwrap_or(LIMIT_DEFAULT)
|
||||
.min(LIMIT_MAX);
|
||||
|
||||
// some clients, at least element, seem to require knowledge of redundant
|
||||
// members for "inline" profiles on the timeline to work properly
|
||||
let (lazy_load_enabled, lazy_load_send_redundant) = match &body.filter.lazy_load_options {
|
||||
LazyLoadOptions::Enabled {
|
||||
include_redundant_members,
|
||||
} => (true, *include_redundant_members),
|
||||
LazyLoadOptions::Disabled => (false, cfg!(feature = "element_hacks")),
|
||||
};
|
||||
let lazy_load_enabled = matches!(filter.lazy_load_options, LazyLoadOptions::Enabled { .. });
|
||||
|
||||
let mut lazy_loaded = HashSet::new();
|
||||
let lazy_load_redundant = if let LazyLoadOptions::Enabled {
|
||||
include_redundant_members,
|
||||
} = filter.lazy_load_options
|
||||
{
|
||||
include_redundant_members
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let base_token = services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_count(&body.event_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Base event id not found."))?;
|
||||
.get_pdu_count(&body.event_id)
|
||||
.map_err(|_| err!(Request(NotFound("Event not found."))));
|
||||
|
||||
let base_event = services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu(&body.event_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Base event not found."))?;
|
||||
.get_pdu(&body.event_id)
|
||||
.map_err(|_| err!(Request(NotFound("Base event not found."))));
|
||||
|
||||
let room_id = base_event.room_id.clone();
|
||||
|
||||
if !services
|
||||
let visible = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &room_id, &body.event_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this event.",
|
||||
));
|
||||
.user_can_see_event(sender_user, &body.room_id, &body.event_id)
|
||||
.map(Ok);
|
||||
|
||||
let (base_token, base_event, visible) = try_join!(base_token, base_event, visible)?;
|
||||
|
||||
if base_event.room_id != body.room_id || base_event.event_id != body.event_id {
|
||||
return Err!(Request(NotFound("Base event not found.")));
|
||||
}
|
||||
|
||||
if !services.rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&room_id,
|
||||
&base_event.sender,
|
||||
)? || lazy_load_send_redundant
|
||||
if !visible
|
||||
|| ignored_filter(&services, (base_token, base_event.clone()), sender_user)
|
||||
.await
|
||||
.is_none()
|
||||
{
|
||||
lazy_loaded.insert(base_event.sender.as_str().to_owned());
|
||||
return Err!(Request(Forbidden("You don't have permission to view this event.")));
|
||||
}
|
||||
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit = usize::try_from(body.limit).unwrap_or(10).min(100);
|
||||
|
||||
let base_event = base_event.to_room_event();
|
||||
|
||||
let events_before: Vec<_> = services
|
||||
let events_before = services
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_until(sender_user, &room_id, base_token)?
|
||||
.pdus_rev(Some(sender_user), room_id, Some(base_token));
|
||||
|
||||
let events_after = services
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus(Some(sender_user), room_id, Some(base_token));
|
||||
|
||||
let (events_before, events_after) = try_join!(events_before, events_after)?;
|
||||
|
||||
let events_before = events_before
|
||||
.ready_filter_map(|item| event_filter(item, filter))
|
||||
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
|
||||
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
|
||||
.take(limit / 2)
|
||||
.filter_map(Result::ok) // Remove buggy events
|
||||
.filter(|(_, pdu)| {
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_before {
|
||||
if !services.rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&room_id,
|
||||
&event.sender,
|
||||
)? || lazy_load_send_redundant
|
||||
{
|
||||
lazy_loaded.insert(event.sender.as_str().to_owned());
|
||||
}
|
||||
}
|
||||
let events_after = events_after
|
||||
.ready_filter_map(|item| event_filter(item, filter))
|
||||
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
|
||||
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
|
||||
.take(limit / 2)
|
||||
.collect();
|
||||
|
||||
let start_token = events_before
|
||||
let (events_before, events_after): (Vec<_>, Vec<_>) = join!(events_before, events_after);
|
||||
|
||||
let state_at = events_after
|
||||
.last()
|
||||
.map_or_else(|| base_token.stringify(), |(count, _)| count.stringify());
|
||||
|
||||
let events_before: Vec<_> = events_before
|
||||
.into_iter()
|
||||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect();
|
||||
|
||||
let events_after: Vec<_> = services
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_after(sender_user, &room_id, base_token)?
|
||||
.take(limit / 2)
|
||||
.filter_map(Result::ok) // Remove buggy events
|
||||
.filter(|(_, pdu)| {
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_after {
|
||||
if !services.rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&room_id,
|
||||
&event.sender,
|
||||
)? || lazy_load_send_redundant
|
||||
{
|
||||
lazy_loaded.insert(event.sender.as_str().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
let shortstatehash = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.pdu_shortstatehash(
|
||||
events_after
|
||||
.last()
|
||||
.map_or(&*body.event_id, |(_, e)| &*e.event_id),
|
||||
)?
|
||||
.map_or(
|
||||
services
|
||||
.rooms
|
||||
.state
|
||||
.get_room_shortstatehash(&room_id)?
|
||||
.expect("All rooms have state"),
|
||||
|hash| hash,
|
||||
);
|
||||
.map(ref_at!(1))
|
||||
.map_or(body.event_id.as_ref(), |e| e.event_id.as_ref());
|
||||
|
||||
let state_ids = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_full_ids(shortstatehash)
|
||||
.pdu_shortstatehash(state_at)
|
||||
.or_else(|_| services.rooms.state.get_room_shortstatehash(room_id))
|
||||
.and_then(|shortstatehash| services.rooms.state_accessor.state_full_ids(shortstatehash))
|
||||
.map_err(|e| err!(Database("State not found: {e}")))
|
||||
.await?;
|
||||
|
||||
let end_token = events_after
|
||||
.last()
|
||||
.map_or_else(|| base_token.stringify(), |(count, _)| count.stringify());
|
||||
let lazy = once(&(base_token, base_event.clone()))
|
||||
.chain(events_before.iter())
|
||||
.chain(events_after.iter())
|
||||
.stream()
|
||||
.fold(LazySet::new(), |lazy, item| {
|
||||
update_lazy(&services, room_id, sender, lazy, item, lazy_load_redundant)
|
||||
})
|
||||
.await;
|
||||
|
||||
let events_after: Vec<_> = events_after
|
||||
.into_iter()
|
||||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect();
|
||||
let lazy = &lazy;
|
||||
let state: Vec<_> = state_ids
|
||||
.iter()
|
||||
.stream()
|
||||
.broad_filter_map(|(shortstatekey, event_id)| {
|
||||
services
|
||||
.rooms
|
||||
.short
|
||||
.get_statekey_from_short(*shortstatekey)
|
||||
.map_ok(move |(event_type, state_key)| (event_type, state_key, event_id))
|
||||
.ok()
|
||||
})
|
||||
.ready_filter_map(|(event_type, state_key, event_id)| {
|
||||
if !lazy_load_enabled || event_type != StateEventType::RoomMember {
|
||||
return Some(event_id);
|
||||
}
|
||||
|
||||
let mut state = Vec::with_capacity(state_ids.len());
|
||||
|
||||
for (shortstatekey, id) in state_ids {
|
||||
let (event_type, state_key) = services
|
||||
.rooms
|
||||
.short
|
||||
.get_statekey_from_short(shortstatekey)?;
|
||||
|
||||
if event_type != StateEventType::RoomMember {
|
||||
let Some(pdu) = services.rooms.timeline.get_pdu(&id)? else {
|
||||
error!("Pdu in state not found: {}", id);
|
||||
continue;
|
||||
};
|
||||
|
||||
state.push(pdu.to_state_event());
|
||||
} else if !lazy_load_enabled || lazy_loaded.contains(&state_key) {
|
||||
let Some(pdu) = services.rooms.timeline.get_pdu(&id)? else {
|
||||
error!("Pdu in state not found: {}", id);
|
||||
continue;
|
||||
};
|
||||
|
||||
state.push(pdu.to_state_event());
|
||||
}
|
||||
}
|
||||
state_key
|
||||
.as_str()
|
||||
.try_into()
|
||||
.ok()
|
||||
.filter(|&user_id: &&UserId| lazy.contains(user_id))
|
||||
.map(|_| event_id)
|
||||
})
|
||||
.broad_filter_map(|event_id: &OwnedEventId| services.rooms.timeline.get_pdu(event_id).ok())
|
||||
.map(|pdu| pdu.to_state_event())
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(get_context::v3::Response {
|
||||
start: Some(start_token),
|
||||
end: Some(end_token),
|
||||
events_before,
|
||||
event: Some(base_event),
|
||||
events_after,
|
||||
event: Some(base_event.to_room_event()),
|
||||
|
||||
start: events_before
|
||||
.last()
|
||||
.map(at!(0))
|
||||
.or(Some(base_token))
|
||||
.as_ref()
|
||||
.map(ToString::to_string),
|
||||
|
||||
end: events_after
|
||||
.last()
|
||||
.map(at!(0))
|
||||
.or(Some(base_token))
|
||||
.as_ref()
|
||||
.map(ToString::to_string),
|
||||
|
||||
events_before: events_before
|
||||
.into_iter()
|
||||
.map(at!(1))
|
||||
.map(|pdu| pdu.to_room_event())
|
||||
.collect(),
|
||||
|
||||
events_after: events_after
|
||||
.into_iter()
|
||||
.map(at!(1))
|
||||
.map(|pdu| pdu.to_room_event())
|
||||
.collect(),
|
||||
|
||||
state,
|
||||
})
|
||||
}
|
||||
|
||||
+45
-21
@@ -1,8 +1,14 @@
|
||||
use axum::extract::State;
|
||||
use ruma::api::client::{
|
||||
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
|
||||
error::ErrorKind,
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduit::{err, Err};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
|
||||
error::ErrorKind,
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
|
||||
use super::SESSION_ID_LENGTH;
|
||||
@@ -19,8 +25,8 @@ pub(crate) async fn get_devices_route(
|
||||
let devices: Vec<device::Device> = services
|
||||
.users
|
||||
.all_devices_metadata(sender_user)
|
||||
.filter_map(Result::ok) // Filter out buggy devices
|
||||
.collect();
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(get_devices::v3::Response {
|
||||
devices,
|
||||
@@ -37,8 +43,9 @@ pub(crate) async fn get_device_route(
|
||||
|
||||
let device = services
|
||||
.users
|
||||
.get_device_metadata(sender_user, &body.body.device_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Device not found."))?;
|
||||
.get_device_metadata(sender_user, &body.body.device_id)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("Device not found."))))?;
|
||||
|
||||
Ok(get_device::v3::Response {
|
||||
device,
|
||||
@@ -48,21 +55,29 @@ pub(crate) async fn get_device_route(
|
||||
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
|
||||
///
|
||||
/// Updates the metadata on a given device of the sender user.
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "update_device")]
|
||||
pub(crate) async fn update_device_route(
|
||||
State(services): State<crate::State>, body: Ruma<update_device::v3::Request>,
|
||||
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<update_device::v3::Request>,
|
||||
) -> Result<update_device::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut device = services
|
||||
.users
|
||||
.get_device_metadata(sender_user, &body.device_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Device not found."))?;
|
||||
.get_device_metadata(sender_user, &body.device_id)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("Device not found."))))?;
|
||||
|
||||
device.display_name.clone_from(&body.display_name);
|
||||
device.last_seen_ip.clone_from(&Some(client.to_string()));
|
||||
device
|
||||
.last_seen_ts
|
||||
.clone_from(&Some(MilliSecondsSinceUnixEpoch::now()));
|
||||
|
||||
services
|
||||
.users
|
||||
.update_device_metadata(sender_user, &body.device_id, &device)?;
|
||||
.update_device_metadata(sender_user, &body.device_id, &device)
|
||||
.await?;
|
||||
|
||||
Ok(update_device::v3::Response {})
|
||||
}
|
||||
@@ -97,22 +112,28 @@ pub(crate) async fn delete_device_route(
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
return Err!(Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
.create(sender_user, sender_device, &uiaainfo, &json);
|
||||
|
||||
return Err!(Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
return Err!(Request(NotJson("Not json.")));
|
||||
}
|
||||
|
||||
services.users.remove_device(sender_user, &body.device_id)?;
|
||||
services
|
||||
.users
|
||||
.remove_device(sender_user, &body.device_id)
|
||||
.await;
|
||||
|
||||
Ok(delete_device::v3::Response {})
|
||||
}
|
||||
@@ -149,7 +170,9 @@ pub(crate) async fn delete_devices_route(
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
@@ -158,14 +181,15 @@ pub(crate) async fn delete_devices_route(
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
.create(sender_user, sender_device, &uiaainfo, &json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
}
|
||||
|
||||
for device_id in &body.devices {
|
||||
services.users.remove_device(sender_user, device_id)?;
|
||||
services.users.remove_device(sender_user, device_id).await;
|
||||
}
|
||||
|
||||
Ok(delete_devices::v3::Response {})
|
||||
|
||||
+106
-103
@@ -1,6 +1,7 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduit::{err, info, warn, Err, Error, Result};
|
||||
use conduit::{info, warn, Err, Error, Result};
|
||||
use futures::{StreamExt, TryFutureExt};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
@@ -18,7 +19,7 @@ use ruma::{
|
||||
},
|
||||
StateEventType,
|
||||
},
|
||||
uint, RoomId, ServerName, UInt, UserId,
|
||||
uint, OwnedRoomId, RoomId, ServerName, UInt, UserId,
|
||||
};
|
||||
use service::Services;
|
||||
|
||||
@@ -36,14 +37,12 @@ pub(crate) async fn get_public_rooms_filtered_route(
|
||||
) -> Result<get_public_rooms_filtered::v3::Response> {
|
||||
if let Some(server) = &body.server {
|
||||
if services
|
||||
.globals
|
||||
.forbidden_remote_room_directory_server_names()
|
||||
.server
|
||||
.config
|
||||
.forbidden_remote_room_directory_server_names
|
||||
.contains(server)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"Server is banned on this homeserver.",
|
||||
));
|
||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,14 +75,12 @@ pub(crate) async fn get_public_rooms_route(
|
||||
) -> Result<get_public_rooms::v3::Response> {
|
||||
if let Some(server) = &body.server {
|
||||
if services
|
||||
.globals
|
||||
.forbidden_remote_room_directory_server_names()
|
||||
.server
|
||||
.config
|
||||
.forbidden_remote_room_directory_server_names
|
||||
.contains(server)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"Server is banned on this homeserver.",
|
||||
));
|
||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,16 +116,22 @@ pub(crate) async fn set_room_visibility_route(
|
||||
) -> Result<set_room_visibility::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services.rooms.metadata.exists(&body.room_id)? {
|
||||
if !services.rooms.metadata.exists(&body.room_id).await {
|
||||
// Return 404 if the room doesn't exist
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Room not found"));
|
||||
}
|
||||
|
||||
if services.users.is_deactivated(sender_user).unwrap_or(false) && body.appservice_info.is_none() {
|
||||
if services
|
||||
.users
|
||||
.is_deactivated(sender_user)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
&& body.appservice_info.is_none()
|
||||
{
|
||||
return Err!(Request(Forbidden("Guests cannot publish to room directories")));
|
||||
}
|
||||
|
||||
if !user_can_publish_room(&services, sender_user, &body.room_id)? {
|
||||
if !user_can_publish_room(&services, sender_user, &body.room_id).await? {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"User is not allowed to publish this room",
|
||||
@@ -138,7 +141,7 @@ pub(crate) async fn set_room_visibility_route(
|
||||
match &body.visibility {
|
||||
room::Visibility::Public => {
|
||||
if services.globals.config.lockdown_public_room_directory
|
||||
&& !services.users.is_admin(sender_user)?
|
||||
&& !services.users.is_admin(sender_user).await
|
||||
&& body.appservice_info.is_none()
|
||||
{
|
||||
info!(
|
||||
@@ -164,7 +167,7 @@ pub(crate) async fn set_room_visibility_route(
|
||||
));
|
||||
}
|
||||
|
||||
services.rooms.directory.set_public(&body.room_id)?;
|
||||
services.rooms.directory.set_public(&body.room_id);
|
||||
|
||||
if services.globals.config.admin_room_notices {
|
||||
services
|
||||
@@ -174,7 +177,7 @@ pub(crate) async fn set_room_visibility_route(
|
||||
}
|
||||
info!("{sender_user} made {0} public to the room directory", body.room_id);
|
||||
},
|
||||
room::Visibility::Private => services.rooms.directory.set_not_public(&body.room_id)?,
|
||||
room::Visibility::Private => services.rooms.directory.set_not_public(&body.room_id),
|
||||
_ => {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
@@ -192,13 +195,13 @@ pub(crate) async fn set_room_visibility_route(
|
||||
pub(crate) async fn get_room_visibility_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_room_visibility::v3::Request>,
|
||||
) -> Result<get_room_visibility::v3::Response> {
|
||||
if !services.rooms.metadata.exists(&body.room_id)? {
|
||||
if !services.rooms.metadata.exists(&body.room_id).await {
|
||||
// Return 404 if the room doesn't exist
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Room not found"));
|
||||
}
|
||||
|
||||
Ok(get_room_visibility::v3::Response {
|
||||
visibility: if services.rooms.directory.is_public_room(&body.room_id)? {
|
||||
visibility: if services.rooms.directory.is_public_room(&body.room_id).await {
|
||||
room::Visibility::Public
|
||||
} else {
|
||||
room::Visibility::Private
|
||||
@@ -257,101 +260,41 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
||||
}
|
||||
}
|
||||
|
||||
let mut all_rooms: Vec<_> = services
|
||||
let mut all_rooms: Vec<PublicRoomsChunk> = services
|
||||
.rooms
|
||||
.directory
|
||||
.public_rooms()
|
||||
.map(|room_id| {
|
||||
let room_id = room_id?;
|
||||
|
||||
let chunk = PublicRoomsChunk {
|
||||
canonical_alias: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_canonical_alias(&room_id)?,
|
||||
name: services.rooms.state_accessor.get_name(&room_id)?,
|
||||
num_joined_members: services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(&room_id)?
|
||||
.unwrap_or_else(|| {
|
||||
warn!("Room {} has no member count", room_id);
|
||||
0
|
||||
})
|
||||
.try_into()
|
||||
.expect("user count should not be that big"),
|
||||
topic: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_topic(&room_id)
|
||||
.unwrap_or(None),
|
||||
world_readable: services.rooms.state_accessor.is_world_readable(&room_id)?,
|
||||
guest_can_join: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.guest_can_join(&room_id)?,
|
||||
avatar_url: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_avatar(&room_id)?
|
||||
.into_option()
|
||||
.unwrap_or_default()
|
||||
.url,
|
||||
join_rule: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomJoinRules, "")?
|
||||
.map(|s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomJoinRulesEventContent| match c.join_rule {
|
||||
JoinRule::Public => Some(PublicRoomJoinRule::Public),
|
||||
JoinRule::Knock => Some(PublicRoomJoinRule::Knock),
|
||||
_ => None,
|
||||
})
|
||||
.map_err(|e| {
|
||||
err!(Database(error!("Invalid room join rule event in database: {e}")))
|
||||
})
|
||||
})
|
||||
.transpose()?
|
||||
.flatten()
|
||||
.ok_or_else(|| Error::bad_database("Missing room join rule event for room."))?,
|
||||
room_type: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_type(&room_id)?,
|
||||
room_id,
|
||||
};
|
||||
Ok(chunk)
|
||||
})
|
||||
.filter_map(|r: Result<_>| r.ok()) // Filter out buggy rooms
|
||||
.filter(|chunk| {
|
||||
.map(ToOwned::to_owned)
|
||||
.then(|room_id| public_rooms_chunk(services, room_id))
|
||||
.filter_map(|chunk| async move {
|
||||
if let Some(query) = filter.generic_search_term.as_ref().map(|q| q.to_lowercase()) {
|
||||
if let Some(name) = &chunk.name {
|
||||
if name.as_str().to_lowercase().contains(&query) {
|
||||
return true;
|
||||
return Some(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(topic) = &chunk.topic {
|
||||
if topic.to_lowercase().contains(&query) {
|
||||
return true;
|
||||
return Some(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(canonical_alias) = &chunk.canonical_alias {
|
||||
if canonical_alias.as_str().to_lowercase().contains(&query) {
|
||||
return true;
|
||||
return Some(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
} else {
|
||||
// No search term
|
||||
true
|
||||
return None;
|
||||
}
|
||||
|
||||
// No search term
|
||||
Some(chunk)
|
||||
})
|
||||
// We need to collect all, so we can sort by member count
|
||||
.collect();
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
all_rooms.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members));
|
||||
|
||||
@@ -394,22 +337,23 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
||||
|
||||
/// Check whether the user can publish to the room directory via power levels of
|
||||
/// room history visibility event or room creator
|
||||
fn user_can_publish_room(services: &Services, user_id: &UserId, room_id: &RoomId) -> Result<bool> {
|
||||
if let Some(event) = services
|
||||
async fn user_can_publish_room(services: &Services, user_id: &UserId, room_id: &RoomId) -> Result<bool> {
|
||||
if let Ok(event) = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")?
|
||||
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
|
||||
.await
|
||||
{
|
||||
serde_json::from_str(event.content.get())
|
||||
.map_err(|_| Error::bad_database("Invalid event content for m.room.power_levels"))
|
||||
.map(|content: RoomPowerLevelsEventContent| {
|
||||
RoomPowerLevels::from(content).user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
|
||||
})
|
||||
} else if let Some(event) =
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")?
|
||||
} else if let Ok(event) = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
{
|
||||
Ok(event.sender == user_id)
|
||||
} else {
|
||||
@@ -419,3 +363,62 @@ fn user_can_publish_room(services: &Services, user_id: &UserId, room_id: &RoomId
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> PublicRoomsChunk {
|
||||
PublicRoomsChunk {
|
||||
canonical_alias: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_canonical_alias(&room_id)
|
||||
.await
|
||||
.ok(),
|
||||
name: services.rooms.state_accessor.get_name(&room_id).await.ok(),
|
||||
num_joined_members: services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(&room_id)
|
||||
.await
|
||||
.unwrap_or(0)
|
||||
.try_into()
|
||||
.expect("joined count overflows ruma UInt"),
|
||||
topic: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_topic(&room_id)
|
||||
.await
|
||||
.ok(),
|
||||
world_readable: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.is_world_readable(&room_id)
|
||||
.await,
|
||||
guest_can_join: services.rooms.state_accessor.guest_can_join(&room_id).await,
|
||||
avatar_url: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_avatar(&room_id)
|
||||
.await
|
||||
.into_option()
|
||||
.unwrap_or_default()
|
||||
.url,
|
||||
join_rule: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
|
||||
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
|
||||
JoinRule::Public => PublicRoomJoinRule::Public,
|
||||
JoinRule::Knock => "knock".into(),
|
||||
JoinRule::KnockRestricted(_) => "knock_restricted".into(),
|
||||
_ => "invite".into(),
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
room_type: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_type(&room_id)
|
||||
.await
|
||||
.ok(),
|
||||
room_id,
|
||||
}
|
||||
}
|
||||
|
||||
+13
-12
@@ -1,10 +1,8 @@
|
||||
use axum::extract::State;
|
||||
use ruma::api::client::{
|
||||
error::ErrorKind,
|
||||
filter::{create_filter, get_filter},
|
||||
};
|
||||
use conduit::err;
|
||||
use ruma::api::client::filter::{create_filter, get_filter};
|
||||
|
||||
use crate::{Error, Result, Ruma};
|
||||
use crate::{Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/filter/{filterId}`
|
||||
///
|
||||
@@ -15,11 +13,13 @@ pub(crate) async fn get_filter_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_filter::v3::Request>,
|
||||
) -> Result<get_filter::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let Some(filter) = services.users.get_filter(sender_user, &body.filter_id)? else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Filter not found."));
|
||||
};
|
||||
|
||||
Ok(get_filter::v3::Response::new(filter))
|
||||
services
|
||||
.users
|
||||
.get_filter(sender_user, &body.filter_id)
|
||||
.await
|
||||
.map(get_filter::v3::Response::new)
|
||||
.map_err(|_| err!(Request(NotFound("Filter not found."))))
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/filter`
|
||||
@@ -29,7 +29,8 @@ pub(crate) async fn create_filter_route(
|
||||
State(services): State<crate::State>, body: Ruma<create_filter::v3::Request>,
|
||||
) -> Result<create_filter::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
Ok(create_filter::v3::Response::new(
|
||||
services.users.create_filter(sender_user, &body.filter)?,
|
||||
))
|
||||
|
||||
let filter_id = services.users.create_filter(sender_user, &body.filter);
|
||||
|
||||
Ok(create_filter::v3::Response::new(filter_id))
|
||||
}
|
||||
|
||||
+109
-91
@@ -4,8 +4,8 @@ use std::{
|
||||
};
|
||||
|
||||
use axum::extract::State;
|
||||
use conduit::{utils, utils::math::continue_exponential_backoff_secs, Err, Error, Result};
|
||||
use futures_util::{stream::FuturesUnordered, StreamExt};
|
||||
use conduit::{err, utils, utils::math::continue_exponential_backoff_secs, Err, Error, Result};
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
@@ -16,12 +16,15 @@ use ruma::{
|
||||
federation,
|
||||
},
|
||||
serde::Raw,
|
||||
DeviceKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
|
||||
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::SESSION_ID_LENGTH;
|
||||
use crate::{service::Services, Ruma};
|
||||
use crate::{
|
||||
service::{users::parse_master_key, Services},
|
||||
Ruma,
|
||||
};
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/upload`
|
||||
///
|
||||
@@ -33,13 +36,13 @@ use crate::{service::Services, Ruma};
|
||||
pub(crate) async fn upload_keys_route(
|
||||
State(services): State<crate::State>, body: Ruma<upload_keys::v3::Request>,
|
||||
) -> Result<upload_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
|
||||
for (key_key, key_value) in &body.one_time_keys {
|
||||
for (key_id, one_time_key) in &body.one_time_keys {
|
||||
services
|
||||
.users
|
||||
.add_one_time_key(sender_user, sender_device, key_key, key_value)?;
|
||||
.add_one_time_key(sender_user, sender_device, key_id, one_time_key)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(device_keys) = &body.device_keys {
|
||||
@@ -47,19 +50,22 @@ pub(crate) async fn upload_keys_route(
|
||||
// This check is needed to assure that signatures are kept
|
||||
if services
|
||||
.users
|
||||
.get_device_keys(sender_user, sender_device)?
|
||||
.is_none()
|
||||
.get_device_keys(sender_user, sender_device)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
services
|
||||
.users
|
||||
.add_device_keys(sender_user, sender_device, device_keys)?;
|
||||
.add_device_keys(sender_user, sender_device, device_keys)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(upload_keys::v3::Response {
|
||||
one_time_key_counts: services
|
||||
.users
|
||||
.count_one_time_keys(sender_user, sender_device)?,
|
||||
.count_one_time_keys(sender_user, sender_device)
|
||||
.await,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -120,7 +126,9 @@ pub(crate) async fn upload_signing_keys_route(
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
@@ -129,20 +137,24 @@ pub(crate) async fn upload_signing_keys_route(
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
.create(sender_user, sender_device, &uiaainfo, &json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
}
|
||||
|
||||
if let Some(master_key) = &body.master_key {
|
||||
services.users.add_cross_signing_keys(
|
||||
sender_user,
|
||||
master_key,
|
||||
&body.self_signing_key,
|
||||
&body.user_signing_key,
|
||||
true, // notify so that other users see the new keys
|
||||
)?;
|
||||
services
|
||||
.users
|
||||
.add_cross_signing_keys(
|
||||
sender_user,
|
||||
master_key,
|
||||
&body.self_signing_key,
|
||||
&body.user_signing_key,
|
||||
true, // notify so that other users see the new keys
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(upload_signing_keys::v3::Response {})
|
||||
@@ -179,9 +191,11 @@ pub(crate) async fn upload_signatures_route(
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Invalid signature value."))?
|
||||
.to_owned(),
|
||||
);
|
||||
|
||||
services
|
||||
.users
|
||||
.sign_key(user_id, key_id, signature, sender_user)?;
|
||||
.sign_key(user_id, key_id, signature, sender_user)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,56 +218,52 @@ pub(crate) async fn get_key_changes_route(
|
||||
|
||||
let mut device_list_updates = HashSet::new();
|
||||
|
||||
let from = body
|
||||
.from
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?;
|
||||
|
||||
let to = body
|
||||
.to
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?;
|
||||
|
||||
device_list_updates.extend(
|
||||
services
|
||||
.users
|
||||
.keys_changed(
|
||||
sender_user.as_str(),
|
||||
body.from
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?,
|
||||
Some(
|
||||
body.to
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?,
|
||||
),
|
||||
)
|
||||
.filter_map(Result::ok),
|
||||
.keys_changed(sender_user, from, Some(to))
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await,
|
||||
);
|
||||
|
||||
for room_id in services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let mut rooms_joined = services.rooms.state_cache.rooms_joined(sender_user).boxed();
|
||||
|
||||
while let Some(room_id) = rooms_joined.next().await {
|
||||
device_list_updates.extend(
|
||||
services
|
||||
.users
|
||||
.keys_changed(
|
||||
room_id.as_ref(),
|
||||
body.from
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?,
|
||||
Some(
|
||||
body.to
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?,
|
||||
),
|
||||
)
|
||||
.filter_map(Result::ok),
|
||||
.room_keys_changed(room_id, from, Some(to))
|
||||
.map(|(user_id, _)| user_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(get_key_changes::v3::Response {
|
||||
changed: device_list_updates.into_iter().collect(),
|
||||
left: Vec::new(), // TODO
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool + Send>(
|
||||
pub(crate) async fn get_keys_helper<F>(
|
||||
services: &Services, sender_user: Option<&UserId>, device_keys_input: &BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>,
|
||||
allowed_signatures: F, include_display_names: bool,
|
||||
) -> Result<get_keys::v3::Response> {
|
||||
) -> Result<get_keys::v3::Response>
|
||||
where
|
||||
F: Fn(&UserId) -> bool + Send + Sync,
|
||||
{
|
||||
let mut master_keys = BTreeMap::new();
|
||||
let mut self_signing_keys = BTreeMap::new();
|
||||
let mut user_signing_keys = BTreeMap::new();
|
||||
@@ -274,56 +284,60 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool + Send>(
|
||||
|
||||
if device_ids.is_empty() {
|
||||
let mut container = BTreeMap::new();
|
||||
for device_id in services.users.all_device_ids(user_id) {
|
||||
let device_id = device_id?;
|
||||
if let Some(mut keys) = services.users.get_device_keys(user_id, &device_id)? {
|
||||
let mut devices = services.users.all_device_ids(user_id).boxed();
|
||||
|
||||
while let Some(device_id) = devices.next().await {
|
||||
if let Ok(mut keys) = services.users.get_device_keys(user_id, device_id).await {
|
||||
let metadata = services
|
||||
.users
|
||||
.get_device_metadata(user_id, &device_id)?
|
||||
.ok_or_else(|| Error::bad_database("all_device_keys contained nonexistent device."))?;
|
||||
.get_device_metadata(user_id, device_id)
|
||||
.await
|
||||
.map_err(|_| err!(Database("all_device_keys contained nonexistent device.")))?;
|
||||
|
||||
add_unsigned_device_display_name(&mut keys, metadata, include_display_names)
|
||||
.map_err(|_| Error::bad_database("invalid device keys in database"))?;
|
||||
.map_err(|_| err!(Database("invalid device keys in database")))?;
|
||||
|
||||
container.insert(device_id, keys);
|
||||
container.insert(device_id.to_owned(), keys);
|
||||
}
|
||||
}
|
||||
|
||||
device_keys.insert(user_id.to_owned(), container);
|
||||
} else {
|
||||
for device_id in device_ids {
|
||||
let mut container = BTreeMap::new();
|
||||
if let Some(mut keys) = services.users.get_device_keys(user_id, device_id)? {
|
||||
if let Ok(mut keys) = services.users.get_device_keys(user_id, device_id).await {
|
||||
let metadata = services
|
||||
.users
|
||||
.get_device_metadata(user_id, device_id)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Tried to get keys for nonexistent device.",
|
||||
))?;
|
||||
.get_device_metadata(user_id, device_id)
|
||||
.await
|
||||
.map_err(|_| err!(Request(InvalidParam("Tried to get keys for nonexistent device."))))?;
|
||||
|
||||
add_unsigned_device_display_name(&mut keys, metadata, include_display_names)
|
||||
.map_err(|_| Error::bad_database("invalid device keys in database"))?;
|
||||
.map_err(|_| err!(Database("invalid device keys in database")))?;
|
||||
|
||||
container.insert(device_id.to_owned(), keys);
|
||||
}
|
||||
|
||||
device_keys.insert(user_id.to_owned(), container);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(master_key) = services
|
||||
if let Ok(master_key) = services
|
||||
.users
|
||||
.get_master_key(sender_user, user_id, &allowed_signatures)?
|
||||
.get_master_key(sender_user, user_id, &allowed_signatures)
|
||||
.await
|
||||
{
|
||||
master_keys.insert(user_id.to_owned(), master_key);
|
||||
}
|
||||
if let Some(self_signing_key) =
|
||||
services
|
||||
.users
|
||||
.get_self_signing_key(sender_user, user_id, &allowed_signatures)?
|
||||
if let Ok(self_signing_key) = services
|
||||
.users
|
||||
.get_self_signing_key(sender_user, user_id, &allowed_signatures)
|
||||
.await
|
||||
{
|
||||
self_signing_keys.insert(user_id.to_owned(), self_signing_key);
|
||||
}
|
||||
if Some(user_id) == sender_user {
|
||||
if let Some(user_signing_key) = services.users.get_user_signing_key(user_id)? {
|
||||
if let Ok(user_signing_key) = services.users.get_user_signing_key(user_id).await {
|
||||
user_signing_keys.insert(user_id.to_owned(), user_signing_key);
|
||||
}
|
||||
}
|
||||
@@ -385,24 +399,27 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool + Send>(
|
||||
|
||||
while let Some((server, response)) = futures.next().await {
|
||||
if let Ok(Ok(response)) = response {
|
||||
for (user, masterkey) in response.master_keys {
|
||||
let (master_key_id, mut master_key) = services.users.parse_master_key(&user, &masterkey)?;
|
||||
for (user, master_key) in response.master_keys {
|
||||
let (master_key_id, mut master_key) = parse_master_key(&user, &master_key)?;
|
||||
|
||||
if let Some(our_master_key) =
|
||||
services
|
||||
.users
|
||||
.get_key(&master_key_id, sender_user, &user, &allowed_signatures)?
|
||||
if let Ok(our_master_key) = services
|
||||
.users
|
||||
.get_key(&master_key_id, sender_user, &user, &allowed_signatures)
|
||||
.await
|
||||
{
|
||||
let (_, our_master_key) = services.users.parse_master_key(&user, &our_master_key)?;
|
||||
master_key.signatures.extend(our_master_key.signatures);
|
||||
let (_, mut our_master_key) = parse_master_key(&user, &our_master_key)?;
|
||||
master_key.signatures.append(&mut our_master_key.signatures);
|
||||
}
|
||||
let json = serde_json::to_value(master_key).expect("to_value always works");
|
||||
let raw = serde_json::from_value(json).expect("Raw::from_value always works");
|
||||
services.users.add_cross_signing_keys(
|
||||
&user, &raw, &None, &None,
|
||||
false, /* Dont notify. A notification would trigger another key request resulting in an
|
||||
* endless loop */
|
||||
)?;
|
||||
services
|
||||
.users
|
||||
.add_cross_signing_keys(
|
||||
&user, &raw, &None, &None,
|
||||
false, /* Dont notify. A notification would trigger another key request resulting in an
|
||||
* endless loop */
|
||||
)
|
||||
.await?;
|
||||
master_keys.insert(user.clone(), raw);
|
||||
}
|
||||
|
||||
@@ -449,7 +466,7 @@ fn add_unsigned_device_display_name(
|
||||
}
|
||||
|
||||
pub(crate) async fn claim_keys_helper(
|
||||
services: &Services, one_time_keys_input: &BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, DeviceKeyAlgorithm>>,
|
||||
services: &Services, one_time_keys_input: &BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, OneTimeKeyAlgorithm>>,
|
||||
) -> Result<claim_keys::v3::Response> {
|
||||
let mut one_time_keys = BTreeMap::new();
|
||||
|
||||
@@ -465,9 +482,10 @@ pub(crate) async fn claim_keys_helper(
|
||||
|
||||
let mut container = BTreeMap::new();
|
||||
for (device_id, key_algorithm) in map {
|
||||
if let Some(one_time_keys) = services
|
||||
if let Ok(one_time_keys) = services
|
||||
.users
|
||||
.take_one_time_key(user_id, device_id, key_algorithm)?
|
||||
.take_one_time_key(user_id, device_id, key_algorithm)
|
||||
.await
|
||||
{
|
||||
let mut c = BTreeMap::new();
|
||||
c.insert(one_time_keys.0, one_time_keys.1);
|
||||
|
||||
+18
-7
@@ -11,6 +11,7 @@ use conduit_service::{
|
||||
media::{Dim, FileMeta, CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, MXC_LENGTH},
|
||||
Services,
|
||||
};
|
||||
use reqwest::Url;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
authenticated_media::{
|
||||
@@ -165,23 +166,33 @@ pub(crate) async fn get_media_preview_route(
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let url = &body.url;
|
||||
if !services.media.url_preview_allowed(url) {
|
||||
let url = Url::parse(&body.url).map_err(|e| {
|
||||
err!(Request(InvalidParam(
|
||||
debug_warn!(%sender_user, %url, "Requested URL is not valid: {e}")
|
||||
)))
|
||||
})?;
|
||||
|
||||
if !services.media.url_preview_allowed(&url) {
|
||||
return Err!(Request(Forbidden(
|
||||
debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
|
||||
)));
|
||||
}
|
||||
|
||||
let preview = services.media.get_url_preview(url).await.map_err(|error| {
|
||||
err!(Request(Unknown(
|
||||
debug_error!(%sender_user, %url, ?error, "Failed to fetch URL preview.")
|
||||
)))
|
||||
})?;
|
||||
let preview = services
|
||||
.media
|
||||
.get_url_preview(&url)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
err!(Request(Unknown(
|
||||
debug_error!(%sender_user, %url, "Failed to fetch URL preview: {error}")
|
||||
)))
|
||||
})?;
|
||||
|
||||
serde_json::value::to_raw_value(&preview)
|
||||
.map(get_media_preview::v1::Response::from_raw_value)
|
||||
.map_err(|error| {
|
||||
err!(Request(Unknown(
|
||||
debug_error!(%sender_user, %url, ?error, "Failed to parse URL preview.")
|
||||
debug_error!(%sender_user, %url, "Failed to parse URL preview: {error}")
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use conduit::{
|
||||
Err, Result,
|
||||
};
|
||||
use conduit_service::media::{Dim, FileMeta, CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN};
|
||||
use reqwest::Url;
|
||||
use ruma::{
|
||||
api::client::media::{
|
||||
create_content, get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
|
||||
@@ -55,25 +56,31 @@ pub(crate) async fn get_media_preview_legacy_route(
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let url = &body.url;
|
||||
if !services.media.url_preview_allowed(url) {
|
||||
let url = Url::parse(&body.url).map_err(|e| {
|
||||
err!(Request(InvalidParam(
|
||||
debug_warn!(%sender_user, %url, "Requested URL is not valid: {e}")
|
||||
)))
|
||||
})?;
|
||||
|
||||
if !services.media.url_preview_allowed(&url) {
|
||||
return Err!(Request(Forbidden(
|
||||
debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
|
||||
)));
|
||||
}
|
||||
|
||||
let preview = services.media.get_url_preview(url).await.map_err(|e| {
|
||||
let preview = services.media.get_url_preview(&url).await.map_err(|e| {
|
||||
err!(Request(Unknown(
|
||||
debug_error!(%sender_user, %url, "Failed to fetch a URL preview: {e}")
|
||||
)))
|
||||
})?;
|
||||
|
||||
let res = serde_json::value::to_raw_value(&preview).map_err(|e| {
|
||||
err!(Request(Unknown(
|
||||
debug_error!(%sender_user, %url, "Failed to parse a URL preview: {e}")
|
||||
)))
|
||||
})?;
|
||||
|
||||
Ok(get_media_preview::v3::Response::from_raw_value(res))
|
||||
serde_json::value::to_raw_value(&preview)
|
||||
.map(get_media_preview::v3::Response::from_raw_value)
|
||||
.map_err(|error| {
|
||||
err!(Request(Unknown(
|
||||
debug_error!(%sender_user, %url, "Failed to parse URL preview: {error}")
|
||||
)))
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/media/v1/preview_url`
|
||||
|
||||
+520
-604
File diff suppressed because it is too large
Load Diff
+193
-234
@@ -1,109 +1,53 @@
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduit::PduCount;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
filter::{RoomEventFilter, UrlFilter},
|
||||
message::{get_message_events, send_message_event},
|
||||
use conduit::{
|
||||
at, is_equal_to,
|
||||
utils::{
|
||||
result::{FlatOk, LogErr},
|
||||
stream::{BroadbandExt, WidebandExt},
|
||||
IterStream, ReadyExt,
|
||||
},
|
||||
events::{MessageLikeEventType, StateEventType},
|
||||
RoomId, UserId,
|
||||
Event, PduCount, Result,
|
||||
};
|
||||
use serde_json::{from_str, Value};
|
||||
|
||||
use crate::{
|
||||
service::{pdu::PduBuilder, Services},
|
||||
utils, Error, PduEvent, Result, Ruma,
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{filter::RoomEventFilter, message::get_message_events},
|
||||
Direction,
|
||||
},
|
||||
events::{AnyStateEvent, StateEventType, TimelineEventType, TimelineEventType::*},
|
||||
serde::Raw,
|
||||
DeviceId, OwnedUserId, RoomId, UserId,
|
||||
};
|
||||
use service::{rooms::timeline::PdusIterItem, Services};
|
||||
|
||||
/// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`
|
||||
///
|
||||
/// Send a message event into the room.
|
||||
///
|
||||
/// - Is a NOOP if the txn id was already used before and returns the same event
|
||||
/// id again
|
||||
/// - The only requirement for the content is that it has to be valid json
|
||||
/// - Tries to send the event into the room, auth rules will determine if it is
|
||||
/// allowed
|
||||
pub(crate) async fn send_message_event_route(
|
||||
State(services): State<crate::State>, body: Ruma<send_message_event::v3::Request>,
|
||||
) -> Result<send_message_event::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_deref();
|
||||
use crate::Ruma;
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
pub(crate) type LazySet = HashSet<OwnedUserId>;
|
||||
|
||||
// Forbid m.room.encrypted if encryption is disabled
|
||||
if MessageLikeEventType::RoomEncrypted == body.event_type && !services.globals.allow_encryption() {
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Encryption has been disabled"));
|
||||
}
|
||||
/// list of safe and common non-state events to ignore if the user is ignored
|
||||
const IGNORED_MESSAGE_TYPES: &[TimelineEventType; 16] = &[
|
||||
RoomMessage,
|
||||
Sticker,
|
||||
CallInvite,
|
||||
CallNotify,
|
||||
RoomEncrypted,
|
||||
Image,
|
||||
File,
|
||||
Audio,
|
||||
Voice,
|
||||
Video,
|
||||
UnstablePollStart,
|
||||
PollStart,
|
||||
KeyVerificationStart,
|
||||
Reaction,
|
||||
Emote,
|
||||
Location,
|
||||
];
|
||||
|
||||
if body.event_type == MessageLikeEventType::CallInvite && services.rooms.directory.is_public_room(&body.room_id)? {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"Room call invites are not allowed in public rooms",
|
||||
));
|
||||
}
|
||||
|
||||
// Check if this is a new transaction id
|
||||
if let Some(response) = services
|
||||
.transaction_ids
|
||||
.existing_txnid(sender_user, sender_device, &body.txn_id)?
|
||||
{
|
||||
// The client might have sent a txnid of the /sendToDevice endpoint
|
||||
// This txnid has no response associated with it
|
||||
if response.is_empty() {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Tried to use txn id already used for an incompatible endpoint.",
|
||||
));
|
||||
}
|
||||
|
||||
let event_id = utils::string_from_bytes(&response)
|
||||
.map_err(|_| Error::bad_database("Invalid txnid bytes in database."))?
|
||||
.try_into()
|
||||
.map_err(|_| Error::bad_database("Invalid event id in txnid data."))?;
|
||||
return Ok(send_message_event::v3::Response {
|
||||
event_id,
|
||||
});
|
||||
}
|
||||
|
||||
let mut unsigned = BTreeMap::new();
|
||||
unsigned.insert("transaction_id".to_owned(), body.txn_id.to_string().into());
|
||||
|
||||
let event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: body.event_type.to_string().into(),
|
||||
content: from_str(body.body.body.json().get())
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid JSON body."))?,
|
||||
unsigned: Some(unsigned),
|
||||
state_key: None,
|
||||
redacts: None,
|
||||
timestamp: if body.appservice_info.is_some() {
|
||||
body.timestamp
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
services
|
||||
.transaction_ids
|
||||
.add_txnid(sender_user, sender_device, &body.txn_id, event_id.as_bytes())?;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
Ok(send_message_event::v3::Response::new((*event_id).to_owned()))
|
||||
}
|
||||
const LIMIT_MAX: usize = 100;
|
||||
const LIMIT_DEFAULT: usize = 10;
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/messages`
|
||||
///
|
||||
@@ -114,169 +58,184 @@ pub(crate) async fn send_message_event_route(
|
||||
pub(crate) async fn get_message_events_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_message_events::v3::Request>,
|
||||
) -> Result<get_message_events::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
let sender = body.sender();
|
||||
let (sender_user, sender_device) = sender;
|
||||
let room_id = &body.room_id;
|
||||
let filter = &body.filter;
|
||||
|
||||
let from = match body.from.clone() {
|
||||
Some(from) => PduCount::try_from_string(&from)?,
|
||||
None => match body.dir {
|
||||
ruma::api::Direction::Forward => PduCount::min(),
|
||||
ruma::api::Direction::Backward => PduCount::max(),
|
||||
},
|
||||
};
|
||||
let from: PduCount = body
|
||||
.from
|
||||
.as_deref()
|
||||
.map(str::parse)
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| match body.dir {
|
||||
Direction::Forward => PduCount::min(),
|
||||
Direction::Backward => PduCount::max(),
|
||||
});
|
||||
|
||||
let to = body
|
||||
.to
|
||||
.as_ref()
|
||||
.and_then(|t| PduCount::try_from_string(t).ok());
|
||||
let to: Option<PduCount> = body.to.as_deref().map(str::parse).flat_ok();
|
||||
|
||||
let limit: usize = body
|
||||
.limit
|
||||
.try_into()
|
||||
.unwrap_or(LIMIT_DEFAULT)
|
||||
.min(LIMIT_MAX);
|
||||
|
||||
services
|
||||
.rooms
|
||||
.lazy_loading
|
||||
.lazy_load_confirm_delivery(sender_user, sender_device, &body.room_id, from)
|
||||
.await?;
|
||||
.lazy_load_confirm_delivery(sender_user, sender_device, room_id, from);
|
||||
|
||||
let limit = usize::try_from(body.limit).unwrap_or(10).min(100);
|
||||
|
||||
let next_token;
|
||||
|
||||
let mut resp = get_message_events::v3::Response::new();
|
||||
|
||||
let mut lazy_loaded = HashSet::new();
|
||||
|
||||
match body.dir {
|
||||
ruma::api::Direction::Forward => {
|
||||
let events_after: Vec<_> = services
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_after(sender_user, &body.room_id, from)?
|
||||
.filter_map(Result::ok) // Filter out buggy events
|
||||
.filter(|(_, pdu)| { contains_url_filter(pdu, &body.filter) && visibility_filter(&services, pdu, sender_user, &body.room_id)
|
||||
|
||||
})
|
||||
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
|
||||
.take(limit)
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_after {
|
||||
/* TODO: Remove the not "element_hacks" check when these are resolved:
|
||||
* https://github.com/vector-im/element-android/issues/3417
|
||||
* https://github.com/vector-im/element-web/issues/21034
|
||||
*/
|
||||
if !cfg!(feature = "element_hacks")
|
||||
&& !services.rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&body.room_id,
|
||||
&event.sender,
|
||||
)? {
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
|
||||
next_token = events_after.last().map(|(count, _)| count).copied();
|
||||
|
||||
let events_after: Vec<_> = events_after
|
||||
.into_iter()
|
||||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect();
|
||||
|
||||
resp.start = from.stringify();
|
||||
resp.end = next_token.map(|count| count.stringify());
|
||||
resp.chunk = events_after;
|
||||
},
|
||||
ruma::api::Direction::Backward => {
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.backfill_if_required(&body.room_id, from)
|
||||
.await?;
|
||||
let events_before: Vec<_> = services
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_until(sender_user, &body.room_id, from)?
|
||||
.filter_map(Result::ok) // Filter out buggy events
|
||||
.filter(|(_, pdu)| {contains_url_filter(pdu, &body.filter) && visibility_filter(&services, pdu, sender_user, &body.room_id)})
|
||||
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
|
||||
.take(limit)
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_before {
|
||||
/* TODO: Remove the not "element_hacks" check when these are resolved:
|
||||
* https://github.com/vector-im/element-android/issues/3417
|
||||
* https://github.com/vector-im/element-web/issues/21034
|
||||
*/
|
||||
if !cfg!(feature = "element_hacks")
|
||||
&& !services.rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&body.room_id,
|
||||
&event.sender,
|
||||
)? {
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
|
||||
next_token = events_before.last().map(|(count, _)| count).copied();
|
||||
|
||||
let events_before: Vec<_> = events_before
|
||||
.into_iter()
|
||||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect();
|
||||
|
||||
resp.start = from.stringify();
|
||||
resp.end = next_token.map(|count| count.stringify());
|
||||
resp.chunk = events_before;
|
||||
},
|
||||
if matches!(body.dir, Direction::Backward) {
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.backfill_if_required(room_id, from)
|
||||
.boxed()
|
||||
.await
|
||||
.log_err()
|
||||
.ok();
|
||||
}
|
||||
|
||||
resp.state = Vec::new();
|
||||
for ll_id in &lazy_loaded {
|
||||
if let Some(member_event) =
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &StateEventType::RoomMember, ll_id.as_str())?
|
||||
{
|
||||
resp.state.push(member_event.to_state_event());
|
||||
}
|
||||
}
|
||||
let it = match body.dir {
|
||||
Direction::Forward => services
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus(Some(sender_user), room_id, Some(from))
|
||||
.await?
|
||||
.boxed(),
|
||||
|
||||
Direction::Backward => services
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_rev(Some(sender_user), room_id, Some(from))
|
||||
.await?
|
||||
.boxed(),
|
||||
};
|
||||
|
||||
let events: Vec<_> = it
|
||||
.ready_take_while(|(count, _)| Some(*count) != to)
|
||||
.ready_filter_map(|item| event_filter(item, filter))
|
||||
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
|
||||
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
|
||||
.take(limit)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let lazy = events
|
||||
.iter()
|
||||
.stream()
|
||||
.fold(LazySet::new(), |lazy, item| {
|
||||
update_lazy(&services, room_id, sender, lazy, item, false)
|
||||
})
|
||||
.await;
|
||||
|
||||
let state = lazy
|
||||
.iter()
|
||||
.stream()
|
||||
.broad_filter_map(|user_id| get_member_event(&services, room_id, user_id))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let next_token = events.last().map(at!(0));
|
||||
|
||||
// remove the feature check when we are sure clients like element can handle it
|
||||
if !cfg!(feature = "element_hacks") {
|
||||
if let Some(next_token) = next_token {
|
||||
services
|
||||
.rooms
|
||||
.lazy_loading
|
||||
.lazy_load_mark_sent(sender_user, sender_device, &body.room_id, lazy_loaded, next_token)
|
||||
.await;
|
||||
.lazy_load_mark_sent(sender_user, sender_device, room_id, lazy, next_token);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resp)
|
||||
let chunk = events
|
||||
.into_iter()
|
||||
.map(at!(1))
|
||||
.map(|pdu| pdu.to_room_event())
|
||||
.collect();
|
||||
|
||||
Ok(get_message_events::v3::Response {
|
||||
start: from.to_string(),
|
||||
end: next_token.as_ref().map(ToString::to_string),
|
||||
chunk,
|
||||
state,
|
||||
})
|
||||
}
|
||||
|
||||
fn visibility_filter(services: &Services, pdu: &PduEvent, user_id: &UserId, room_id: &RoomId) -> bool {
|
||||
async fn get_member_event(services: &Services, room_id: &RoomId, user_id: &UserId) -> Option<Raw<AnyStateEvent>> {
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(user_id, room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())
|
||||
.await
|
||||
.map(|member_event| member_event.to_state_event())
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn contains_url_filter(pdu: &PduEvent, filter: &RoomEventFilter) -> bool {
|
||||
if filter.url_filter.is_none() {
|
||||
return true;
|
||||
pub(crate) async fn update_lazy(
|
||||
services: &Services, room_id: &RoomId, sender: (&UserId, &DeviceId), mut lazy: LazySet, item: &PdusIterItem,
|
||||
force: bool,
|
||||
) -> LazySet {
|
||||
let (_, event) = &item;
|
||||
let (sender_user, sender_device) = sender;
|
||||
|
||||
/* TODO: Remove the not "element_hacks" check when these are resolved:
|
||||
* https://github.com/vector-im/element-android/issues/3417
|
||||
* https://github.com/vector-im/element-web/issues/21034
|
||||
*/
|
||||
if force || cfg!(features = "element_hacks") {
|
||||
lazy.insert(event.sender().into());
|
||||
return lazy;
|
||||
}
|
||||
|
||||
let content: Value = from_str(pdu.content.get()).unwrap();
|
||||
match filter.url_filter {
|
||||
Some(UrlFilter::EventsWithoutUrl) => !content["url"].is_string(),
|
||||
Some(UrlFilter::EventsWithUrl) => content["url"].is_string(),
|
||||
None => true,
|
||||
if lazy.contains(event.sender()) {
|
||||
return lazy;
|
||||
}
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.lazy_loading
|
||||
.lazy_load_was_sent_before(sender_user, sender_device, room_id, event.sender())
|
||||
.await
|
||||
{
|
||||
lazy.insert(event.sender().into());
|
||||
}
|
||||
|
||||
lazy
|
||||
}
|
||||
|
||||
pub(crate) async fn ignored_filter(services: &Services, item: PdusIterItem, user_id: &UserId) -> Option<PdusIterItem> {
|
||||
let (_, pdu) = &item;
|
||||
|
||||
// exclude Synapse's dummy events from bloating up response bodies. clients
|
||||
// don't need to see this.
|
||||
if pdu.kind.to_cow_str() == "org.matrix.dummy_event" {
|
||||
return None;
|
||||
}
|
||||
|
||||
if IGNORED_MESSAGE_TYPES.iter().any(is_equal_to!(&pdu.kind))
|
||||
&& services.users.user_is_ignored(&pdu.sender, user_id).await
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(item)
|
||||
}
|
||||
|
||||
pub(crate) async fn visibility_filter(
|
||||
services: &Services, item: PdusIterItem, user_id: &UserId,
|
||||
) -> Option<PdusIterItem> {
|
||||
let (_, pdu) = &item;
|
||||
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(user_id, &pdu.room_id, &pdu.event_id)
|
||||
.await
|
||||
.then_some(item)
|
||||
}
|
||||
|
||||
pub(crate) fn event_filter(item: PdusIterItem, filter: &RoomEventFilter) -> Option<PdusIterItem> {
|
||||
let (_, pdu) = &item;
|
||||
pdu.matches(filter).then_some(item)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ pub(super) mod relations;
|
||||
pub(super) mod report;
|
||||
pub(super) mod room;
|
||||
pub(super) mod search;
|
||||
pub(super) mod send;
|
||||
pub(super) mod session;
|
||||
pub(super) mod space;
|
||||
pub(super) mod state;
|
||||
@@ -36,6 +37,7 @@ pub(super) mod unstable;
|
||||
pub(super) mod unversioned;
|
||||
pub(super) mod user_directory;
|
||||
pub(super) mod voip;
|
||||
pub(super) mod well_known;
|
||||
|
||||
pub use account::full_user_deactivate;
|
||||
pub(super) use account::*;
|
||||
@@ -52,7 +54,7 @@ pub(super) use keys::*;
|
||||
pub(super) use media::*;
|
||||
pub(super) use media_legacy::*;
|
||||
pub(super) use membership::*;
|
||||
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room, validate_and_add_event_id};
|
||||
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room};
|
||||
pub(super) use message::*;
|
||||
pub(super) use openid::*;
|
||||
pub(super) use presence::*;
|
||||
@@ -65,6 +67,7 @@ pub(super) use relations::*;
|
||||
pub(super) use report::*;
|
||||
pub(super) use room::*;
|
||||
pub(super) use search::*;
|
||||
pub(super) use send::*;
|
||||
pub(super) use session::*;
|
||||
pub(super) use space::*;
|
||||
pub(super) use state::*;
|
||||
@@ -78,6 +81,7 @@ pub(super) use unstable::*;
|
||||
pub(super) use unversioned::*;
|
||||
pub(super) use user_directory::*;
|
||||
pub(super) use voip::*;
|
||||
pub(super) use well_known::*;
|
||||
|
||||
/// generated device ID length
|
||||
const DEVICE_ID_LENGTH: usize = 10;
|
||||
|
||||
@@ -28,7 +28,8 @@ pub(crate) async fn set_presence_route(
|
||||
|
||||
services
|
||||
.presence
|
||||
.set_presence(sender_user, &body.presence, None, None, body.status_msg.clone())?;
|
||||
.set_presence(sender_user, &body.presence, None, None, body.status_msg.clone())
|
||||
.await?;
|
||||
|
||||
Ok(set_presence::v3::Response {})
|
||||
}
|
||||
@@ -49,14 +50,15 @@ pub(crate) async fn get_presence_route(
|
||||
|
||||
let mut presence_event = None;
|
||||
|
||||
for _room_id in services
|
||||
let has_shared_rooms = services
|
||||
.rooms
|
||||
.user
|
||||
.get_shared_rooms(vec![sender_user.clone(), body.user_id.clone()])?
|
||||
{
|
||||
if let Some(presence) = services.presence.get_presence(&body.user_id)? {
|
||||
.state_cache
|
||||
.user_sees_user(sender_user, &body.user_id)
|
||||
.await;
|
||||
|
||||
if has_shared_rooms {
|
||||
if let Ok(presence) = services.presence.get_presence(&body.user_id).await {
|
||||
presence_event = Some(presence);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+106
-128
@@ -1,5 +1,10 @@
|
||||
use axum::extract::State;
|
||||
use conduit::{pdu::PduBuilder, warn, Err, Error, Result};
|
||||
use conduit::{
|
||||
pdu::PduBuilder,
|
||||
utils::{stream::TryIgnore, IterStream},
|
||||
warn, Err, Error, Result,
|
||||
};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
@@ -8,11 +13,10 @@ use ruma::{
|
||||
},
|
||||
federation,
|
||||
},
|
||||
events::{room::member::RoomMemberEventContent, StateEventType, TimelineEventType},
|
||||
events::{room::member::RoomMemberEventContent, StateEventType},
|
||||
presence::PresenceState,
|
||||
OwnedMxcUri, OwnedRoomId, UserId,
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use service::Services;
|
||||
|
||||
use crate::Ruma;
|
||||
@@ -35,16 +39,18 @@ pub(crate) async fn set_displayname_route(
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&body.user_id)
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
update_displayname(&services, &body.user_id, body.displayname.clone(), all_joined_rooms).await?;
|
||||
update_displayname(&services, &body.user_id, body.displayname.clone(), &all_joined_rooms).await?;
|
||||
|
||||
if services.globals.allow_local_presence() {
|
||||
// Presence update
|
||||
services
|
||||
.presence
|
||||
.ping_presence(&body.user_id, &PresenceState::Online)?;
|
||||
.ping_presence(&body.user_id, &PresenceState::Online)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(set_display_name::v3::Response {})
|
||||
@@ -72,22 +78,19 @@ pub(crate) async fn get_displayname_route(
|
||||
)
|
||||
.await
|
||||
{
|
||||
if !services.users.exists(&body.user_id)? {
|
||||
if !services.users.exists(&body.user_id).await {
|
||||
services.users.create(&body.user_id, None)?;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_displayname(&body.user_id, response.displayname.clone())
|
||||
.await?;
|
||||
.set_displayname(&body.user_id, response.displayname.clone());
|
||||
services
|
||||
.users
|
||||
.set_avatar_url(&body.user_id, response.avatar_url.clone())
|
||||
.await?;
|
||||
.set_avatar_url(&body.user_id, response.avatar_url.clone());
|
||||
services
|
||||
.users
|
||||
.set_blurhash(&body.user_id, response.blurhash.clone())
|
||||
.await?;
|
||||
.set_blurhash(&body.user_id, response.blurhash.clone());
|
||||
|
||||
return Ok(get_display_name::v3::Response {
|
||||
displayname: response.displayname,
|
||||
@@ -95,14 +98,14 @@ pub(crate) async fn get_displayname_route(
|
||||
}
|
||||
}
|
||||
|
||||
if !services.users.exists(&body.user_id)? {
|
||||
if !services.users.exists(&body.user_id).await {
|
||||
// Return 404 if this user doesn't exist and we couldn't fetch it over
|
||||
// federation
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
|
||||
}
|
||||
|
||||
Ok(get_display_name::v3::Response {
|
||||
displayname: services.users.displayname(&body.user_id)?,
|
||||
displayname: services.users.displayname(&body.user_id).await.ok(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -124,15 +127,16 @@ pub(crate) async fn set_avatar_url_route(
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&body.user_id)
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
update_avatar_url(
|
||||
&services,
|
||||
&body.user_id,
|
||||
body.avatar_url.clone(),
|
||||
body.blurhash.clone(),
|
||||
all_joined_rooms,
|
||||
&all_joined_rooms,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -140,7 +144,9 @@ pub(crate) async fn set_avatar_url_route(
|
||||
// Presence update
|
||||
services
|
||||
.presence
|
||||
.ping_presence(&body.user_id, &PresenceState::Online)?;
|
||||
.ping_presence(&body.user_id, &PresenceState::Online)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(set_avatar_url::v3::Response {})
|
||||
@@ -168,22 +174,21 @@ pub(crate) async fn get_avatar_url_route(
|
||||
)
|
||||
.await
|
||||
{
|
||||
if !services.users.exists(&body.user_id)? {
|
||||
if !services.users.exists(&body.user_id).await {
|
||||
services.users.create(&body.user_id, None)?;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_displayname(&body.user_id, response.displayname.clone())
|
||||
.await?;
|
||||
.set_displayname(&body.user_id, response.displayname.clone());
|
||||
|
||||
services
|
||||
.users
|
||||
.set_avatar_url(&body.user_id, response.avatar_url.clone())
|
||||
.await?;
|
||||
.set_avatar_url(&body.user_id, response.avatar_url.clone());
|
||||
|
||||
services
|
||||
.users
|
||||
.set_blurhash(&body.user_id, response.blurhash.clone())
|
||||
.await?;
|
||||
.set_blurhash(&body.user_id, response.blurhash.clone());
|
||||
|
||||
return Ok(get_avatar_url::v3::Response {
|
||||
avatar_url: response.avatar_url,
|
||||
@@ -192,15 +197,15 @@ pub(crate) async fn get_avatar_url_route(
|
||||
}
|
||||
}
|
||||
|
||||
if !services.users.exists(&body.user_id)? {
|
||||
if !services.users.exists(&body.user_id).await {
|
||||
// Return 404 if this user doesn't exist and we couldn't fetch it over
|
||||
// federation
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
|
||||
}
|
||||
|
||||
Ok(get_avatar_url::v3::Response {
|
||||
avatar_url: services.users.avatar_url(&body.user_id)?,
|
||||
blurhash: services.users.blurhash(&body.user_id)?,
|
||||
avatar_url: services.users.avatar_url(&body.user_id).await.ok(),
|
||||
blurhash: services.users.blurhash(&body.user_id).await.ok(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -226,31 +231,30 @@ pub(crate) async fn get_profile_route(
|
||||
)
|
||||
.await
|
||||
{
|
||||
if !services.users.exists(&body.user_id)? {
|
||||
if !services.users.exists(&body.user_id).await {
|
||||
services.users.create(&body.user_id, None)?;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_displayname(&body.user_id, response.displayname.clone())
|
||||
.await?;
|
||||
.set_displayname(&body.user_id, response.displayname.clone());
|
||||
|
||||
services
|
||||
.users
|
||||
.set_avatar_url(&body.user_id, response.avatar_url.clone())
|
||||
.await?;
|
||||
.set_avatar_url(&body.user_id, response.avatar_url.clone());
|
||||
|
||||
services
|
||||
.users
|
||||
.set_blurhash(&body.user_id, response.blurhash.clone())
|
||||
.await?;
|
||||
.set_blurhash(&body.user_id, response.blurhash.clone());
|
||||
|
||||
services
|
||||
.users
|
||||
.set_timezone(&body.user_id, response.tz.clone())
|
||||
.await?;
|
||||
.set_timezone(&body.user_id, response.tz.clone());
|
||||
|
||||
for (profile_key, profile_key_value) in &response.custom_profile_fields {
|
||||
services
|
||||
.users
|
||||
.set_profile_key(&body.user_id, profile_key, Some(profile_key_value.clone()))?;
|
||||
.set_profile_key(&body.user_id, profile_key, Some(profile_key_value.clone()));
|
||||
}
|
||||
|
||||
return Ok(get_profile::v3::Response {
|
||||
@@ -263,134 +267,108 @@ pub(crate) async fn get_profile_route(
|
||||
}
|
||||
}
|
||||
|
||||
if !services.users.exists(&body.user_id)? {
|
||||
if !services.users.exists(&body.user_id).await {
|
||||
// Return 404 if this user doesn't exist and we couldn't fetch it over
|
||||
// federation
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
|
||||
}
|
||||
|
||||
Ok(get_profile::v3::Response {
|
||||
avatar_url: services.users.avatar_url(&body.user_id)?,
|
||||
blurhash: services.users.blurhash(&body.user_id)?,
|
||||
displayname: services.users.displayname(&body.user_id)?,
|
||||
tz: services.users.timezone(&body.user_id)?,
|
||||
avatar_url: services.users.avatar_url(&body.user_id).await.ok(),
|
||||
blurhash: services.users.blurhash(&body.user_id).await.ok(),
|
||||
displayname: services.users.displayname(&body.user_id).await.ok(),
|
||||
tz: services.users.timezone(&body.user_id).await.ok(),
|
||||
custom_profile_fields: services
|
||||
.users
|
||||
.all_profile_keys(&body.user_id)
|
||||
.filter_map(Result::ok)
|
||||
.collect(),
|
||||
.collect()
|
||||
.await,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_displayname(
|
||||
services: &Services, user_id: &UserId, displayname: Option<String>, all_joined_rooms: Vec<OwnedRoomId>,
|
||||
services: &Services, user_id: &UserId, displayname: Option<String>, all_joined_rooms: &[OwnedRoomId],
|
||||
) -> Result<()> {
|
||||
let current_display_name = services.users.displayname(user_id).unwrap_or_default();
|
||||
let current_display_name = services.users.displayname(user_id).await.ok();
|
||||
|
||||
if displayname == current_display_name {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_displayname(user_id, displayname.clone())
|
||||
.await?;
|
||||
services.users.set_displayname(user_id, displayname.clone());
|
||||
|
||||
// Send a new join membership event into all joined rooms
|
||||
let all_joined_rooms: Vec<_> = all_joined_rooms
|
||||
.iter()
|
||||
.map(|room_id| {
|
||||
Ok::<_, Error>((
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
displayname: displayname.clone(),
|
||||
join_authorized_via_users_server: None,
|
||||
..serde_json::from_str(
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())?
|
||||
.ok_or_else(|| {
|
||||
Error::bad_database("Tried to send display name update for user not in the room.")
|
||||
})?
|
||||
.content
|
||||
.get(),
|
||||
)
|
||||
.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(user_id.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
room_id,
|
||||
))
|
||||
})
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
let mut joined_rooms = Vec::new();
|
||||
for room_id in all_joined_rooms {
|
||||
let Ok(content) = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(room_id, &StateEventType::RoomMember, user_id.as_str())
|
||||
.await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
update_all_rooms(services, all_joined_rooms, user_id).await;
|
||||
let pdu = PduBuilder::state(
|
||||
user_id.to_string(),
|
||||
&RoomMemberEventContent {
|
||||
displayname: displayname.clone(),
|
||||
join_authorized_via_users_server: None,
|
||||
..content
|
||||
},
|
||||
);
|
||||
|
||||
joined_rooms.push((pdu, room_id));
|
||||
}
|
||||
|
||||
update_all_rooms(services, joined_rooms, user_id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_avatar_url(
|
||||
services: &Services, user_id: &UserId, avatar_url: Option<OwnedMxcUri>, blurhash: Option<String>,
|
||||
all_joined_rooms: Vec<OwnedRoomId>,
|
||||
all_joined_rooms: &[OwnedRoomId],
|
||||
) -> Result<()> {
|
||||
let current_avatar_url = services.users.avatar_url(user_id).unwrap_or_default();
|
||||
let current_blurhash = services.users.blurhash(user_id).unwrap_or_default();
|
||||
let current_avatar_url = services.users.avatar_url(user_id).await.ok();
|
||||
let current_blurhash = services.users.blurhash(user_id).await.ok();
|
||||
|
||||
if current_avatar_url == avatar_url && current_blurhash == blurhash {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_avatar_url(user_id, avatar_url.clone())
|
||||
.await?;
|
||||
services
|
||||
.users
|
||||
.set_blurhash(user_id, blurhash.clone())
|
||||
.await?;
|
||||
services.users.set_avatar_url(user_id, avatar_url.clone());
|
||||
|
||||
services.users.set_blurhash(user_id, blurhash.clone());
|
||||
|
||||
// Send a new join membership event into all joined rooms
|
||||
let avatar_url = &avatar_url;
|
||||
let blurhash = &blurhash;
|
||||
let all_joined_rooms: Vec<_> = all_joined_rooms
|
||||
.iter()
|
||||
.map(|room_id| {
|
||||
Ok::<_, Error>((
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
avatar_url: avatar_url.clone(),
|
||||
blurhash: blurhash.clone(),
|
||||
join_authorized_via_users_server: None,
|
||||
..serde_json::from_str(
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())?
|
||||
.ok_or_else(|| {
|
||||
Error::bad_database("Tried to send avatar URL update for user not in the room.")
|
||||
})?
|
||||
.content
|
||||
.get(),
|
||||
)
|
||||
.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(user_id.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
.try_stream()
|
||||
.and_then(|room_id: &OwnedRoomId| async move {
|
||||
let content = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(room_id, &StateEventType::RoomMember, user_id.as_str())
|
||||
.await?;
|
||||
|
||||
let pdu = PduBuilder::state(
|
||||
user_id.to_string(),
|
||||
&RoomMemberEventContent {
|
||||
avatar_url: avatar_url.clone(),
|
||||
blurhash: blurhash.clone(),
|
||||
join_authorized_via_users_server: None,
|
||||
..content
|
||||
},
|
||||
room_id,
|
||||
))
|
||||
);
|
||||
|
||||
Ok((pdu, room_id))
|
||||
})
|
||||
.filter_map(Result::ok)
|
||||
.collect();
|
||||
.ignore_err()
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
update_all_rooms(services, all_joined_rooms, user_id).await;
|
||||
|
||||
|
||||
+228
-171
@@ -1,19 +1,19 @@
|
||||
use axum::extract::State;
|
||||
use conduit::err;
|
||||
use conduit::{err, Err};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
push::{
|
||||
delete_pushrule, get_pushers, get_pushrule, get_pushrule_actions, get_pushrule_enabled, get_pushrules_all,
|
||||
set_pusher, set_pushrule, set_pushrule_actions, set_pushrule_enabled, RuleScope,
|
||||
get_pushrules_global_scope, set_pusher, set_pushrule, set_pushrule_actions, set_pushrule_enabled,
|
||||
},
|
||||
},
|
||||
events::{
|
||||
push_rules::{PushRulesEvent, PushRulesEventContent},
|
||||
GlobalAccountDataEventType,
|
||||
},
|
||||
push::{InsertPushRuleError, RemovePushRuleError, Ruleset},
|
||||
CanonicalJsonObject,
|
||||
push::{InsertPushRuleError, PredefinedContentRuleId, PredefinedOverrideRuleId, RemovePushRuleError, Ruleset},
|
||||
CanonicalJsonObject, CanonicalJsonValue,
|
||||
};
|
||||
use service::Services;
|
||||
|
||||
@@ -27,48 +27,113 @@ pub(crate) async fn get_pushrules_all_route(
|
||||
) -> Result<get_pushrules_all::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let global_ruleset: Ruleset;
|
||||
|
||||
let Ok(event) =
|
||||
services
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())
|
||||
let Some(content_value) = services
|
||||
.account_data
|
||||
.get_global::<CanonicalJsonObject>(sender_user, GlobalAccountDataEventType::PushRules)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|event| event.get("content").cloned())
|
||||
.filter(CanonicalJsonValue::is_object)
|
||||
else {
|
||||
// push rules event doesn't exist, create it and return default
|
||||
return recreate_push_rules_and_return(&services, sender_user);
|
||||
};
|
||||
|
||||
if let Some(event) = event {
|
||||
let value = serde_json::from_str::<CanonicalJsonObject>(event.get())
|
||||
.map_err(|e| err!(Database(warn!("Invalid push rules account data event in database: {e}"))))?;
|
||||
|
||||
let Some(content_value) = value.get("content") else {
|
||||
// user somehow has a push rule event with no content key, recreate it and
|
||||
// return server default silently
|
||||
return recreate_push_rules_and_return(&services, sender_user);
|
||||
};
|
||||
|
||||
if content_value.to_string().is_empty() {
|
||||
// user somehow has a push rule event with empty content, recreate it and return
|
||||
// server default silently
|
||||
return recreate_push_rules_and_return(&services, sender_user);
|
||||
}
|
||||
|
||||
let account_data_content = serde_json::from_value::<PushRulesEventContent>(content_value.clone().into())
|
||||
.map_err(|e| err!(Database(warn!("Invalid push rules account data event in database: {e}"))))?;
|
||||
|
||||
global_ruleset = account_data_content.global;
|
||||
} else {
|
||||
// user somehow has non-existent push rule event. recreate it and return server
|
||||
// default silently
|
||||
return recreate_push_rules_and_return(&services, sender_user);
|
||||
}
|
||||
return recreate_push_rules_and_return(&services, sender_user).await;
|
||||
};
|
||||
|
||||
let account_data_content = serde_json::from_value::<PushRulesEventContent>(content_value.into())
|
||||
.map_err(|e| err!(Database(warn!("Invalid push rules account data event in database: {e}"))))?;
|
||||
|
||||
let mut global_ruleset = account_data_content.global;
|
||||
|
||||
// remove old deprecated mentions push rules as per MSC4210
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
use ruma::push::RuleKind::*;
|
||||
|
||||
global_ruleset
|
||||
.remove(Override, PredefinedOverrideRuleId::ContainsDisplayName)
|
||||
.ok();
|
||||
global_ruleset
|
||||
.remove(Override, PredefinedOverrideRuleId::RoomNotif)
|
||||
.ok();
|
||||
|
||||
global_ruleset
|
||||
.remove(Content, PredefinedContentRuleId::ContainsUserName)
|
||||
.ok();
|
||||
};
|
||||
|
||||
Ok(get_pushrules_all::v3::Response {
|
||||
global: global_ruleset,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/global/`
|
||||
///
|
||||
/// Retrieves the push rules event for this user.
|
||||
///
|
||||
/// This appears to be the exact same as `GET /_matrix/client/r0/pushrules/`.
|
||||
pub(crate) async fn get_pushrules_global_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_pushrules_global_scope::v3::Request>,
|
||||
) -> Result<get_pushrules_global_scope::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let Some(content_value) = services
|
||||
.account_data
|
||||
.get_global::<CanonicalJsonObject>(sender_user, GlobalAccountDataEventType::PushRules)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|event| event.get("content").cloned())
|
||||
.filter(CanonicalJsonValue::is_object)
|
||||
else {
|
||||
// user somehow has non-existent push rule event. recreate it and return server
|
||||
// default silently
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(PushRulesEvent {
|
||||
content: PushRulesEventContent {
|
||||
global: Ruleset::server_default(sender_user),
|
||||
},
|
||||
})
|
||||
.expect("to json always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(get_pushrules_global_scope::v3::Response {
|
||||
global: Ruleset::server_default(sender_user),
|
||||
});
|
||||
};
|
||||
|
||||
let account_data_content = serde_json::from_value::<PushRulesEventContent>(content_value.into())
|
||||
.map_err(|e| err!(Database(warn!("Invalid push rules account data event in database: {e}"))))?;
|
||||
|
||||
let mut global_ruleset = account_data_content.global;
|
||||
|
||||
// remove old deprecated mentions push rules as per MSC4210
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
use ruma::push::RuleKind::*;
|
||||
|
||||
global_ruleset
|
||||
.remove(Override, PredefinedOverrideRuleId::ContainsDisplayName)
|
||||
.ok();
|
||||
global_ruleset
|
||||
.remove(Override, PredefinedOverrideRuleId::RoomNotif)
|
||||
.ok();
|
||||
|
||||
global_ruleset
|
||||
.remove(Content, PredefinedContentRuleId::ContainsUserName)
|
||||
.ok();
|
||||
};
|
||||
|
||||
Ok(get_pushrules_global_scope::v3::Response {
|
||||
global: global_ruleset,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
///
|
||||
/// Retrieves a single specified push rule for this user.
|
||||
@@ -77,16 +142,23 @@ pub(crate) async fn get_pushrule_route(
|
||||
) -> Result<get_pushrule::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services
|
||||
// remove old deprecated mentions push rules as per MSC4210
|
||||
#[allow(deprecated)]
|
||||
if body.rule_id.as_str() == PredefinedContentRuleId::ContainsUserName.as_str()
|
||||
|| body.rule_id.as_str() == PredefinedOverrideRuleId::ContainsDisplayName.as_str()
|
||||
|| body.rule_id.as_str() == PredefinedOverrideRuleId::RoomNotif.as_str()
|
||||
{
|
||||
return Err!(Request(NotFound("Push rule not found.")));
|
||||
}
|
||||
|
||||
let event: PushRulesEvent = services
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
|
||||
|
||||
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
let rule = account_data
|
||||
let rule = event
|
||||
.content
|
||||
.global
|
||||
.get(body.kind.clone(), &body.rule_id)
|
||||
.map(Into::into);
|
||||
@@ -100,7 +172,7 @@ pub(crate) async fn get_pushrule_route(
|
||||
}
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
/// # `PUT /_matrix/client/r0/pushrules/global/{kind}/{ruleId}`
|
||||
///
|
||||
/// Creates a single specified push rule for this user.
|
||||
pub(crate) async fn set_pushrule_route(
|
||||
@@ -109,20 +181,11 @@ pub(crate) async fn set_pushrule_route(
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let body = body.body;
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services
|
||||
let mut account_data: PushRulesEvent = services
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
|
||||
|
||||
if let Err(error) =
|
||||
account_data
|
||||
@@ -155,17 +218,20 @@ pub(crate) async fn set_pushrule_route(
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
services.account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(set_pushrule::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
|
||||
/// # `GET /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/actions`
|
||||
///
|
||||
/// Gets the actions of a single specified push rule for this user.
|
||||
pub(crate) async fn get_pushrule_actions_route(
|
||||
@@ -173,34 +239,34 @@ pub(crate) async fn get_pushrule_actions_route(
|
||||
) -> Result<get_pushrule_actions::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
// remove old deprecated mentions push rules as per MSC4210
|
||||
#[allow(deprecated)]
|
||||
if body.rule_id.as_str() == PredefinedContentRuleId::ContainsUserName.as_str()
|
||||
|| body.rule_id.as_str() == PredefinedOverrideRuleId::ContainsDisplayName.as_str()
|
||||
|| body.rule_id.as_str() == PredefinedOverrideRuleId::RoomNotif.as_str()
|
||||
{
|
||||
return Err!(Request(NotFound("Push rule not found.")));
|
||||
}
|
||||
|
||||
let event = services
|
||||
let event: PushRulesEvent = services
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
|
||||
|
||||
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
let global = account_data.global;
|
||||
let actions = global
|
||||
let actions = event
|
||||
.content
|
||||
.global
|
||||
.get(body.kind.clone(), &body.rule_id)
|
||||
.map(|rule| rule.actions().to_owned())
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."))?;
|
||||
.ok_or(err!(Request(NotFound("Push rule not found."))))?;
|
||||
|
||||
Ok(get_pushrule_actions::v3::Response {
|
||||
actions,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
|
||||
/// # `PUT /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/actions`
|
||||
///
|
||||
/// Sets the actions of a single specified push rule for this user.
|
||||
pub(crate) async fn set_pushrule_actions_route(
|
||||
@@ -208,20 +274,11 @@ pub(crate) async fn set_pushrule_actions_route(
|
||||
) -> Result<set_pushrule_actions::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services
|
||||
let mut account_data: PushRulesEvent = services
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
|
||||
|
||||
if account_data
|
||||
.content
|
||||
@@ -232,17 +289,20 @@ pub(crate) async fn set_pushrule_actions_route(
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."));
|
||||
}
|
||||
|
||||
services.account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(set_pushrule_actions::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
|
||||
/// # `GET /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/enabled`
|
||||
///
|
||||
/// Gets the enabled status of a single specified push rule for this user.
|
||||
pub(crate) async fn get_pushrule_enabled_route(
|
||||
@@ -250,33 +310,36 @@ pub(crate) async fn get_pushrule_enabled_route(
|
||||
) -> Result<get_pushrule_enabled::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
// remove old deprecated mentions push rules as per MSC4210
|
||||
#[allow(deprecated)]
|
||||
if body.rule_id.as_str() == PredefinedContentRuleId::ContainsUserName.as_str()
|
||||
|| body.rule_id.as_str() == PredefinedOverrideRuleId::ContainsDisplayName.as_str()
|
||||
|| body.rule_id.as_str() == PredefinedOverrideRuleId::RoomNotif.as_str()
|
||||
{
|
||||
return Ok(get_pushrule_enabled::v3::Response {
|
||||
enabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
let event = services
|
||||
let event: PushRulesEvent = services
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
|
||||
|
||||
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
|
||||
let global = account_data.content.global;
|
||||
let enabled = global
|
||||
let enabled = event
|
||||
.content
|
||||
.global
|
||||
.get(body.kind.clone(), &body.rule_id)
|
||||
.map(ruma::push::AnyPushRuleRef::enabled)
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."))?;
|
||||
.ok_or(err!(Request(NotFound("Push rule not found."))))?;
|
||||
|
||||
Ok(get_pushrule_enabled::v3::Response {
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
|
||||
/// # `PUT /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/enabled`
|
||||
///
|
||||
/// Sets the enabled status of a single specified push rule for this user.
|
||||
pub(crate) async fn set_pushrule_enabled_route(
|
||||
@@ -284,20 +347,11 @@ pub(crate) async fn set_pushrule_enabled_route(
|
||||
) -> Result<set_pushrule_enabled::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services
|
||||
let mut account_data: PushRulesEvent = services
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
|
||||
|
||||
if account_data
|
||||
.content
|
||||
@@ -308,17 +362,20 @@ pub(crate) async fn set_pushrule_enabled_route(
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."));
|
||||
}
|
||||
|
||||
services.account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(set_pushrule_enabled::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
/// # `DELETE /_matrix/client/r0/pushrules/global/{kind}/{ruleId}`
|
||||
///
|
||||
/// Deletes a single specified push rule for this user.
|
||||
pub(crate) async fn delete_pushrule_route(
|
||||
@@ -326,20 +383,11 @@ pub(crate) async fn delete_pushrule_route(
|
||||
) -> Result<delete_pushrule::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services
|
||||
let mut account_data: PushRulesEvent = services
|
||||
.account_data
|
||||
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
|
||||
|
||||
if let Err(error) = account_data
|
||||
.content
|
||||
@@ -357,12 +405,15 @@ pub(crate) async fn delete_pushrule_route(
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
services.account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(delete_pushrule::v3::Response {})
|
||||
}
|
||||
@@ -376,7 +427,7 @@ pub(crate) async fn get_pushers_route(
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
Ok(get_pushers::v3::Response {
|
||||
pushers: services.pusher.get_pushers(sender_user)?,
|
||||
pushers: services.pusher.get_pushers(sender_user).await,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -390,27 +441,33 @@ pub(crate) async fn set_pushers_route(
|
||||
) -> Result<set_pusher::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services.pusher.set_pusher(sender_user, &body.action)?;
|
||||
services
|
||||
.pusher
|
||||
.set_pusher(sender_user, &body.action)
|
||||
.await?;
|
||||
|
||||
Ok(set_pusher::v3::Response::default())
|
||||
Ok(set_pusher::v3::Response::new())
|
||||
}
|
||||
|
||||
/// user somehow has bad push rules, these must always exist per spec.
|
||||
/// so recreate it and return server default silently
|
||||
fn recreate_push_rules_and_return(
|
||||
async fn recreate_push_rules_and_return(
|
||||
services: &Services, sender_user: &ruma::UserId,
|
||||
) -> Result<get_pushrules_all::v3::Response> {
|
||||
services.account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(PushRulesEvent {
|
||||
content: PushRulesEventContent {
|
||||
global: Ruleset::server_default(sender_user),
|
||||
},
|
||||
})
|
||||
.expect("to json always works"),
|
||||
)?;
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(PushRulesEvent {
|
||||
content: PushRulesEventContent {
|
||||
global: Ruleset::server_default(sender_user),
|
||||
},
|
||||
})
|
||||
.expect("to json always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(get_pushrules_all::v3::Response {
|
||||
global: Ruleset::server_default(sender_user),
|
||||
|
||||
@@ -31,27 +31,32 @@ pub(crate) async fn set_read_marker_route(
|
||||
event_id: fully_read.clone(),
|
||||
},
|
||||
};
|
||||
services.account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::FullyRead,
|
||||
&serde_json::to_value(fully_read_event).expect("to json value always works"),
|
||||
)?;
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::FullyRead,
|
||||
&serde_json::to_value(fully_read_event).expect("to json value always works"),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if body.private_read_receipt.is_some() || body.read_receipt.is_some() {
|
||||
services
|
||||
.rooms
|
||||
.user
|
||||
.reset_notification_counts(sender_user, &body.room_id)?;
|
||||
.reset_notification_counts(sender_user, &body.room_id);
|
||||
}
|
||||
|
||||
if let Some(event) = &body.private_read_receipt {
|
||||
let count = services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_count(event)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Event does not exist."))?;
|
||||
.get_pdu_count(event)
|
||||
.await
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Event does not exist."))?;
|
||||
|
||||
let count = match count {
|
||||
PduCount::Backfilled(_) => {
|
||||
return Err(Error::BadRequest(
|
||||
@@ -64,7 +69,7 @@ pub(crate) async fn set_read_marker_route(
|
||||
services
|
||||
.rooms
|
||||
.read_receipt
|
||||
.private_read_set(&body.room_id, sender_user, count)?;
|
||||
.private_read_set(&body.room_id, sender_user, count);
|
||||
}
|
||||
|
||||
if let Some(event) = &body.read_receipt {
|
||||
@@ -83,14 +88,18 @@ pub(crate) async fn set_read_marker_route(
|
||||
let mut receipt_content = BTreeMap::new();
|
||||
receipt_content.insert(event.to_owned(), receipts);
|
||||
|
||||
services.rooms.read_receipt.readreceipt_update(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&ruma::events::receipt::ReceiptEvent {
|
||||
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
|
||||
room_id: body.room_id.clone(),
|
||||
},
|
||||
)?;
|
||||
services
|
||||
.rooms
|
||||
.read_receipt
|
||||
.readreceipt_update(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&ruma::events::receipt::ReceiptEvent {
|
||||
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
|
||||
room_id: body.room_id.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(set_read_marker::v3::Response {})
|
||||
@@ -111,7 +120,7 @@ pub(crate) async fn create_receipt_route(
|
||||
services
|
||||
.rooms
|
||||
.user
|
||||
.reset_notification_counts(sender_user, &body.room_id)?;
|
||||
.reset_notification_counts(sender_user, &body.room_id);
|
||||
}
|
||||
|
||||
match body.receipt_type {
|
||||
@@ -121,12 +130,15 @@ pub(crate) async fn create_receipt_route(
|
||||
event_id: body.event_id.clone(),
|
||||
},
|
||||
};
|
||||
services.account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::FullyRead,
|
||||
&serde_json::to_value(fully_read_event).expect("to json value always works"),
|
||||
)?;
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::FullyRead,
|
||||
&serde_json::to_value(fully_read_event).expect("to json value always works"),
|
||||
)
|
||||
.await?;
|
||||
},
|
||||
create_receipt::v3::ReceiptType::Read => {
|
||||
let mut user_receipts = BTreeMap::new();
|
||||
@@ -143,21 +155,27 @@ pub(crate) async fn create_receipt_route(
|
||||
let mut receipt_content = BTreeMap::new();
|
||||
receipt_content.insert(body.event_id.clone(), receipts);
|
||||
|
||||
services.rooms.read_receipt.readreceipt_update(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&ruma::events::receipt::ReceiptEvent {
|
||||
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
|
||||
room_id: body.room_id.clone(),
|
||||
},
|
||||
)?;
|
||||
services
|
||||
.rooms
|
||||
.read_receipt
|
||||
.readreceipt_update(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&ruma::events::receipt::ReceiptEvent {
|
||||
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
|
||||
room_id: body.room_id.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
},
|
||||
create_receipt::v3::ReceiptType::ReadPrivate => {
|
||||
let count = services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_count(&body.event_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Event does not exist."))?;
|
||||
.get_pdu_count(&body.event_id)
|
||||
.await
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Event does not exist."))?;
|
||||
|
||||
let count = match count {
|
||||
PduCount::Backfilled(_) => {
|
||||
return Err(Error::BadRequest(
|
||||
@@ -170,7 +188,7 @@ pub(crate) async fn create_receipt_route(
|
||||
services
|
||||
.rooms
|
||||
.read_receipt
|
||||
.private_read_set(&body.room_id, sender_user, count)?;
|
||||
.private_read_set(&body.room_id, sender_user, count);
|
||||
},
|
||||
_ => return Err(Error::bad_database("Unsupported receipt type")),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
use axum::extract::State;
|
||||
use ruma::{
|
||||
api::client::redact::redact_event,
|
||||
events::{room::redaction::RoomRedactionEventContent, TimelineEventType},
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use ruma::{api::client::redact::redact_event, events::room::redaction::RoomRedactionEventContent};
|
||||
|
||||
use crate::{service::pdu::PduBuilder, Result, Ruma};
|
||||
|
||||
@@ -25,16 +21,11 @@ pub(crate) async fn redact_event_route(
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomRedaction,
|
||||
content: to_raw_value(&RoomRedactionEventContent {
|
||||
redacts: Some(body.event_id.clone().into()),
|
||||
..PduBuilder::timeline(&RoomRedactionEventContent {
|
||||
redacts: Some(body.event_id.clone()),
|
||||
reason: body.reason.clone(),
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: None,
|
||||
redacts: Some(body.event_id.into()),
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
@@ -44,8 +35,7 @@ pub(crate) async fn redact_event_route(
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
let event_id = (*event_id).to_owned();
|
||||
Ok(redact_event::v3::Response {
|
||||
event_id,
|
||||
event_id: event_id.into(),
|
||||
})
|
||||
}
|
||||
|
||||
+132
-36
@@ -1,30 +1,43 @@
|
||||
use axum::extract::State;
|
||||
use ruma::api::client::relations::{
|
||||
get_relating_events, get_relating_events_with_rel_type, get_relating_events_with_rel_type_and_event_type,
|
||||
use conduit::{
|
||||
at,
|
||||
utils::{result::FlatOk, stream::WidebandExt, IterStream, ReadyExt},
|
||||
PduCount, Result,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
api::{
|
||||
client::relations::{
|
||||
get_relating_events, get_relating_events_with_rel_type, get_relating_events_with_rel_type_and_event_type,
|
||||
},
|
||||
Direction,
|
||||
},
|
||||
events::{relation::RelationType, TimelineEventType},
|
||||
EventId, RoomId, UInt, UserId,
|
||||
};
|
||||
use service::{rooms::timeline::PdusIterItem, Services};
|
||||
|
||||
use crate::{Result, Ruma};
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
|
||||
pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_relating_events_with_rel_type_and_event_type::v1::Request>,
|
||||
) -> Result<get_relating_events_with_rel_type_and_event_type::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let res = services.rooms.pdu_metadata.paginate_relations_with_filter(
|
||||
sender_user,
|
||||
paginate_relations_with_filter(
|
||||
&services,
|
||||
body.sender_user(),
|
||||
&body.room_id,
|
||||
&body.event_id,
|
||||
&Some(body.event_type.clone()),
|
||||
&Some(body.rel_type.clone()),
|
||||
&body.from,
|
||||
&body.to,
|
||||
&body.limit,
|
||||
body.event_type.clone().into(),
|
||||
body.rel_type.clone().into(),
|
||||
body.from.as_deref(),
|
||||
body.to.as_deref(),
|
||||
body.limit,
|
||||
body.recurse,
|
||||
body.dir,
|
||||
)?;
|
||||
|
||||
Ok(get_relating_events_with_rel_type_and_event_type::v1::Response {
|
||||
)
|
||||
.await
|
||||
.map(|res| get_relating_events_with_rel_type_and_event_type::v1::Response {
|
||||
chunk: res.chunk,
|
||||
next_batch: res.next_batch,
|
||||
prev_batch: res.prev_batch,
|
||||
@@ -36,22 +49,21 @@ pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route(
|
||||
pub(crate) async fn get_relating_events_with_rel_type_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_relating_events_with_rel_type::v1::Request>,
|
||||
) -> Result<get_relating_events_with_rel_type::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let res = services.rooms.pdu_metadata.paginate_relations_with_filter(
|
||||
sender_user,
|
||||
paginate_relations_with_filter(
|
||||
&services,
|
||||
body.sender_user(),
|
||||
&body.room_id,
|
||||
&body.event_id,
|
||||
&None,
|
||||
&Some(body.rel_type.clone()),
|
||||
&body.from,
|
||||
&body.to,
|
||||
&body.limit,
|
||||
None,
|
||||
body.rel_type.clone().into(),
|
||||
body.from.as_deref(),
|
||||
body.to.as_deref(),
|
||||
body.limit,
|
||||
body.recurse,
|
||||
body.dir,
|
||||
)?;
|
||||
|
||||
Ok(get_relating_events_with_rel_type::v1::Response {
|
||||
)
|
||||
.await
|
||||
.map(|res| get_relating_events_with_rel_type::v1::Response {
|
||||
chunk: res.chunk,
|
||||
next_batch: res.next_batch,
|
||||
prev_batch: res.prev_batch,
|
||||
@@ -63,18 +75,102 @@ pub(crate) async fn get_relating_events_with_rel_type_route(
|
||||
pub(crate) async fn get_relating_events_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_relating_events::v1::Request>,
|
||||
) -> Result<get_relating_events::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services.rooms.pdu_metadata.paginate_relations_with_filter(
|
||||
sender_user,
|
||||
paginate_relations_with_filter(
|
||||
&services,
|
||||
body.sender_user(),
|
||||
&body.room_id,
|
||||
&body.event_id,
|
||||
&None,
|
||||
&None,
|
||||
&body.from,
|
||||
&body.to,
|
||||
&body.limit,
|
||||
None,
|
||||
None,
|
||||
body.from.as_deref(),
|
||||
body.to.as_deref(),
|
||||
body.limit,
|
||||
body.recurse,
|
||||
body.dir,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn paginate_relations_with_filter(
|
||||
services: &Services, sender_user: &UserId, room_id: &RoomId, target: &EventId,
|
||||
filter_event_type: Option<TimelineEventType>, filter_rel_type: Option<RelationType>, from: Option<&str>,
|
||||
to: Option<&str>, limit: Option<UInt>, recurse: bool, dir: Direction,
|
||||
) -> Result<get_relating_events::v1::Response> {
|
||||
let start: PduCount = from
|
||||
.map(str::parse)
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| match dir {
|
||||
Direction::Forward => PduCount::min(),
|
||||
Direction::Backward => PduCount::max(),
|
||||
});
|
||||
|
||||
let to: Option<PduCount> = to.map(str::parse).flat_ok();
|
||||
|
||||
// Use limit or else 30, with maximum 100
|
||||
let limit: usize = limit
|
||||
.map(TryInto::try_into)
|
||||
.flat_ok()
|
||||
.unwrap_or(30)
|
||||
.min(100);
|
||||
|
||||
// Spec (v1.10) recommends depth of at least 3
|
||||
let depth: u8 = if recurse {
|
||||
3
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
let events: Vec<PdusIterItem> = services
|
||||
.rooms
|
||||
.pdu_metadata
|
||||
.get_relations(sender_user, room_id, target, start, limit, depth, dir)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|(_, pdu)| {
|
||||
filter_event_type
|
||||
.as_ref()
|
||||
.is_none_or(|kind| *kind == pdu.kind)
|
||||
})
|
||||
.filter(|(_, pdu)| {
|
||||
filter_rel_type
|
||||
.as_ref()
|
||||
.is_none_or(|rel_type| pdu.relation_type_equal(rel_type))
|
||||
})
|
||||
.stream()
|
||||
.ready_take_while(|(count, _)| Some(*count) != to)
|
||||
.wide_filter_map(|item| visibility_filter(services, sender_user, item))
|
||||
.take(limit)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let next_batch = match dir {
|
||||
Direction::Forward => events.last(),
|
||||
Direction::Backward => events.first(),
|
||||
}
|
||||
.map(at!(0))
|
||||
.as_ref()
|
||||
.map(ToString::to_string);
|
||||
|
||||
Ok(get_relating_events::v1::Response {
|
||||
next_batch,
|
||||
prev_batch: from.map(Into::into),
|
||||
recursion_depth: recurse.then_some(depth.into()),
|
||||
chunk: events
|
||||
.into_iter()
|
||||
.map(at!(1))
|
||||
.map(|pdu| pdu.to_message_like_event())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn visibility_filter(services: &Services, sender_user: &UserId, item: PdusIterItem) -> Option<PdusIterItem> {
|
||||
let (_, pdu) = &item;
|
||||
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id)
|
||||
.await
|
||||
.then_some(item)
|
||||
}
|
||||
|
||||
+101
-58
@@ -1,87 +1,130 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduit::{info, utils::ReadyExt, Err};
|
||||
use rand::Rng;
|
||||
use ruma::{
|
||||
api::client::{error::ErrorKind, room::report_content},
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
room::{report_content, report_room},
|
||||
},
|
||||
events::room::message,
|
||||
int, EventId, RoomId, UserId,
|
||||
};
|
||||
use tokio::time::sleep;
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
debug_info,
|
||||
service::{pdu::PduEvent, Services},
|
||||
utils::HtmlEscape,
|
||||
Error, Result, Ruma,
|
||||
};
|
||||
|
||||
/// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}`
|
||||
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
|
||||
///
|
||||
/// Reports an inappropriate event to homeserver admins
|
||||
pub(crate) async fn report_event_route(
|
||||
State(services): State<crate::State>, body: Ruma<report_content::v3::Request>,
|
||||
) -> Result<report_content::v3::Response> {
|
||||
/// Reports an abusive room to homeserver admins
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "report_room")]
|
||||
pub(crate) async fn report_room_route(
|
||||
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<report_room::v3::Request>,
|
||||
) -> Result<report_room::v3::Response> {
|
||||
// user authentication
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
info!(
|
||||
"Received /report request by user {sender_user} for room {} and event ID {}",
|
||||
body.room_id, body.event_id
|
||||
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
|
||||
body.room_id,
|
||||
body.reason.as_deref().unwrap_or("")
|
||||
);
|
||||
|
||||
delay_response().await;
|
||||
|
||||
// check if we know about the reported event ID or if it's invalid
|
||||
let Some(pdu) = services.rooms.timeline.get_pdu(&body.event_id)? else {
|
||||
if body.reason.as_ref().is_some_and(|s| s.len() > 750) {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Event ID is not known to us or Event ID is invalid",
|
||||
ErrorKind::InvalidParam,
|
||||
"Reason too long, should be 750 characters or fewer",
|
||||
));
|
||||
};
|
||||
|
||||
is_report_valid(
|
||||
&services,
|
||||
&pdu.event_id,
|
||||
&body.room_id,
|
||||
sender_user,
|
||||
&body.reason,
|
||||
body.score,
|
||||
&pdu,
|
||||
)?;
|
||||
delay_response().await;
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(&services.globals.config.server_name, &body.room_id)
|
||||
.await
|
||||
{
|
||||
return Err!(Request(NotFound(
|
||||
"Room does not exist to us, no local users have joined at all"
|
||||
)));
|
||||
}
|
||||
|
||||
// send admin room message that we received the report with an @room ping for
|
||||
// urgency
|
||||
services
|
||||
.admin
|
||||
.send_message(message::RoomMessageEventContent::text_html(
|
||||
format!(
|
||||
"@room Report received from: {}\n\nEvent ID: {}\nRoom ID: {}\nSent By: {}\n\nReport Score: {}\nReport \
|
||||
Reason: {}",
|
||||
sender_user.to_owned(),
|
||||
pdu.event_id,
|
||||
pdu.room_id,
|
||||
pdu.sender.clone(),
|
||||
body.score.unwrap_or_else(|| ruma::Int::from(0)),
|
||||
body.reason.as_deref().unwrap_or("")
|
||||
),
|
||||
format!(
|
||||
"<details><summary>@room Report received from: <a href=\"https://matrix.to/#/{0}\">{0}\
|
||||
</a></summary><ul><li>Event Info<ul><li>Event ID: <code>{1}</code>\
|
||||
<a href=\"https://matrix.to/#/{2}/{1}\">🔗</a></li><li>Room ID: <code>{2}</code>\
|
||||
</li><li>Sent By: <a href=\"https://matrix.to/#/{3}\">{3}</a></li></ul></li><li>\
|
||||
Report Info<ul><li>Report Score: {4}</li><li>Report Reason: {5}</li></ul></li>\
|
||||
</ul></details>",
|
||||
sender_user.to_owned(),
|
||||
pdu.event_id.clone(),
|
||||
pdu.room_id.clone(),
|
||||
pdu.sender.clone(),
|
||||
body.score.unwrap_or_else(|| ruma::Int::from(0)),
|
||||
HtmlEscape(body.reason.as_deref().unwrap_or(""))
|
||||
),
|
||||
))
|
||||
.await;
|
||||
.send_message(message::RoomMessageEventContent::text_markdown(format!(
|
||||
"@room Room report received from {} -\n\nRoom ID: {}\n\nReport Reason: {}",
|
||||
sender_user.to_owned(),
|
||||
body.room_id,
|
||||
body.reason.as_deref().unwrap_or("")
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
Ok(report_room::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}`
|
||||
///
|
||||
/// Reports an inappropriate event to homeserver admins
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "report_event")]
|
||||
pub(crate) async fn report_event_route(
|
||||
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<report_content::v3::Request>,
|
||||
) -> Result<report_content::v3::Response> {
|
||||
// user authentication
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
info!(
|
||||
"Received event report by user {sender_user} for room {} and event ID {}, with reason: \"{}\"",
|
||||
body.room_id,
|
||||
body.event_id,
|
||||
body.reason.as_deref().unwrap_or("")
|
||||
);
|
||||
|
||||
delay_response().await;
|
||||
|
||||
// check if we know about the reported event ID or if it's invalid
|
||||
let Ok(pdu) = services.rooms.timeline.get_pdu(&body.event_id).await else {
|
||||
return Err!(Request(NotFound("Event ID is not known to us or Event ID is invalid")));
|
||||
};
|
||||
|
||||
is_event_report_valid(
|
||||
&services,
|
||||
&pdu.event_id,
|
||||
&body.room_id,
|
||||
sender_user,
|
||||
body.reason.as_ref(),
|
||||
body.score,
|
||||
&pdu,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// send admin room message that we received the report with an @room ping for
|
||||
// urgency
|
||||
services
|
||||
.admin
|
||||
.send_message(message::RoomMessageEventContent::text_markdown(format!(
|
||||
"@room Event report received from {} -\n\nEvent ID: {}\nRoom ID: {}\nSent By: {}\n\nReport Score: \
|
||||
{}\nReport Reason: {}",
|
||||
sender_user.to_owned(),
|
||||
pdu.event_id,
|
||||
pdu.room_id,
|
||||
pdu.sender,
|
||||
body.score.unwrap_or_else(|| ruma::Int::from(0)),
|
||||
body.reason.as_deref().unwrap_or("")
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
Ok(report_content::v3::Response {})
|
||||
}
|
||||
@@ -92,9 +135,9 @@ pub(crate) async fn report_event_route(
|
||||
/// check if score is in valid range
|
||||
/// check if report reasoning is less than or equal to 750 characters
|
||||
/// check if reporting user is in the reporting room
|
||||
fn is_report_valid(
|
||||
services: &Services, event_id: &EventId, room_id: &RoomId, sender_user: &UserId, reason: &Option<String>,
|
||||
score: Option<ruma::Int>, pdu: &std::sync::Arc<PduEvent>,
|
||||
async fn is_event_report_valid(
|
||||
services: &Services, event_id: &EventId, room_id: &RoomId, sender_user: &UserId, reason: Option<&String>,
|
||||
score: Option<ruma::Int>, pdu: &PduEvent,
|
||||
) -> Result<()> {
|
||||
debug_info!("Checking if report from user {sender_user} for event {event_id} in room {room_id} is valid");
|
||||
|
||||
@@ -123,8 +166,8 @@ fn is_report_valid(
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(room_id)
|
||||
.filter_map(Result::ok)
|
||||
.any(|user_id| user_id == *sender_user)
|
||||
.ready_any(|user_id| user_id == sender_user)
|
||||
.await
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
@@ -139,7 +182,7 @@ fn is_report_valid(
|
||||
/// random delay sending a response per spec suggestion regarding
|
||||
/// enumerating for potential events existing in our server.
|
||||
async fn delay_response() {
|
||||
let time_to_wait = rand::thread_rng().gen_range(3..10);
|
||||
let time_to_wait = rand::thread_rng().gen_range(2..5);
|
||||
debug_info!("Got successful /report request, waiting {time_to_wait} seconds before sending successful response.");
|
||||
sleep(Duration::from_secs(time_to_wait)).await;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
use axum::extract::State;
|
||||
use conduit::{Error, Result};
|
||||
use futures::StreamExt;
|
||||
use ruma::api::client::{error::ErrorKind, room::aliases};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/aliases`
|
||||
///
|
||||
/// Lists all aliases of the room.
|
||||
///
|
||||
/// - Only users joined to the room are allowed to call this, or if
|
||||
/// `history_visibility` is world readable in the room
|
||||
pub(crate) async fn get_room_aliases_route(
|
||||
State(services): State<crate::State>, body: Ruma<aliases::v3::Request>,
|
||||
) -> Result<aliases::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, &body.room_id)
|
||||
.await
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(aliases::v3::Response {
|
||||
aliases: services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await,
|
||||
})
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::{cmp::max, collections::BTreeMap};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduit::{debug_info, debug_warn, err, Err};
|
||||
use conduit::{debug_info, debug_warn, error, info, pdu::PduBuilder, warn, Err, Error, Result};
|
||||
use futures::FutureExt;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
room::{self, aliases, create_room, get_room_event, upgrade_room},
|
||||
room::{self, create_room},
|
||||
},
|
||||
events::{
|
||||
room::{
|
||||
@@ -17,36 +18,18 @@ use ruma::{
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
name::RoomNameEventContent,
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
tombstone::RoomTombstoneEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
StateEventType, TimelineEventType,
|
||||
TimelineEventType,
|
||||
},
|
||||
int,
|
||||
serde::{JsonObject, Raw},
|
||||
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId,
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
use tracing::{error, info, warn};
|
||||
use service::{appservice::RegistrationInfo, Services};
|
||||
|
||||
use super::invite_helper;
|
||||
use crate::{
|
||||
service::{appservice::RegistrationInfo, pdu::PduBuilder, Services},
|
||||
Error, Result, Ruma,
|
||||
};
|
||||
|
||||
/// Recommended transferable state events list from the spec
|
||||
const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 9] = &[
|
||||
StateEventType::RoomServerAcl,
|
||||
StateEventType::RoomEncryption,
|
||||
StateEventType::RoomName,
|
||||
StateEventType::RoomAvatar,
|
||||
StateEventType::RoomTopic,
|
||||
StateEventType::RoomGuestAccess,
|
||||
StateEventType::RoomHistoryVisibility,
|
||||
StateEventType::RoomJoinRules,
|
||||
StateEventType::RoomPowerLevels,
|
||||
];
|
||||
use crate::{client::invite_helper, Ruma};
|
||||
|
||||
/// # `POST /_matrix/client/v3/createRoom`
|
||||
///
|
||||
@@ -74,7 +57,7 @@ pub(crate) async fn create_room_route(
|
||||
|
||||
if !services.globals.allow_room_creation()
|
||||
&& body.appservice_info.is_none()
|
||||
&& !services.users.is_admin(sender_user)?
|
||||
&& !services.users.is_admin(sender_user).await
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Room creation has been disabled."));
|
||||
}
|
||||
@@ -86,7 +69,7 @@ pub(crate) async fn create_room_route(
|
||||
};
|
||||
|
||||
// check if room ID doesn't already exist instead of erroring on auth check
|
||||
if services.rooms.short.get_shortroomid(&room_id)?.is_some() {
|
||||
if services.rooms.short.get_shortroomid(&room_id).await.is_ok() {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::RoomInUse,
|
||||
"Room with that custom room ID already exists",
|
||||
@@ -95,7 +78,7 @@ pub(crate) async fn create_room_route(
|
||||
|
||||
if body.visibility == room::Visibility::Public
|
||||
&& services.globals.config.lockdown_public_room_directory
|
||||
&& !services.users.is_admin(sender_user)?
|
||||
&& !services.users.is_admin(sender_user).await
|
||||
&& body.appservice_info.is_none()
|
||||
{
|
||||
info!(
|
||||
@@ -118,11 +101,15 @@ pub(crate) async fn create_room_route(
|
||||
return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")));
|
||||
}
|
||||
|
||||
let _short_id = services.rooms.short.get_or_create_shortroomid(&room_id)?;
|
||||
let _short_id = services
|
||||
.rooms
|
||||
.short
|
||||
.get_or_create_shortroomid(&room_id)
|
||||
.await;
|
||||
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
|
||||
|
||||
let alias: Option<OwnedRoomAliasId> = if let Some(alias) = &body.room_alias_name {
|
||||
Some(room_alias_check(&services, alias, &body.appservice_info).await?)
|
||||
let alias: Option<OwnedRoomAliasId> = if let Some(alias) = body.room_alias_name.as_ref() {
|
||||
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -145,8 +132,7 @@ pub(crate) async fn create_room_route(
|
||||
None => services.globals.default_room_version(),
|
||||
};
|
||||
|
||||
#[allow(clippy::single_match_else)]
|
||||
let content = match &body.creation_content {
|
||||
let create_content = match &body.creation_content {
|
||||
Some(content) => {
|
||||
use RoomVersionId::*;
|
||||
|
||||
@@ -208,16 +194,15 @@ pub(crate) async fn create_room_route(
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&content).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
content: to_raw_value(&create_content).expect("create event content serialization"),
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
..Default::default()
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
// 2. Let the room creator join
|
||||
@@ -225,28 +210,21 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
membership: MembershipState::Join,
|
||||
displayname: services.users.displayname(sender_user)?,
|
||||
avatar_url: services.users.avatar_url(sender_user)?,
|
||||
PduBuilder::state(
|
||||
sender_user.to_string(),
|
||||
&RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
is_direct: Some(body.is_direct),
|
||||
third_party_invite: None,
|
||||
blurhash: services.users.blurhash(sender_user)?,
|
||||
reason: None,
|
||||
join_authorized_via_users_server: None,
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
..RoomMemberEventContent::new(MembershipState::Join)
|
||||
},
|
||||
),
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
// 3. Power levels
|
||||
@@ -260,13 +238,21 @@ pub(crate) async fn create_room_route(
|
||||
let mut users = BTreeMap::from_iter([(sender_user.clone(), int!(100))]);
|
||||
|
||||
if preset == RoomPreset::TrustedPrivateChat {
|
||||
for invite_ in &body.invite {
|
||||
users.insert(invite_.clone(), int!(100));
|
||||
for invite in &body.invite {
|
||||
if services.users.user_is_ignored(sender_user, invite).await {
|
||||
return Err!(Request(Forbidden("You cannot invite users you have ignored to rooms.")));
|
||||
} else if services.users.user_is_ignored(invite, sender_user).await {
|
||||
// silently drop the invite to the recipient if they've been ignored by the
|
||||
// sender, pretend it worked
|
||||
continue;
|
||||
}
|
||||
|
||||
users.insert(invite.clone(), int!(100));
|
||||
}
|
||||
}
|
||||
|
||||
let power_levels_content =
|
||||
default_power_levels_content(&body.power_level_content_override, &body.visibility, users)?;
|
||||
default_power_levels_content(body.power_level_content_override.as_ref(), &body.visibility, users)?;
|
||||
|
||||
services
|
||||
.rooms
|
||||
@@ -274,16 +260,15 @@ pub(crate) async fn create_room_route(
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomPowerLevels,
|
||||
content: to_raw_value(&power_levels_content).expect("to_raw_value always works on serde_json::Value"),
|
||||
unsigned: None,
|
||||
content: to_raw_value(&power_levels_content).expect("serialized power_levels event content"),
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
..Default::default()
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
// 4. Canonical room alias
|
||||
@@ -292,22 +277,18 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCanonicalAlias,
|
||||
content: to_raw_value(&RoomCanonicalAliasEventContent {
|
||||
PduBuilder::state(
|
||||
String::new(),
|
||||
&RoomCanonicalAliasEventContent {
|
||||
alias: Some(room_alias_id.to_owned()),
|
||||
alt_aliases: vec![],
|
||||
})
|
||||
.expect("We checked that alias earlier, it must be fine"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
},
|
||||
),
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -318,23 +299,19 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomJoinRules,
|
||||
content: to_raw_value(&RoomJoinRulesEventContent::new(match preset {
|
||||
PduBuilder::state(
|
||||
String::new(),
|
||||
&RoomJoinRulesEventContent::new(match preset {
|
||||
RoomPreset::PublicChat => JoinRule::Public,
|
||||
// according to spec "invite" is the default
|
||||
_ => JoinRule::Invite,
|
||||
}))
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
}),
|
||||
),
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
// 5.2 History Visibility
|
||||
@@ -342,19 +319,15 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomHistoryVisibility,
|
||||
content: to_raw_value(&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared))
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
PduBuilder::state(
|
||||
String::new(),
|
||||
&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared),
|
||||
),
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
// 5.3 Guest Access
|
||||
@@ -362,22 +335,18 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomGuestAccess,
|
||||
content: to_raw_value(&RoomGuestAccessEventContent::new(match preset {
|
||||
PduBuilder::state(
|
||||
String::new(),
|
||||
&RoomGuestAccessEventContent::new(match preset {
|
||||
RoomPreset::PublicChat => GuestAccess::Forbidden,
|
||||
_ => GuestAccess::CanJoin,
|
||||
}))
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
}),
|
||||
),
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
// 6. Events listed in initial_state
|
||||
@@ -410,6 +379,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
|
||||
.boxed()
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -419,19 +389,12 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomName,
|
||||
content: to_raw_value(&RoomNameEventContent::new(name.clone()))
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
PduBuilder::state(String::new(), &RoomNameEventContent::new(name.clone())),
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -440,28 +403,35 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomTopic,
|
||||
content: to_raw_value(&RoomTopicEventContent {
|
||||
PduBuilder::state(
|
||||
String::new(),
|
||||
&RoomTopicEventContent {
|
||||
topic: topic.clone(),
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
},
|
||||
),
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 8. Events implied by invite (and TODO: invite_3pid)
|
||||
drop(state_lock);
|
||||
for user_id in &body.invite {
|
||||
if let Err(e) = invite_helper(&services, sender_user, user_id, &room_id, None, body.is_direct).await {
|
||||
if services.users.user_is_ignored(sender_user, user_id).await {
|
||||
return Err!(Request(Forbidden("You cannot invite users you have ignored to rooms.")));
|
||||
} else if services.users.user_is_ignored(user_id, sender_user).await {
|
||||
// silently drop the invite to the recipient if they've been ignored by the
|
||||
// sender, pretend it worked
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = invite_helper(&services, sender_user, user_id, &room_id, None, body.is_direct)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
warn!(%e, "Failed to send invite");
|
||||
}
|
||||
}
|
||||
@@ -475,7 +445,7 @@ pub(crate) async fn create_room_route(
|
||||
}
|
||||
|
||||
if body.visibility == room::Visibility::Public {
|
||||
services.rooms.directory.set_public(&room_id)?;
|
||||
services.rooms.directory.set_public(&room_id);
|
||||
|
||||
if services.globals.config.admin_room_notices {
|
||||
services
|
||||
@@ -491,353 +461,9 @@ pub(crate) async fn create_room_route(
|
||||
Ok(create_room::v3::Response::new(room_id))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}`
|
||||
///
|
||||
/// Gets a single event.
|
||||
///
|
||||
/// - You have to currently be joined to the room (TODO: Respect history
|
||||
/// visibility)
|
||||
pub(crate) async fn get_room_event_route(
|
||||
State(services): State<crate::State>, body: Ruma<get_room_event::v3::Request>,
|
||||
) -> Result<get_room_event::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu(&body.event_id)?
|
||||
.ok_or_else(|| err!(Request(NotFound("Event {} not found.", &body.event_id))))?;
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &event.room_id, &body.event_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this event.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut event = (*event).clone();
|
||||
event.add_age()?;
|
||||
|
||||
Ok(get_room_event::v3::Response {
|
||||
event: event.to_room_event(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/aliases`
|
||||
///
|
||||
/// Lists all aliases of the room.
|
||||
///
|
||||
/// - Only users joined to the room are allowed to call this, or if
|
||||
/// `history_visibility` is world readable in the room
|
||||
pub(crate) async fn get_room_aliases_route(
|
||||
State(services): State<crate::State>, body: Ruma<aliases::v3::Request>,
|
||||
) -> Result<aliases::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, &body.room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(aliases::v3::Response {
|
||||
aliases: services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.filter_map(Result::ok)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
|
||||
///
|
||||
/// Upgrades the room.
|
||||
///
|
||||
/// - Creates a replacement room
|
||||
/// - Sends a tombstone event into the current room
|
||||
/// - Sender user joins the room
|
||||
/// - Transfers some state events
|
||||
/// - Moves local aliases
|
||||
/// - Modifies old room power levels to prevent users from speaking
|
||||
pub(crate) async fn upgrade_room_route(
|
||||
State(services): State<crate::State>, body: Ruma<upgrade_room::v3::Request>,
|
||||
) -> Result<upgrade_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services
|
||||
.globals
|
||||
.supported_room_versions()
|
||||
.contains(&body.new_version)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UnsupportedRoomVersion,
|
||||
"This server does not support that room version.",
|
||||
));
|
||||
}
|
||||
|
||||
// Create a replacement room
|
||||
let replacement_room = RoomId::new(services.globals.server_name());
|
||||
|
||||
let _short_id = services
|
||||
.rooms
|
||||
.short
|
||||
.get_or_create_shortroomid(&replacement_room)?;
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
|
||||
// Send a m.room.tombstone event to the old room to indicate that it is not
|
||||
// intended to be used any further Fail if the sender does not have the required
|
||||
// permissions
|
||||
let tombstone_event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomTombstone,
|
||||
content: to_raw_value(&RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: replacement_room.clone(),
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change lock to replacement room
|
||||
drop(state_lock);
|
||||
let state_lock = services.rooms.state.mutex.lock(&replacement_room).await;
|
||||
|
||||
// Get the old room creation event
|
||||
let mut create_event_content = serde_json::from_str::<CanonicalJsonObject>(
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &StateEventType::RoomCreate, "")?
|
||||
.ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
|
||||
.content
|
||||
.get(),
|
||||
)
|
||||
.map_err(|_| Error::bad_database("Invalid room event in database."))?;
|
||||
|
||||
// Use the m.room.tombstone event as the predecessor
|
||||
let predecessor = Some(ruma::events::room::create::PreviousRoom::new(
|
||||
body.room_id.clone(),
|
||||
(*tombstone_event_id).to_owned(),
|
||||
));
|
||||
|
||||
// Send a m.room.create event containing a predecessor field and the applicable
|
||||
// room_version
|
||||
{
|
||||
use RoomVersionId::*;
|
||||
match body.new_version {
|
||||
V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
|
||||
create_event_content.insert(
|
||||
"creator".into(),
|
||||
json!(&sender_user).try_into().map_err(|e| {
|
||||
info!("Error forming creation event: {e}");
|
||||
Error::BadRequest(ErrorKind::BadJson, "Error forming creation event")
|
||||
})?,
|
||||
);
|
||||
},
|
||||
_ => {
|
||||
// "creator" key no longer exists in V11+ rooms
|
||||
create_event_content.remove("creator");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
create_event_content.insert(
|
||||
"room_version".into(),
|
||||
json!(&body.new_version)
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
|
||||
);
|
||||
create_event_content.insert(
|
||||
"predecessor".into(),
|
||||
json!(predecessor)
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
|
||||
);
|
||||
|
||||
// Validate creation event content
|
||||
if serde_json::from_str::<CanonicalJsonObject>(
|
||||
to_raw_value(&create_event_content)
|
||||
.expect("Error forming creation event")
|
||||
.get(),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"));
|
||||
}
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&create_event_content).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Join the new room
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
membership: MembershipState::Join,
|
||||
displayname: services.users.displayname(sender_user)?,
|
||||
avatar_url: services.users.avatar_url(sender_user)?,
|
||||
is_direct: None,
|
||||
third_party_invite: None,
|
||||
blurhash: services.users.blurhash(sender_user)?,
|
||||
reason: None,
|
||||
join_authorized_via_users_server: None,
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Replicate transferable state events to the new room
|
||||
for event_type in TRANSFERABLE_STATE_EVENTS {
|
||||
let event_content = match services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, event_type, "")?
|
||||
{
|
||||
Some(v) => v.content.clone(),
|
||||
None => continue, // Skipping missing events.
|
||||
};
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: event_type.to_string().into(),
|
||||
content: event_content,
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Moves any local aliases to the new room
|
||||
for alias in services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
services
|
||||
.rooms
|
||||
.alias
|
||||
.remove_alias(&alias, sender_user)
|
||||
.await?;
|
||||
services
|
||||
.rooms
|
||||
.alias
|
||||
.set_alias(&alias, &replacement_room, sender_user)?;
|
||||
}
|
||||
|
||||
// Get the old room power levels
|
||||
let mut power_levels_event_content: RoomPowerLevelsEventContent = serde_json::from_str(
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &StateEventType::RoomPowerLevels, "")?
|
||||
.ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
|
||||
.content
|
||||
.get(),
|
||||
)
|
||||
.map_err(|_| Error::bad_database("Invalid room event in database."))?;
|
||||
|
||||
// Setting events_default and invite to the greater of 50 and users_default + 1
|
||||
let new_level = max(
|
||||
int!(50),
|
||||
power_levels_event_content
|
||||
.users_default
|
||||
.checked_add(int!(1))
|
||||
.ok_or_else(|| {
|
||||
Error::BadRequest(ErrorKind::BadJson, "users_default power levels event content is not valid")
|
||||
})?,
|
||||
);
|
||||
power_levels_event_content.events_default = new_level;
|
||||
power_levels_event_content.invite = new_level;
|
||||
|
||||
// Modify the power levels in the old room to prevent sending of events and
|
||||
// inviting new users
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomPowerLevels,
|
||||
content: to_raw_value(&power_levels_event_content).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
// Return the replacement room id
|
||||
Ok(upgrade_room::v3::Response {
|
||||
replacement_room,
|
||||
})
|
||||
}
|
||||
|
||||
/// creates the power_levels_content for the PDU builder
|
||||
fn default_power_levels_content(
|
||||
power_level_content_override: &Option<Raw<RoomPowerLevelsEventContent>>, visibility: &room::Visibility,
|
||||
power_level_content_override: Option<&Raw<RoomPowerLevelsEventContent>>, visibility: &room::Visibility,
|
||||
users: BTreeMap<OwnedUserId, Int>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut power_levels_content = serde_json::to_value(RoomPowerLevelsEventContent {
|
||||
@@ -887,7 +513,7 @@ fn default_power_levels_content(
|
||||
|
||||
/// if a room is being created with a room alias, run our checks
|
||||
async fn room_alias_check(
|
||||
services: &Services, room_alias_name: &str, appservice_info: &Option<RegistrationInfo>,
|
||||
services: &Services, room_alias_name: &str, appservice_info: Option<&RegistrationInfo>,
|
||||
) -> Result<OwnedRoomAliasId> {
|
||||
// Basic checks on the room alias validity
|
||||
if room_alias_name.contains(':') {
|
||||
@@ -921,13 +547,14 @@ async fn room_alias_check(
|
||||
if services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&full_room_alias)?
|
||||
.is_some()
|
||||
.resolve_local_alias(&full_room_alias)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::RoomInUse, "Room alias already exists."));
|
||||
}
|
||||
|
||||
if let Some(ref info) = appservice_info {
|
||||
if let Some(info) = appservice_info {
|
||||
if !info.aliases.is_match(full_room_alias.as_str()) {
|
||||
return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias is not in namespace."));
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use axum::extract::State;
|
||||
use conduit::{err, Err, Event, Result};
|
||||
use futures::{try_join, FutureExt, TryFutureExt};
|
||||
use ruma::api::client::room::get_room_event;
|
||||
|
||||
use crate::{client::ignored_filter, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}`
|
||||
///
|
||||
/// Gets a single event.
|
||||
pub(crate) async fn get_room_event_route(
|
||||
State(services): State<crate::State>, ref body: Ruma<get_room_event::v3::Request>,
|
||||
) -> Result<get_room_event::v3::Response> {
|
||||
let event = services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu(&body.event_id)
|
||||
.map_err(|_| err!(Request(NotFound("Event {} not found.", &body.event_id))));
|
||||
|
||||
let token = services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_count(&body.event_id)
|
||||
.map_err(|_| err!(Request(NotFound("Event not found."))));
|
||||
|
||||
let visible = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(body.sender_user(), &body.room_id, &body.event_id)
|
||||
.map(Ok);
|
||||
|
||||
let (token, mut event, visible) = try_join!(token, event, visible)?;
|
||||
|
||||
if !visible
|
||||
|| ignored_filter(&services, (token, event.clone()), body.sender_user())
|
||||
.await
|
||||
.is_none()
|
||||
{
|
||||
return Err!(Request(Forbidden("You don't have permission to view this event.")));
|
||||
}
|
||||
|
||||
if event.event_id() != &body.event_id || event.room_id() != body.room_id {
|
||||
return Err!(Request(NotFound("Event not found")));
|
||||
}
|
||||
|
||||
event.add_age().ok();
|
||||
|
||||
let event = event.to_room_event();
|
||||
|
||||
Ok(get_room_event::v3::Response {
|
||||
event,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use axum::extract::State;
|
||||
use conduit::{at, utils::BoolExt, Err, Result};
|
||||
use futures::StreamExt;
|
||||
use ruma::api::client::room::initial_sync::v3::{PaginationChunk, Request, Response};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
const LIMIT_MAX: usize = 100;
|
||||
|
||||
pub(crate) async fn room_initial_sync_route(
|
||||
State(services): State<crate::State>, body: Ruma<Request>,
|
||||
) -> Result<Response> {
|
||||
let room_id = &body.room_id;
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(body.sender_user(), room_id)
|
||||
.await
|
||||
{
|
||||
return Err!(Request(Forbidden("No room preview available.")));
|
||||
}
|
||||
|
||||
let limit = LIMIT_MAX;
|
||||
let events: Vec<_> = services
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_rev(None, room_id, None)
|
||||
.await?
|
||||
.take(limit)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let state: Vec<_> = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full_pdus(room_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|pdu| pdu.to_state_event())
|
||||
.collect();
|
||||
|
||||
let messages = PaginationChunk {
|
||||
start: events.last().map(at!(0)).as_ref().map(ToString::to_string),
|
||||
|
||||
end: events
|
||||
.first()
|
||||
.map(at!(0))
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default(),
|
||||
|
||||
chunk: events
|
||||
.into_iter()
|
||||
.map(at!(1))
|
||||
.map(|pdu| pdu.to_room_event())
|
||||
.collect(),
|
||||
};
|
||||
|
||||
Ok(Response {
|
||||
room_id: room_id.to_owned(),
|
||||
account_data: None,
|
||||
state: state.into(),
|
||||
messages: messages.chunk.is_empty().or_some(messages),
|
||||
visibility: services.rooms.directory.visibility(room_id).await.into(),
|
||||
membership: services
|
||||
.rooms
|
||||
.state_cache
|
||||
.user_membership(body.sender_user(), room_id)
|
||||
.await,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
mod aliases;
|
||||
mod create;
|
||||
mod event;
|
||||
mod initial_sync;
|
||||
mod upgrade;
|
||||
|
||||
pub(crate) use self::{
|
||||
aliases::get_room_aliases_route, create::create_room_route, event::get_room_event_route,
|
||||
initial_sync::room_initial_sync_route, upgrade::upgrade_room_route,
|
||||
};
|
||||
@@ -0,0 +1,294 @@
|
||||
use std::cmp::max;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduit::{err, info, pdu::PduBuilder, Error, Result};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
api::client::{error::ErrorKind, room::upgrade_room},
|
||||
events::{
|
||||
room::{
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
power_levels::RoomPowerLevelsEventContent,
|
||||
tombstone::RoomTombstoneEventContent,
|
||||
},
|
||||
StateEventType, TimelineEventType,
|
||||
},
|
||||
int, CanonicalJsonObject, RoomId, RoomVersionId,
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// Recommended transferable state events list from the spec
|
||||
const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 9] = &[
|
||||
StateEventType::RoomServerAcl,
|
||||
StateEventType::RoomEncryption,
|
||||
StateEventType::RoomName,
|
||||
StateEventType::RoomAvatar,
|
||||
StateEventType::RoomTopic,
|
||||
StateEventType::RoomGuestAccess,
|
||||
StateEventType::RoomHistoryVisibility,
|
||||
StateEventType::RoomJoinRules,
|
||||
StateEventType::RoomPowerLevels,
|
||||
];
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
|
||||
///
|
||||
/// Upgrades the room.
|
||||
///
|
||||
/// - Creates a replacement room
|
||||
/// - Sends a tombstone event into the current room
|
||||
/// - Sender user joins the room
|
||||
/// - Transfers some state events
|
||||
/// - Moves local aliases
|
||||
/// - Modifies old room power levels to prevent users from speaking
|
||||
pub(crate) async fn upgrade_room_route(
|
||||
State(services): State<crate::State>, body: Ruma<upgrade_room::v3::Request>,
|
||||
) -> Result<upgrade_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services
|
||||
.globals
|
||||
.supported_room_versions()
|
||||
.contains(&body.new_version)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UnsupportedRoomVersion,
|
||||
"This server does not support that room version.",
|
||||
));
|
||||
}
|
||||
|
||||
// Create a replacement room
|
||||
let replacement_room = RoomId::new(services.globals.server_name());
|
||||
|
||||
let _short_id = services
|
||||
.rooms
|
||||
.short
|
||||
.get_or_create_shortroomid(&replacement_room)
|
||||
.await;
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
|
||||
// Send a m.room.tombstone event to the old room to indicate that it is not
|
||||
// intended to be used any further Fail if the sender does not have the required
|
||||
// permissions
|
||||
let tombstone_event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
String::new(),
|
||||
&RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: replacement_room.clone(),
|
||||
},
|
||||
),
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change lock to replacement room
|
||||
drop(state_lock);
|
||||
let state_lock = services.rooms.state.mutex.lock(&replacement_room).await;
|
||||
|
||||
// Get the old room creation event
|
||||
let mut create_event_content: CanonicalJsonObject = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&body.room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.map_err(|_| err!(Database("Found room without m.room.create event.")))?;
|
||||
|
||||
// Use the m.room.tombstone event as the predecessor
|
||||
let predecessor = Some(ruma::events::room::create::PreviousRoom::new(
|
||||
body.room_id.clone(),
|
||||
(*tombstone_event_id).to_owned(),
|
||||
));
|
||||
|
||||
// Send a m.room.create event containing a predecessor field and the applicable
|
||||
// room_version
|
||||
{
|
||||
use RoomVersionId::*;
|
||||
match body.new_version {
|
||||
V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
|
||||
create_event_content.insert(
|
||||
"creator".into(),
|
||||
json!(&sender_user).try_into().map_err(|e| {
|
||||
info!("Error forming creation event: {e}");
|
||||
Error::BadRequest(ErrorKind::BadJson, "Error forming creation event")
|
||||
})?,
|
||||
);
|
||||
},
|
||||
_ => {
|
||||
// "creator" key no longer exists in V11+ rooms
|
||||
create_event_content.remove("creator");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
create_event_content.insert(
|
||||
"room_version".into(),
|
||||
json!(&body.new_version)
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
|
||||
);
|
||||
create_event_content.insert(
|
||||
"predecessor".into(),
|
||||
json!(predecessor)
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
|
||||
);
|
||||
|
||||
// Validate creation event content
|
||||
if serde_json::from_str::<CanonicalJsonObject>(
|
||||
to_raw_value(&create_event_content)
|
||||
.expect("Error forming creation event")
|
||||
.get(),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"));
|
||||
}
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&create_event_content).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(String::new()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Join the new room
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
membership: MembershipState::Join,
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
is_direct: None,
|
||||
third_party_invite: None,
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
reason: None,
|
||||
join_authorized_via_users_server: None,
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Replicate transferable state events to the new room
|
||||
for event_type in TRANSFERABLE_STATE_EVENTS {
|
||||
let event_content = match services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, event_type, "")
|
||||
.await
|
||||
{
|
||||
Ok(v) => v.content.clone(),
|
||||
Err(_) => continue, // Skipping missing events.
|
||||
};
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: event_type.to_string().into(),
|
||||
content: event_content,
|
||||
state_key: Some(String::new()),
|
||||
..Default::default()
|
||||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Moves any local aliases to the new room
|
||||
let mut local_aliases = services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.boxed();
|
||||
|
||||
while let Some(alias) = local_aliases.next().await {
|
||||
services
|
||||
.rooms
|
||||
.alias
|
||||
.remove_alias(alias, sender_user)
|
||||
.await?;
|
||||
|
||||
services
|
||||
.rooms
|
||||
.alias
|
||||
.set_alias(alias, &replacement_room, sender_user)?;
|
||||
}
|
||||
|
||||
// Get the old room power levels
|
||||
let power_levels_event_content: RoomPowerLevelsEventContent = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&body.room_id, &StateEventType::RoomPowerLevels, "")
|
||||
.await
|
||||
.map_err(|_| err!(Database("Found room without m.room.power_levels event.")))?;
|
||||
|
||||
// Setting events_default and invite to the greater of 50 and users_default + 1
|
||||
let new_level = max(
|
||||
int!(50),
|
||||
power_levels_event_content
|
||||
.users_default
|
||||
.checked_add(int!(1))
|
||||
.ok_or_else(|| err!(Request(BadJson("users_default power levels event content is not valid"))))?,
|
||||
);
|
||||
|
||||
// Modify the power levels in the old room to prevent sending of events and
|
||||
// inviting new users
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
String::new(),
|
||||
&RoomPowerLevelsEventContent {
|
||||
events_default: new_level,
|
||||
invite: new_level,
|
||||
..power_levels_event_content
|
||||
},
|
||||
),
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
// Return the replacement room id
|
||||
Ok(upgrade_room::v3::Response {
|
||||
replacement_room,
|
||||
})
|
||||
}
|
||||
+176
-151
@@ -1,21 +1,33 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduit::{
|
||||
at, is_true,
|
||||
result::FlatOk,
|
||||
utils::{stream::ReadyExt, IterStream},
|
||||
Err, PduEvent, Result,
|
||||
};
|
||||
use futures::{future::OptionFuture, FutureExt, StreamExt, TryFutureExt};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
search::search_events::{
|
||||
self,
|
||||
v3::{EventContextResult, ResultCategories, ResultRoomEvents, SearchResult},
|
||||
},
|
||||
api::client::search::search_events::{
|
||||
self,
|
||||
v3::{Criteria, EventContextResult, ResultCategories, ResultRoomEvents, SearchResult},
|
||||
},
|
||||
events::AnyStateEvent,
|
||||
serde::Raw,
|
||||
uint, OwnedRoomId,
|
||||
OwnedRoomId, RoomId, UInt, UserId,
|
||||
};
|
||||
use tracing::debug;
|
||||
use search_events::v3::{Request, Response};
|
||||
use service::{rooms::search::RoomQuery, Services};
|
||||
|
||||
use crate::{Error, Result, Ruma};
|
||||
use crate::Ruma;
|
||||
|
||||
type RoomStates = BTreeMap<OwnedRoomId, RoomState>;
|
||||
type RoomState = Vec<Raw<AnyStateEvent>>;
|
||||
|
||||
const LIMIT_DEFAULT: usize = 10;
|
||||
const LIMIT_MAX: usize = 100;
|
||||
const BATCH_MAX: usize = 20;
|
||||
|
||||
/// # `POST /_matrix/client/r0/search`
|
||||
///
|
||||
@@ -23,160 +35,173 @@ use crate::{Error, Result, Ruma};
|
||||
///
|
||||
/// - Only works if the user is currently joined to the room (TODO: Respect
|
||||
/// history visibility)
|
||||
pub(crate) async fn search_events_route(
|
||||
State(services): State<crate::State>, body: Ruma<search_events::v3::Request>,
|
||||
) -> Result<search_events::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
pub(crate) async fn search_events_route(State(services): State<crate::State>, body: Ruma<Request>) -> Result<Response> {
|
||||
let sender_user = body.sender_user();
|
||||
let next_batch = body.next_batch.as_deref();
|
||||
let room_events_result: OptionFuture<_> = body
|
||||
.search_categories
|
||||
.room_events
|
||||
.as_ref()
|
||||
.map(|criteria| category_room_events(&services, sender_user, next_batch, criteria))
|
||||
.into();
|
||||
|
||||
let search_criteria = body.search_categories.room_events.as_ref().unwrap();
|
||||
let filter = &search_criteria.filter;
|
||||
let include_state = &search_criteria.include_state;
|
||||
Ok(Response {
|
||||
search_categories: ResultCategories {
|
||||
room_events: room_events_result
|
||||
.await
|
||||
.unwrap_or_else(|| Ok(ResultRoomEvents::default()))?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let room_ids = filter.rooms.clone().unwrap_or_else(|| {
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.filter_map(Result::ok)
|
||||
.collect()
|
||||
});
|
||||
#[allow(clippy::map_unwrap_or)]
|
||||
async fn category_room_events(
|
||||
services: &Services, sender_user: &UserId, next_batch: Option<&str>, criteria: &Criteria,
|
||||
) -> Result<ResultRoomEvents> {
|
||||
let filter = &criteria.filter;
|
||||
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit: usize = filter
|
||||
.limit
|
||||
.unwrap_or_else(|| uint!(10))
|
||||
.try_into()
|
||||
.unwrap_or(10)
|
||||
.min(100);
|
||||
.map(TryInto::try_into)
|
||||
.flat_ok()
|
||||
.unwrap_or(LIMIT_DEFAULT)
|
||||
.min(LIMIT_MAX);
|
||||
|
||||
let mut room_states: BTreeMap<OwnedRoomId, Vec<Raw<AnyStateEvent>>> = BTreeMap::new();
|
||||
let next_batch: usize = next_batch
|
||||
.map(str::parse)
|
||||
.transpose()?
|
||||
.unwrap_or(0)
|
||||
.min(limit.saturating_mul(BATCH_MAX));
|
||||
|
||||
if include_state.is_some_and(|include_state| include_state) {
|
||||
for room_id in &room_ids {
|
||||
if !services.rooms.state_cache.is_joined(sender_user, room_id)? {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
|
||||
// check if sender_user can see state events
|
||||
if services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, room_id)?
|
||||
{
|
||||
let room_state = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full(room_id)
|
||||
.await?
|
||||
.values()
|
||||
.map(|pdu| pdu.to_state_event())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
debug!("Room state: {:?}", room_state);
|
||||
|
||||
room_states.insert(room_id.clone(), room_state);
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut searches = Vec::new();
|
||||
|
||||
for room_id in &room_ids {
|
||||
if !services.rooms.state_cache.is_joined(sender_user, room_id)? {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(search) = services
|
||||
.rooms
|
||||
.search
|
||||
.search_pdus(room_id, &search_criteria.search_term)?
|
||||
{
|
||||
searches.push(search.0.peekable());
|
||||
}
|
||||
}
|
||||
|
||||
let skip: usize = match body.next_batch.as_ref().map(|s| s.parse()) {
|
||||
Some(Ok(s)) => s,
|
||||
Some(Err(_)) => return Err(Error::BadRequest(ErrorKind::InvalidParam, "Invalid next_batch token.")),
|
||||
None => 0, // Default to the start
|
||||
};
|
||||
|
||||
let mut results = Vec::new();
|
||||
let next_batch = skip.saturating_add(limit);
|
||||
|
||||
for _ in 0..next_batch {
|
||||
if let Some(s) = searches
|
||||
.iter_mut()
|
||||
.map(|s| (s.peek().cloned(), s))
|
||||
.max_by_key(|(peek, _)| peek.clone())
|
||||
.and_then(|(_, i)| i.next())
|
||||
{
|
||||
results.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
let results: Vec<_> = results
|
||||
.iter()
|
||||
.skip(skip)
|
||||
.filter_map(|result| {
|
||||
let rooms = filter
|
||||
.rooms
|
||||
.clone()
|
||||
.map(IntoIterator::into_iter)
|
||||
.map(IterStream::stream)
|
||||
.map(StreamExt::boxed)
|
||||
.unwrap_or_else(|| {
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_from_id(result)
|
||||
.ok()?
|
||||
.filter(|pdu| {
|
||||
!pdu.is_redacted()
|
||||
&& services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|pdu| pdu.to_room_event())
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.map(ToOwned::to_owned)
|
||||
.boxed()
|
||||
});
|
||||
|
||||
let results: Vec<_> = rooms
|
||||
.filter_map(|room_id| async move {
|
||||
check_room_visible(services, sender_user, &room_id, criteria)
|
||||
.await
|
||||
.is_ok()
|
||||
.then_some(room_id)
|
||||
})
|
||||
.map(|result| {
|
||||
Ok::<_, Error>(SearchResult {
|
||||
context: EventContextResult {
|
||||
end: None,
|
||||
events_after: Vec::new(),
|
||||
events_before: Vec::new(),
|
||||
profile_info: BTreeMap::new(),
|
||||
start: None,
|
||||
},
|
||||
rank: None,
|
||||
result: Some(result),
|
||||
})
|
||||
.filter_map(|room_id| async move {
|
||||
let query = RoomQuery {
|
||||
room_id: &room_id,
|
||||
user_id: Some(sender_user),
|
||||
criteria,
|
||||
skip: next_batch,
|
||||
limit,
|
||||
};
|
||||
|
||||
let (count, results) = services.rooms.search.search_pdus(&query).await.ok()?;
|
||||
|
||||
results
|
||||
.collect::<Vec<_>>()
|
||||
.map(|results| (room_id.clone(), count, results))
|
||||
.map(Some)
|
||||
.await
|
||||
})
|
||||
.filter_map(Result::ok)
|
||||
.take(limit)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let total: UInt = results
|
||||
.iter()
|
||||
.fold(0, |a: usize, (_, count, _)| a.saturating_add(*count))
|
||||
.try_into()?;
|
||||
|
||||
let state: RoomStates = results
|
||||
.iter()
|
||||
.stream()
|
||||
.ready_filter(|_| criteria.include_state.is_some_and(is_true!()))
|
||||
.filter_map(|(room_id, ..)| async move {
|
||||
procure_room_state(services, room_id)
|
||||
.map_ok(|state| (room_id.clone(), state))
|
||||
.await
|
||||
.ok()
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let results: Vec<SearchResult> = results
|
||||
.into_iter()
|
||||
.map(at!(2))
|
||||
.flatten()
|
||||
.stream()
|
||||
.map(|pdu| pdu.to_room_event())
|
||||
.map(|result| SearchResult {
|
||||
rank: None,
|
||||
result: Some(result),
|
||||
context: EventContextResult {
|
||||
profile_info: BTreeMap::new(), //TODO
|
||||
events_after: Vec::new(), //TODO
|
||||
events_before: Vec::new(), //TODO
|
||||
start: None, //TODO
|
||||
end: None, //TODO
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let highlights = criteria
|
||||
.search_term
|
||||
.split_terminator(|c: char| !c.is_alphanumeric())
|
||||
.map(str::to_lowercase)
|
||||
.collect();
|
||||
|
||||
let more_unloaded_results = searches.iter_mut().any(|s| s.peek().is_some());
|
||||
let next_batch = more_unloaded_results.then(|| next_batch.to_string());
|
||||
let next_batch = (results.len() >= limit)
|
||||
.then_some(next_batch.saturating_add(results.len()))
|
||||
.as_ref()
|
||||
.map(ToString::to_string);
|
||||
|
||||
Ok(search_events::v3::Response::new(ResultCategories {
|
||||
room_events: ResultRoomEvents {
|
||||
count: Some(results.len().try_into().unwrap_or_else(|_| uint!(0))),
|
||||
groups: BTreeMap::new(), // TODO
|
||||
next_batch,
|
||||
results,
|
||||
state: room_states,
|
||||
highlights: search_criteria
|
||||
.search_term
|
||||
.split_terminator(|c: char| !c.is_alphanumeric())
|
||||
.map(str::to_lowercase)
|
||||
.collect(),
|
||||
},
|
||||
}))
|
||||
Ok(ResultRoomEvents {
|
||||
count: Some(total),
|
||||
next_batch,
|
||||
results,
|
||||
state,
|
||||
highlights,
|
||||
groups: BTreeMap::new(), // TODO
|
||||
})
|
||||
}
|
||||
|
||||
async fn procure_room_state(services: &Services, room_id: &RoomId) -> Result<RoomState> {
|
||||
let state_map = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full(room_id)
|
||||
.await?;
|
||||
|
||||
let state_events = state_map.values().map(PduEvent::to_state_event).collect();
|
||||
|
||||
Ok(state_events)
|
||||
}
|
||||
|
||||
async fn check_room_visible(services: &Services, user_id: &UserId, room_id: &RoomId, search: &Criteria) -> Result {
|
||||
let check_visible = search.filter.rooms.is_some();
|
||||
let check_state = check_visible && search.include_state.is_some_and(is_true!());
|
||||
|
||||
let is_joined = !check_visible || services.rooms.state_cache.is_joined(user_id, room_id).await;
|
||||
|
||||
let state_visible = !check_state
|
||||
|| services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(user_id, room_id)
|
||||
.await;
|
||||
|
||||
if !is_joined || !state_visible {
|
||||
return Err!(Request(Forbidden("You don't have permission to view {room_id:?}")));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduit::{err, Err};
|
||||
use ruma::{api::client::message::send_message_event, events::MessageLikeEventType};
|
||||
use serde_json::from_str;
|
||||
|
||||
use crate::{service::pdu::PduBuilder, utils, Result, Ruma};
|
||||
|
||||
/// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`
|
||||
///
|
||||
/// Send a message event into the room.
|
||||
///
|
||||
/// - Is a NOOP if the txn id was already used before and returns the same event
|
||||
/// id again
|
||||
/// - The only requirement for the content is that it has to be valid json
|
||||
/// - Tries to send the event into the room, auth rules will determine if it is
|
||||
/// allowed
|
||||
pub(crate) async fn send_message_event_route(
|
||||
State(services): State<crate::State>, body: Ruma<send_message_event::v3::Request>,
|
||||
) -> Result<send_message_event::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
let sender_device = body.sender_device.as_deref();
|
||||
let appservice_info = body.appservice_info.as_ref();
|
||||
|
||||
// Forbid m.room.encrypted if encryption is disabled
|
||||
if MessageLikeEventType::RoomEncrypted == body.event_type && !services.globals.allow_encryption() {
|
||||
return Err!(Request(Forbidden("Encryption has been disabled")));
|
||||
}
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
|
||||
if body.event_type == MessageLikeEventType::CallInvite
|
||||
&& services.rooms.directory.is_public_room(&body.room_id).await
|
||||
{
|
||||
return Err!(Request(Forbidden("Room call invites are not allowed in public rooms")));
|
||||
}
|
||||
|
||||
// Check if this is a new transaction id
|
||||
if let Ok(response) = services
|
||||
.transaction_ids
|
||||
.existing_txnid(sender_user, sender_device, &body.txn_id)
|
||||
.await
|
||||
{
|
||||
// The client might have sent a txnid of the /sendToDevice endpoint
|
||||
// This txnid has no response associated with it
|
||||
if response.is_empty() {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Tried to use txn id already used for an incompatible endpoint."
|
||||
)));
|
||||
}
|
||||
|
||||
return Ok(send_message_event::v3::Response {
|
||||
event_id: utils::string_from_bytes(&response)
|
||||
.map(TryInto::try_into)
|
||||
.map_err(|e| err!(Database("Invalid event_id in txnid data: {e:?}")))??,
|
||||
});
|
||||
}
|
||||
|
||||
let mut unsigned = BTreeMap::new();
|
||||
unsigned.insert("transaction_id".to_owned(), body.txn_id.to_string().into());
|
||||
|
||||
let content =
|
||||
from_str(body.body.body.json().get()).map_err(|e| err!(Request(BadJson("Invalid JSON body: {e}"))))?;
|
||||
|
||||
let event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: body.event_type.clone().into(),
|
||||
content,
|
||||
unsigned: Some(unsigned),
|
||||
timestamp: appservice_info.and(body.timestamp),
|
||||
..Default::default()
|
||||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
services
|
||||
.transaction_ids
|
||||
.add_txnid(sender_user, sender_device, &body.txn_id, event_id.as_bytes());
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
Ok(send_message_event::v3::Response {
|
||||
event_id: event_id.into(),
|
||||
})
|
||||
}
|
||||
+48
-33
@@ -1,5 +1,7 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduit::{debug, err, info, utils::ReadyExt, warn, Err};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
@@ -19,7 +21,6 @@ use ruma::{
|
||||
UserId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||
use crate::{utils, utils::hash, Error, Result, Ruma};
|
||||
@@ -79,21 +80,22 @@ pub(crate) async fn login_route(
|
||||
UserId::parse(user)
|
||||
} else {
|
||||
warn!("Bad login type: {:?}", &body.login_info);
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
|
||||
return Err!(Request(Forbidden("Bad login type.")));
|
||||
}
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
|
||||
|
||||
let hash = services
|
||||
.users
|
||||
.password_hash(&user_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::forbidden(), "Wrong username or password."))?;
|
||||
.password_hash(&user_id)
|
||||
.await
|
||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
||||
|
||||
if hash.is_empty() {
|
||||
return Err(Error::BadRequest(ErrorKind::UserDeactivated, "The user has been deactivated"));
|
||||
return Err!(Request(UserDeactivated("The user has been deactivated")));
|
||||
}
|
||||
|
||||
if hash::verify_password(password, &hash).is_err() {
|
||||
return Err(Error::BadRequest(ErrorKind::forbidden(), "Wrong username or password."));
|
||||
return Err!(Request(Forbidden("Wrong username or password.")));
|
||||
}
|
||||
|
||||
user_id
|
||||
@@ -112,15 +114,12 @@ pub(crate) async fn login_route(
|
||||
|
||||
let username = token.claims.sub.to_lowercase();
|
||||
|
||||
UserId::parse_with_server_name(username, services.globals.server_name()).map_err(|e| {
|
||||
warn!("Failed to parse username from user logging in: {e}");
|
||||
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
|
||||
})?
|
||||
UserId::parse_with_server_name(username, services.globals.server_name())
|
||||
.map_err(|e| err!(Request(InvalidUsername(debug_error!(?e, "Failed to parse login username")))))?
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Unknown,
|
||||
"Token login is not supported (server has no jwt decoding key).",
|
||||
));
|
||||
return Err!(Request(Unknown(
|
||||
"Token login is not supported (server has no jwt decoding key)."
|
||||
)));
|
||||
}
|
||||
},
|
||||
#[allow(deprecated)]
|
||||
@@ -169,29 +168,40 @@ pub(crate) async fn login_route(
|
||||
let token = utils::random_string(TOKEN_LENGTH);
|
||||
|
||||
// Determine if device_id was provided and exists in the db for this user
|
||||
let device_exists = body.device_id.as_ref().map_or(false, |device_id| {
|
||||
let device_exists = if body.device_id.is_some() {
|
||||
services
|
||||
.users
|
||||
.all_device_ids(&user_id)
|
||||
.any(|x| x.as_ref().map_or(false, |v| v == device_id))
|
||||
});
|
||||
.ready_any(|v| v == device_id)
|
||||
.await
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if device_exists {
|
||||
services.users.set_token(&user_id, &device_id, &token)?;
|
||||
services
|
||||
.users
|
||||
.set_token(&user_id, &device_id, &token)
|
||||
.await?;
|
||||
} else {
|
||||
services.users.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&token,
|
||||
body.initial_device_display_name.clone(),
|
||||
Some(client.to_string()),
|
||||
)?;
|
||||
services
|
||||
.users
|
||||
.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&token,
|
||||
body.initial_device_display_name.clone(),
|
||||
Some(client.to_string()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// send client well-known if specified so the client knows to reconfigure itself
|
||||
let client_discovery_info: Option<DiscoveryInfo> = services
|
||||
.globals
|
||||
.well_known_client()
|
||||
.server
|
||||
.config
|
||||
.well_known
|
||||
.client
|
||||
.as_ref()
|
||||
.map(|server| DiscoveryInfo::new(HomeserverInfo::new(server.to_string())));
|
||||
|
||||
@@ -228,10 +238,13 @@ pub(crate) async fn logout_route(
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
services.users.remove_device(sender_user, sender_device)?;
|
||||
services
|
||||
.users
|
||||
.remove_device(sender_user, sender_device)
|
||||
.await;
|
||||
|
||||
// send device list update for user after logout
|
||||
services.users.mark_device_key_update(sender_user)?;
|
||||
services.users.mark_device_key_update(sender_user).await;
|
||||
|
||||
Ok(logout::v3::Response::new())
|
||||
}
|
||||
@@ -256,12 +269,14 @@ pub(crate) async fn logout_all_route(
|
||||
) -> Result<logout_all::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
for device_id in services.users.all_device_ids(sender_user).flatten() {
|
||||
services.users.remove_device(sender_user, &device_id)?;
|
||||
}
|
||||
services
|
||||
.users
|
||||
.all_device_ids(sender_user)
|
||||
.for_each(|device_id| services.users.remove_device(sender_user, device_id))
|
||||
.await;
|
||||
|
||||
// send device list update for user after logout
|
||||
services.users.mark_device_key_update(sender_user)?;
|
||||
services.users.mark_device_key_update(sender_user).await;
|
||||
|
||||
Ok(logout_all::v3::Response::new())
|
||||
}
|
||||
|
||||
+44
-52
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduit::{debug_info, error, pdu::PduBuilder, Error, Result};
|
||||
use conduit::{err, pdu::PduBuilder, utils::BoolExt, Err, Error, PduEvent, Result};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
@@ -84,12 +84,10 @@ pub(crate) async fn get_state_events_route(
|
||||
if !services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, &body.room_id)?
|
||||
.user_can_see_state_events(sender_user, &body.room_id)
|
||||
.await
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view the room state.",
|
||||
));
|
||||
return Err!(Request(Forbidden("You don't have permission to view the room state.")));
|
||||
}
|
||||
|
||||
Ok(get_state_events::v3::Response {
|
||||
@@ -99,7 +97,7 @@ pub(crate) async fn get_state_events_route(
|
||||
.room_state_full(&body.room_id)
|
||||
.await?
|
||||
.values()
|
||||
.map(|pdu| pdu.to_state_event())
|
||||
.map(PduEvent::to_state_event)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
@@ -120,43 +118,34 @@ pub(crate) async fn get_state_events_for_key_route(
|
||||
if !services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, &body.room_id)?
|
||||
.user_can_see_state_events(sender_user, &body.room_id)
|
||||
.await
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You don't have permission to view the room state.",
|
||||
));
|
||||
return Err!(Request(Forbidden("You don't have permission to view the room state.")));
|
||||
}
|
||||
|
||||
let event = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &body.event_type, &body.state_key)?
|
||||
.ok_or_else(|| {
|
||||
debug_info!("State event {:?} not found in room {:?}", &body.event_type, &body.room_id);
|
||||
Error::BadRequest(ErrorKind::NotFound, "State event not found.")
|
||||
.room_state_get(&body.room_id, &body.event_type, &body.state_key)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
err!(Request(NotFound(debug_warn!(
|
||||
room_id = ?body.room_id,
|
||||
event_type = ?body.event_type,
|
||||
"State event not found in room.",
|
||||
))))
|
||||
})?;
|
||||
if body
|
||||
|
||||
let event_format = body
|
||||
.format
|
||||
.as_ref()
|
||||
.is_some_and(|f| f.to_lowercase().eq("event"))
|
||||
{
|
||||
Ok(get_state_events_for_key::v3::Response {
|
||||
content: None,
|
||||
event: serde_json::from_str(event.to_state_event().json().get()).map_err(|e| {
|
||||
error!("Invalid room state event in database: {}", e);
|
||||
Error::bad_database("Invalid room state event in database")
|
||||
})?,
|
||||
})
|
||||
} else {
|
||||
Ok(get_state_events_for_key::v3::Response {
|
||||
content: Some(serde_json::from_str(event.content.get()).map_err(|e| {
|
||||
error!("Invalid room state event content in database: {}", e);
|
||||
Error::bad_database("Invalid room state event content in database")
|
||||
})?),
|
||||
event: None,
|
||||
})
|
||||
}
|
||||
.is_some_and(|f| f.to_lowercase().eq("event"));
|
||||
|
||||
Ok(get_state_events_for_key::v3::Response {
|
||||
content: event_format.or(|| event.get_content_as_value()),
|
||||
event: event_format.then(|| event.to_state_event_value()),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/rooms/{roomid}/state/{eventType}`
|
||||
@@ -187,11 +176,10 @@ async fn send_state_event_for_key_helper(
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: event_type.to_string().into(),
|
||||
content: serde_json::from_str(json.json().get()).expect("content is valid json"),
|
||||
unsigned: None,
|
||||
content: serde_json::from_str(json.json().get())?,
|
||||
state_key: Some(state_key),
|
||||
redacts: None,
|
||||
timestamp,
|
||||
..Default::default()
|
||||
},
|
||||
sender,
|
||||
room_id,
|
||||
@@ -204,7 +192,7 @@ async fn send_state_event_for_key_helper(
|
||||
|
||||
async fn allowed_to_send_state_event(
|
||||
services: &Services, room_id: &RoomId, event_type: &StateEventType, json: &Raw<AnyStateEventContent>,
|
||||
) -> Result<()> {
|
||||
) -> Result {
|
||||
match event_type {
|
||||
// Forbid m.room.encryption if encryption is disabled
|
||||
StateEventType::RoomEncryption => {
|
||||
@@ -214,7 +202,7 @@ async fn allowed_to_send_state_event(
|
||||
},
|
||||
// admin room is a sensitive room, it should not ever be made public
|
||||
StateEventType::RoomJoinRules => {
|
||||
if let Some(admin_room_id) = services.admin.get_admin_room()? {
|
||||
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
|
||||
if admin_room_id == room_id {
|
||||
if let Ok(join_rule) = serde_json::from_str::<RoomJoinRulesEventContent>(json.json().get()) {
|
||||
if join_rule.join_rule == JoinRule::Public {
|
||||
@@ -229,7 +217,7 @@ async fn allowed_to_send_state_event(
|
||||
},
|
||||
// admin room is a sensitive room, it should not ever be made world readable
|
||||
StateEventType::RoomHistoryVisibility => {
|
||||
if let Some(admin_room_id) = services.admin.get_admin_room()? {
|
||||
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
|
||||
if admin_room_id == room_id {
|
||||
if let Ok(visibility_content) =
|
||||
serde_json::from_str::<RoomHistoryVisibilityEventContent>(json.json().get())
|
||||
@@ -254,23 +242,27 @@ async fn allowed_to_send_state_event(
|
||||
}
|
||||
|
||||
for alias in aliases {
|
||||
if !services.globals.server_is_ours(alias.server_name())
|
||||
|| services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&alias)?
|
||||
.filter(|room| room == room_id) // Make sure it's the right room
|
||||
.is_none()
|
||||
if !services.globals.server_is_ours(alias.server_name()) {
|
||||
return Err!(Request(Forbidden("canonical_alias must be for this server")));
|
||||
}
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&alias)
|
||||
.await
|
||||
.is_ok_and(|room| room == room_id)
|
||||
// Make sure it's the right room
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::forbidden(),
|
||||
"You are only allowed to send canonical_alias events when its aliases already exist",
|
||||
));
|
||||
return Err!(Request(Forbidden(
|
||||
"You are only allowed to send canonical_alias events when its aliases already exist"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
mod v3;
|
||||
mod v4;
|
||||
|
||||
use conduit::{
|
||||
utils::stream::{BroadbandExt, ReadyExt},
|
||||
PduCount,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruma::{RoomId, UserId};
|
||||
|
||||
pub(crate) use self::{v3::sync_events_route, v4::sync_events_v4_route};
|
||||
use crate::{service::Services, Error, PduEvent, Result};
|
||||
|
||||
async fn load_timeline(
|
||||
services: &Services, sender_user: &UserId, room_id: &RoomId, roomsincecount: PduCount,
|
||||
next_batch: Option<PduCount>, limit: usize,
|
||||
) -> Result<(Vec<(PduCount, PduEvent)>, bool), Error> {
|
||||
let last_timeline_count = services
|
||||
.rooms
|
||||
.timeline
|
||||
.last_timeline_count(Some(sender_user), room_id)
|
||||
.await?;
|
||||
|
||||
if last_timeline_count <= roomsincecount {
|
||||
return Ok((Vec::new(), false));
|
||||
}
|
||||
|
||||
let mut non_timeline_pdus = services
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_rev(Some(sender_user), room_id, None)
|
||||
.await?
|
||||
.ready_skip_while(|&(pducount, _)| pducount > next_batch.unwrap_or_else(PduCount::max))
|
||||
.ready_take_while(|&(pducount, _)| pducount > roomsincecount);
|
||||
|
||||
// Take the last events for the timeline
|
||||
let timeline_pdus: Vec<_> = non_timeline_pdus
|
||||
.by_ref()
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect();
|
||||
|
||||
// They /sync response doesn't always return all messages, so we say the output
|
||||
// is limited unless there are events in non_timeline_pdus
|
||||
let limited = non_timeline_pdus.next().await.is_some();
|
||||
|
||||
Ok((timeline_pdus, limited))
|
||||
}
|
||||
|
||||
async fn share_encrypted_room(
|
||||
services: &Services, sender_user: &UserId, user_id: &UserId, ignore_room: Option<&RoomId>,
|
||||
) -> bool {
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.get_shared_rooms(sender_user, user_id)
|
||||
.ready_filter(|&room_id| Some(room_id) != ignore_room)
|
||||
.broad_any(|other_room_id| {
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.is_encrypted_room(other_room_id)
|
||||
})
|
||||
.await
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,771 @@
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use axum::extract::State;
|
||||
use conduit::{
|
||||
debug, error, extract_variant,
|
||||
utils::{
|
||||
math::{ruma_from_usize, usize_from_ruma, usize_from_u64_truncated},
|
||||
BoolExt, IterStream, ReadyExt, TryFutureExtExt,
|
||||
},
|
||||
warn, Error, PduCount, Result,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt, TryFutureExt};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
sync::sync_events::{
|
||||
self,
|
||||
v4::{SlidingOp, SlidingSyncRoomHero},
|
||||
DeviceLists, UnreadNotificationsCount,
|
||||
},
|
||||
},
|
||||
directory::RoomTypeFilter,
|
||||
events::{
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
AnyRawAccountDataEvent, StateEventType,
|
||||
TimelineEventType::{self, *},
|
||||
},
|
||||
state_res::Event,
|
||||
uint, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, UInt, UserId,
|
||||
};
|
||||
use service::{rooms::read_receipt::pack_receipts, Services};
|
||||
|
||||
use super::{load_timeline, share_encrypted_room};
|
||||
use crate::{client::ignored_filter, Ruma};
|
||||
|
||||
const SINGLE_CONNECTION_SYNC: &str = "single_connection_sync";
|
||||
const DEFAULT_BUMP_TYPES: &[TimelineEventType; 6] =
|
||||
&[RoomMessage, RoomEncrypted, Sticker, CallInvite, PollStart, Beacon];
|
||||
|
||||
/// POST `/_matrix/client/unstable/org.matrix.msc3575/sync`
|
||||
///
|
||||
/// Sliding Sync endpoint (future endpoint: `/_matrix/client/v4/sync`)
|
||||
pub(crate) async fn sync_events_v4_route(
|
||||
State(services): State<crate::State>, body: Ruma<sync_events::v4::Request>,
|
||||
) -> Result<sync_events::v4::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.expect("user is authenticated");
|
||||
let mut body = body.body;
|
||||
// Setup watchers, so if there's no response, we can wait for them
|
||||
let watcher = services.sync.watch(sender_user, &sender_device);
|
||||
|
||||
let next_batch = services.globals.next_count()?;
|
||||
|
||||
let conn_id = body
|
||||
.conn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| SINGLE_CONNECTION_SYNC.to_owned());
|
||||
|
||||
let globalsince = body
|
||||
.pos
|
||||
.as_ref()
|
||||
.and_then(|string| string.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
if globalsince != 0
|
||||
&& !services
|
||||
.sync
|
||||
.remembered(sender_user.clone(), sender_device.clone(), conn_id.clone())
|
||||
{
|
||||
debug!("Restarting sync stream because it was gone from the database");
|
||||
return Err(Error::Request(
|
||||
ErrorKind::UnknownPos,
|
||||
"Connection data lost since last time".into(),
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
));
|
||||
}
|
||||
|
||||
if globalsince == 0 {
|
||||
services
|
||||
.sync
|
||||
.forget_sync_request_connection(sender_user.clone(), sender_device.clone(), conn_id.clone());
|
||||
}
|
||||
|
||||
// Get sticky parameters from cache
|
||||
let known_rooms =
|
||||
services
|
||||
.sync
|
||||
.update_sync_request_with_cache(sender_user.clone(), sender_device.clone(), &mut body);
|
||||
|
||||
let all_joined_rooms: Vec<_> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let all_invited_rooms: Vec<_> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_invited(sender_user)
|
||||
.map(|r| r.0)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let all_rooms = all_joined_rooms
|
||||
.iter()
|
||||
.chain(all_invited_rooms.iter())
|
||||
.map(Clone::clone)
|
||||
.collect();
|
||||
|
||||
if body.extensions.to_device.enabled.unwrap_or(false) {
|
||||
services
|
||||
.users
|
||||
.remove_to_device_events(sender_user, &sender_device, globalsince)
|
||||
.await;
|
||||
}
|
||||
|
||||
let mut left_encrypted_users = HashSet::new(); // Users that have left any encrypted rooms the sender was in
|
||||
let mut device_list_changes = HashSet::new();
|
||||
let mut device_list_left = HashSet::new();
|
||||
|
||||
let mut receipts = sync_events::v4::Receipts {
|
||||
rooms: BTreeMap::new(),
|
||||
};
|
||||
|
||||
let mut account_data = sync_events::v4::AccountData {
|
||||
global: Vec::new(),
|
||||
rooms: BTreeMap::new(),
|
||||
};
|
||||
if body.extensions.account_data.enabled.unwrap_or(false) {
|
||||
account_data.global = services
|
||||
.account_data
|
||||
.changes_since(None, sender_user, globalsince)
|
||||
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Global))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
if let Some(rooms) = body.extensions.account_data.rooms {
|
||||
for room in rooms {
|
||||
account_data.rooms.insert(
|
||||
room.clone(),
|
||||
services
|
||||
.account_data
|
||||
.changes_since(Some(&room), sender_user, globalsince)
|
||||
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
|
||||
.collect()
|
||||
.await,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if body.extensions.e2ee.enabled.unwrap_or(false) {
|
||||
// Look for device list updates of this account
|
||||
device_list_changes.extend(
|
||||
services
|
||||
.users
|
||||
.keys_changed(sender_user, globalsince, None)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await,
|
||||
);
|
||||
|
||||
for room_id in &all_joined_rooms {
|
||||
let Ok(current_shortstatehash) = services.rooms.state.get_room_shortstatehash(room_id).await else {
|
||||
error!("Room {room_id} has no state");
|
||||
continue;
|
||||
};
|
||||
|
||||
let since_shortstatehash = services
|
||||
.rooms
|
||||
.user
|
||||
.get_token_shortstatehash(room_id, globalsince)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let encrypted_room = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_get(current_shortstatehash, &StateEventType::RoomEncryption, "")
|
||||
.await
|
||||
.is_ok();
|
||||
|
||||
if let Some(since_shortstatehash) = since_shortstatehash {
|
||||
// Skip if there are only timeline changes
|
||||
if since_shortstatehash == current_shortstatehash {
|
||||
continue;
|
||||
}
|
||||
|
||||
let since_encryption = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_get(since_shortstatehash, &StateEventType::RoomEncryption, "")
|
||||
.await;
|
||||
|
||||
let since_sender_member: Option<RoomMemberEventContent> = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_get_content(since_shortstatehash, &StateEventType::RoomMember, sender_user.as_str())
|
||||
.ok()
|
||||
.await;
|
||||
|
||||
let joined_since_last_sync =
|
||||
since_sender_member.map_or(true, |member| member.membership != MembershipState::Join);
|
||||
|
||||
let new_encrypted_room = encrypted_room && since_encryption.is_err();
|
||||
|
||||
if encrypted_room {
|
||||
let current_state_ids: HashMap<_, OwnedEventId> = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_full_ids(current_shortstatehash)
|
||||
.await?;
|
||||
|
||||
let since_state_ids = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_full_ids(since_shortstatehash)
|
||||
.await?;
|
||||
|
||||
for (key, id) in current_state_ids {
|
||||
if since_state_ids.get(&key) != Some(&id) {
|
||||
let Ok(pdu) = services.rooms.timeline.get_pdu(&id).await else {
|
||||
error!("Pdu in state not found: {id}");
|
||||
continue;
|
||||
};
|
||||
if pdu.kind == RoomMember {
|
||||
if let Some(state_key) = &pdu.state_key {
|
||||
let user_id = UserId::parse(state_key.clone())
|
||||
.map_err(|_| Error::bad_database("Invalid UserId in member PDU."))?;
|
||||
|
||||
if user_id == *sender_user {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content: RoomMemberEventContent = pdu.get_content()?;
|
||||
match content.membership {
|
||||
MembershipState::Join => {
|
||||
// A new user joined an encrypted room
|
||||
if !share_encrypted_room(&services, sender_user, &user_id, Some(room_id))
|
||||
.await
|
||||
{
|
||||
device_list_changes.insert(user_id);
|
||||
}
|
||||
},
|
||||
MembershipState::Leave => {
|
||||
// Write down users that have left encrypted rooms we are in
|
||||
left_encrypted_users.insert(user_id);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if joined_since_last_sync || new_encrypted_room {
|
||||
// If the user is in a new encrypted room, give them all joined users
|
||||
device_list_changes.extend(
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(room_id)
|
||||
// Don't send key updates from the sender to the sender
|
||||
.ready_filter(|user_id| sender_user != user_id)
|
||||
// Only send keys if the sender doesn't share an encrypted room with the target
|
||||
// already
|
||||
.filter_map(|user_id| {
|
||||
share_encrypted_room(&services, sender_user, user_id, Some(room_id))
|
||||
.map(|res| res.or_some(user_id.to_owned()))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.await,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Look for device list updates in this room
|
||||
device_list_changes.extend(
|
||||
services
|
||||
.users
|
||||
.room_keys_changed(room_id, globalsince, None)
|
||||
.map(|(user_id, _)| user_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await,
|
||||
);
|
||||
}
|
||||
|
||||
for user_id in left_encrypted_users {
|
||||
let dont_share_encrypted_room = !share_encrypted_room(&services, sender_user, &user_id, None).await;
|
||||
|
||||
// If the user doesn't share an encrypted room with the target anymore, we need
|
||||
// to tell them
|
||||
if dont_share_encrypted_room {
|
||||
device_list_left.insert(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut lists = BTreeMap::new();
|
||||
let mut todo_rooms = BTreeMap::new(); // and required state
|
||||
|
||||
for (list_id, list) in &body.lists {
|
||||
let active_rooms = match list.filters.clone().and_then(|f| f.is_invite) {
|
||||
Some(true) => &all_invited_rooms,
|
||||
Some(false) => &all_joined_rooms,
|
||||
None => &all_rooms,
|
||||
};
|
||||
|
||||
let active_rooms = match list.filters.clone().map(|f| f.not_room_types) {
|
||||
Some(filter) if filter.is_empty() => active_rooms.clone(),
|
||||
Some(value) => filter_rooms(&services, active_rooms, &value, true).await,
|
||||
None => active_rooms.clone(),
|
||||
};
|
||||
|
||||
let active_rooms = match list.filters.clone().map(|f| f.room_types) {
|
||||
Some(filter) if filter.is_empty() => active_rooms.clone(),
|
||||
Some(value) => filter_rooms(&services, &active_rooms, &value, false).await,
|
||||
None => active_rooms,
|
||||
};
|
||||
|
||||
let mut new_known_rooms = BTreeSet::new();
|
||||
|
||||
let ranges = list.ranges.clone();
|
||||
lists.insert(
|
||||
list_id.clone(),
|
||||
sync_events::v4::SyncList {
|
||||
ops: ranges
|
||||
.into_iter()
|
||||
.map(|mut r| {
|
||||
r.0 = r.0.clamp(
|
||||
uint!(0),
|
||||
UInt::try_from(active_rooms.len().saturating_sub(1)).unwrap_or(UInt::MAX),
|
||||
);
|
||||
r.1 =
|
||||
r.1.clamp(r.0, UInt::try_from(active_rooms.len().saturating_sub(1)).unwrap_or(UInt::MAX));
|
||||
|
||||
let room_ids = if !active_rooms.is_empty() {
|
||||
active_rooms[usize_from_ruma(r.0)..=usize_from_ruma(r.1)].to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
new_known_rooms.extend(room_ids.iter().cloned());
|
||||
for room_id in &room_ids {
|
||||
let todo_room =
|
||||
todo_rooms
|
||||
.entry(room_id.clone())
|
||||
.or_insert((BTreeSet::new(), 0_usize, u64::MAX));
|
||||
|
||||
let limit: usize = list
|
||||
.room_details
|
||||
.timeline_limit
|
||||
.map(u64::from)
|
||||
.map_or(10, usize_from_u64_truncated)
|
||||
.min(100);
|
||||
|
||||
todo_room
|
||||
.0
|
||||
.extend(list.room_details.required_state.iter().cloned());
|
||||
|
||||
todo_room.1 = todo_room.1.max(limit);
|
||||
// 0 means unknown because it got out of date
|
||||
todo_room.2 = todo_room.2.min(
|
||||
known_rooms
|
||||
.get(list_id.as_str())
|
||||
.and_then(|k| k.get(room_id))
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
);
|
||||
}
|
||||
sync_events::v4::SyncOp {
|
||||
op: SlidingOp::Sync,
|
||||
range: Some(r),
|
||||
index: None,
|
||||
room_ids,
|
||||
room_id: None,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
count: ruma_from_usize(active_rooms.len()),
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(conn_id) = &body.conn_id {
|
||||
services.sync.update_sync_known_rooms(
|
||||
sender_user.clone(),
|
||||
sender_device.clone(),
|
||||
conn_id.clone(),
|
||||
list_id.clone(),
|
||||
new_known_rooms,
|
||||
globalsince,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut known_subscription_rooms = BTreeSet::new();
|
||||
for (room_id, room) in &body.room_subscriptions {
|
||||
if !services.rooms.metadata.exists(room_id).await {
|
||||
continue;
|
||||
}
|
||||
let todo_room = todo_rooms
|
||||
.entry(room_id.clone())
|
||||
.or_insert((BTreeSet::new(), 0_usize, u64::MAX));
|
||||
|
||||
let limit: usize = room
|
||||
.timeline_limit
|
||||
.map(u64::from)
|
||||
.map_or(10, usize_from_u64_truncated)
|
||||
.min(100);
|
||||
|
||||
todo_room.0.extend(room.required_state.iter().cloned());
|
||||
todo_room.1 = todo_room.1.max(limit);
|
||||
// 0 means unknown because it got out of date
|
||||
todo_room.2 = todo_room.2.min(
|
||||
known_rooms
|
||||
.get("subscriptions")
|
||||
.and_then(|k| k.get(room_id))
|
||||
.copied()
|
||||
.unwrap_or(0),
|
||||
);
|
||||
known_subscription_rooms.insert(room_id.clone());
|
||||
}
|
||||
|
||||
for r in body.unsubscribe_rooms {
|
||||
known_subscription_rooms.remove(&r);
|
||||
body.room_subscriptions.remove(&r);
|
||||
}
|
||||
|
||||
if let Some(conn_id) = &body.conn_id {
|
||||
services.sync.update_sync_known_rooms(
|
||||
sender_user.clone(),
|
||||
sender_device.clone(),
|
||||
conn_id.clone(),
|
||||
"subscriptions".to_owned(),
|
||||
known_subscription_rooms,
|
||||
globalsince,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(conn_id) = &body.conn_id {
|
||||
services.sync.update_sync_subscriptions(
|
||||
sender_user.clone(),
|
||||
sender_device.clone(),
|
||||
conn_id.clone(),
|
||||
body.room_subscriptions,
|
||||
);
|
||||
}
|
||||
|
||||
let mut rooms = BTreeMap::new();
|
||||
for (room_id, (required_state_request, timeline_limit, roomsince)) in &todo_rooms {
|
||||
let roomsincecount = PduCount::Normal(*roomsince);
|
||||
|
||||
let mut timestamp: Option<_> = None;
|
||||
let mut invite_state = None;
|
||||
let (timeline_pdus, limited);
|
||||
if all_invited_rooms.contains(room_id) {
|
||||
// TODO: figure out a timestamp we can use for remote invites
|
||||
invite_state = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.invite_state(sender_user, room_id)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
(timeline_pdus, limited) = (Vec::new(), true);
|
||||
} else {
|
||||
(timeline_pdus, limited) =
|
||||
match load_timeline(&services, sender_user, room_id, roomsincecount, None, *timeline_limit).await {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
warn!("Encountered missing timeline in {}, error {}", room_id, err);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
account_data.rooms.insert(
|
||||
room_id.clone(),
|
||||
services
|
||||
.account_data
|
||||
.changes_since(Some(room_id), sender_user, *roomsince)
|
||||
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
|
||||
.collect()
|
||||
.await,
|
||||
);
|
||||
|
||||
let vector: Vec<_> = services
|
||||
.rooms
|
||||
.read_receipt
|
||||
.readreceipts_since(room_id, *roomsince)
|
||||
.filter_map(|(read_user, ts, v)| async move {
|
||||
services
|
||||
.users
|
||||
.user_is_ignored(read_user, sender_user)
|
||||
.await
|
||||
.or_some((read_user.to_owned(), ts, v))
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let receipt_size = vector.len();
|
||||
receipts
|
||||
.rooms
|
||||
.insert(room_id.clone(), pack_receipts(Box::new(vector.into_iter())));
|
||||
|
||||
if roomsince != &0
|
||||
&& timeline_pdus.is_empty()
|
||||
&& account_data.rooms.get(room_id).is_some_and(Vec::is_empty)
|
||||
&& receipt_size == 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let prev_batch = timeline_pdus
|
||||
.first()
|
||||
.map_or(Ok::<_, Error>(None), |(pdu_count, _)| {
|
||||
Ok(Some(match pdu_count {
|
||||
PduCount::Backfilled(_) => {
|
||||
error!("timeline in backfill state?!");
|
||||
"0".to_owned()
|
||||
},
|
||||
PduCount::Normal(c) => c.to_string(),
|
||||
}))
|
||||
})?
|
||||
.or_else(|| {
|
||||
if roomsince != &0 {
|
||||
Some(roomsince.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let room_events: Vec<_> = timeline_pdus
|
||||
.iter()
|
||||
.stream()
|
||||
.filter_map(|item| ignored_filter(&services, item.clone(), sender_user))
|
||||
.map(|(_, pdu)| pdu.to_sync_room_event())
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
for (_, pdu) in timeline_pdus {
|
||||
let ts = MilliSecondsSinceUnixEpoch(pdu.origin_server_ts);
|
||||
if DEFAULT_BUMP_TYPES.contains(pdu.event_type()) && timestamp.is_none_or(|time| time <= ts) {
|
||||
timestamp = Some(ts);
|
||||
}
|
||||
}
|
||||
|
||||
let required_state = required_state_request
|
||||
.iter()
|
||||
.stream()
|
||||
.filter_map(|state| async move {
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &state.0, &state.1)
|
||||
.await
|
||||
.map(|s| s.to_sync_state_event())
|
||||
.ok()
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
// Heroes
|
||||
let heroes: Vec<_> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(room_id)
|
||||
.ready_filter(|member| member != sender_user)
|
||||
.filter_map(|user_id| {
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_member(room_id, user_id)
|
||||
.map_ok(|memberevent| SlidingSyncRoomHero {
|
||||
user_id: user_id.into(),
|
||||
name: memberevent.displayname,
|
||||
avatar: memberevent.avatar_url,
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.take(5)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let name = match heroes.len().cmp(&(1_usize)) {
|
||||
Ordering::Greater => {
|
||||
let firsts = heroes[1..]
|
||||
.iter()
|
||||
.map(|h| h.name.clone().unwrap_or_else(|| h.user_id.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let last = heroes[0]
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| heroes[0].user_id.to_string());
|
||||
|
||||
Some(format!("{firsts} and {last}"))
|
||||
},
|
||||
Ordering::Equal => Some(
|
||||
heroes[0]
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| heroes[0].user_id.to_string()),
|
||||
),
|
||||
Ordering::Less => None,
|
||||
};
|
||||
|
||||
let heroes_avatar = if heroes.len() == 1 {
|
||||
heroes[0].avatar.clone()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
rooms.insert(
|
||||
room_id.clone(),
|
||||
sync_events::v4::SlidingSyncRoom {
|
||||
name: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_name(room_id)
|
||||
.await
|
||||
.ok()
|
||||
.or(name),
|
||||
avatar: if let Some(heroes_avatar) = heroes_avatar {
|
||||
ruma::JsOption::Some(heroes_avatar)
|
||||
} else {
|
||||
match services.rooms.state_accessor.get_avatar(room_id).await {
|
||||
ruma::JsOption::Some(avatar) => ruma::JsOption::from_option(avatar.url),
|
||||
ruma::JsOption::Null => ruma::JsOption::Null,
|
||||
ruma::JsOption::Undefined => ruma::JsOption::Undefined,
|
||||
}
|
||||
},
|
||||
initial: Some(roomsince == &0),
|
||||
is_dm: None,
|
||||
invite_state,
|
||||
unread_notifications: UnreadNotificationsCount {
|
||||
highlight_count: Some(
|
||||
services
|
||||
.rooms
|
||||
.user
|
||||
.highlight_count(sender_user, room_id)
|
||||
.await
|
||||
.try_into()
|
||||
.expect("notification count can't go that high"),
|
||||
),
|
||||
notification_count: Some(
|
||||
services
|
||||
.rooms
|
||||
.user
|
||||
.notification_count(sender_user, room_id)
|
||||
.await
|
||||
.try_into()
|
||||
.expect("notification count can't go that high"),
|
||||
),
|
||||
},
|
||||
timeline: room_events,
|
||||
required_state,
|
||||
prev_batch,
|
||||
limited,
|
||||
joined_count: Some(
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(room_id)
|
||||
.await
|
||||
.unwrap_or(0)
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| uint!(0)),
|
||||
),
|
||||
invited_count: Some(
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_invited_count(room_id)
|
||||
.await
|
||||
.unwrap_or(0)
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| uint!(0)),
|
||||
),
|
||||
num_live: None, // Count events in timeline greater than global sync counter
|
||||
timestamp,
|
||||
heroes: Some(heroes),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if rooms
|
||||
.iter()
|
||||
.all(|(_, r)| r.timeline.is_empty() && r.required_state.is_empty())
|
||||
{
|
||||
// Hang a few seconds so requests are not spammed
|
||||
// Stop hanging if new info arrives
|
||||
let default = Duration::from_secs(30);
|
||||
let duration = cmp::min(body.timeout.unwrap_or(default), default);
|
||||
_ = tokio::time::timeout(duration, watcher).await;
|
||||
}
|
||||
|
||||
Ok(sync_events::v4::Response {
|
||||
initial: globalsince == 0,
|
||||
txn_id: body.txn_id.clone(),
|
||||
pos: next_batch.to_string(),
|
||||
lists,
|
||||
rooms,
|
||||
extensions: sync_events::v4::Extensions {
|
||||
to_device: if body.extensions.to_device.enabled.unwrap_or(false) {
|
||||
Some(sync_events::v4::ToDevice {
|
||||
events: services
|
||||
.users
|
||||
.get_to_device_events(sender_user, &sender_device)
|
||||
.collect()
|
||||
.await,
|
||||
next_batch: next_batch.to_string(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
e2ee: sync_events::v4::E2EE {
|
||||
device_lists: DeviceLists {
|
||||
changed: device_list_changes.into_iter().collect(),
|
||||
left: device_list_left.into_iter().collect(),
|
||||
},
|
||||
device_one_time_keys_count: services
|
||||
.users
|
||||
.count_one_time_keys(sender_user, &sender_device)
|
||||
.await,
|
||||
// Fallback keys are not yet supported
|
||||
device_unused_fallback_key_types: None,
|
||||
},
|
||||
account_data,
|
||||
receipts,
|
||||
typing: sync_events::v4::Typing {
|
||||
rooms: BTreeMap::new(),
|
||||
},
|
||||
},
|
||||
delta_token: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn filter_rooms(
|
||||
services: &Services, rooms: &[OwnedRoomId], filter: &[RoomTypeFilter], negate: bool,
|
||||
) -> Vec<OwnedRoomId> {
|
||||
rooms
|
||||
.iter()
|
||||
.stream()
|
||||
.filter_map(|r| async move {
|
||||
let room_type = services.rooms.state_accessor.get_room_type(r).await;
|
||||
|
||||
if room_type.as_ref().is_err_and(|e| !e.is_not_found()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let room_type_filter = RoomTypeFilter::from(room_type.ok());
|
||||
|
||||
let include = if negate {
|
||||
!filter.contains(&room_type_filter)
|
||||
} else {
|
||||
filter.is_empty() || filter.contains(&room_type_filter)
|
||||
};
|
||||
|
||||
include.then_some(r.to_owned())
|
||||
})
|
||||
.collect()
|
||||
.await
|
||||
}
|
||||
+43
-52
@@ -9,7 +9,7 @@ use ruma::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{Error, Result, Ruma};
|
||||
use crate::{Result, Ruma};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}`
|
||||
///
|
||||
@@ -21,32 +21,30 @@ pub(crate) async fn update_tag_route(
|
||||
) -> Result<create_tag::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services
|
||||
let mut tags_event = services
|
||||
.account_data
|
||||
.get(Some(&body.room_id), sender_user, RoomAccountDataEventType::Tag)?;
|
||||
|
||||
let mut tags_event = event.map_or_else(
|
||||
|| {
|
||||
Ok(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
})
|
||||
},
|
||||
|e| serde_json::from_str(e.get()).map_err(|_| Error::bad_database("Invalid account data event in db.")),
|
||||
)?;
|
||||
.get_room(&body.room_id, sender_user, RoomAccountDataEventType::Tag)
|
||||
.await
|
||||
.unwrap_or(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
});
|
||||
|
||||
tags_event
|
||||
.content
|
||||
.tags
|
||||
.insert(body.tag.clone().into(), body.tag_info.clone());
|
||||
|
||||
services.account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)?;
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(create_tag::v3::Response {})
|
||||
}
|
||||
@@ -61,29 +59,27 @@ pub(crate) async fn delete_tag_route(
|
||||
) -> Result<delete_tag::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services
|
||||
let mut tags_event = services
|
||||
.account_data
|
||||
.get(Some(&body.room_id), sender_user, RoomAccountDataEventType::Tag)?;
|
||||
|
||||
let mut tags_event = event.map_or_else(
|
||||
|| {
|
||||
Ok(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
})
|
||||
},
|
||||
|e| serde_json::from_str(e.get()).map_err(|_| Error::bad_database("Invalid account data event in db.")),
|
||||
)?;
|
||||
.get_room(&body.room_id, sender_user, RoomAccountDataEventType::Tag)
|
||||
.await
|
||||
.unwrap_or(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
});
|
||||
|
||||
tags_event.content.tags.remove(&body.tag.clone().into());
|
||||
|
||||
services.account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)?;
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(delete_tag::v3::Response {})
|
||||
}
|
||||
@@ -98,20 +94,15 @@ pub(crate) async fn get_tags_route(
|
||||
) -> Result<get_tags::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services
|
||||
let tags_event = services
|
||||
.account_data
|
||||
.get(Some(&body.room_id), sender_user, RoomAccountDataEventType::Tag)?;
|
||||
|
||||
let tags_event = event.map_or_else(
|
||||
|| {
|
||||
Ok(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
})
|
||||
},
|
||||
|e| serde_json::from_str(e.get()).map_err(|_| Error::bad_database("Invalid account data event in db.")),
|
||||
)?;
|
||||
.get_room(&body.room_id, sender_user, RoomAccountDataEventType::Tag)
|
||||
.await
|
||||
.unwrap_or(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
});
|
||||
|
||||
Ok(get_tags::v3::Response {
|
||||
tags: tags_event.content.tags,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user