Compare commits

..

2 Commits

Author SHA1 Message Date
strawberry 00221db8fe dedupe auto_room_join to one function
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-06 15:00:17 -04:00
strawberry 65b3bc7c48 chore: bump ruma and deps
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-06 14:54:46 -04:00
355 changed files with 18236 additions and 24883 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
dotenv_if_exists use flake
use flake ".#${DIRENV_DEVSHELL:-default}"
PATH_add bin PATH_add bin
dotenv_if_exists
-264
View File
@@ -1,264 +0,0 @@
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:
- main
tags:
- '*'
# Allows you to run this workflow manually from the Actions tab
#workflow_dispatch:
#concurrency:
# group: ${{ gitea.head_ref || gitea.ref_name }}
# cancel-in-progress: true
env:
# Required to make some things output color
TERM: ansi
# Publishing to my nix binary cache
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
# conduwuit.cachix.org
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
# Just in case incremental is still being set to true, speeds up CI
CARGO_INCREMENTAL: 0
# Custom nix binary cache if fork is being used
ATTIC_ENDPOINT: ${{ vars.ATTIC_ENDPOINT }}
ATTIC_PUBLIC_KEY: ${{ vars.ATTIC_PUBLIC_KEY }}
# Get error output from nix that we can actually use
NIX_CONFIG: show-trace = true
#permissions:
# packages: write
# contents: read
jobs:
tests:
name: Test
runs-on: ubuntu-latest
steps:
- name: Sync repository
uses: https://github.com/https://github.com/actions/checkout@v4
- name: Tag comparison check
if: startsWith(gitea.ref, 'refs/tags/v')
run: |
# Tag mismatch with latest repo tag check to prevent potential downgrades
LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)
if [ $LATEST_TAG != ${{ gitea.ref_name }} ]; then
echo '# WARNING: Attempting to run this workflow for a tag that is not the latest repo tag. Aborting.'
echo '# WARNING: Attempting to run this workflow for a tag that is not the latest repo tag. Aborting.' >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Install Nix
uses: https://github.com/DeterminateSystems/nix-installer-action@main
with:
diagnostic-endpoint: ""
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
- name: Enable Cachix binary cache
run: |
nix profile install nixpkgs#cachix
cachix use crane
cachix use nix-community
- name: Configure Magic Nix Cache
uses: https://github.com/DeterminateSystems/magic-nix-cache-action@main
with:
diagnostic-endpoint: ""
upstream-cache: "https://attic.kennel.juneis.dog/conduwuit"
- name: Apply Nix binary cache configuration
run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit https://cache.lix.systems https://conduwuit.cachix.org
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o= conduwuit.cachix.org-1:MFRm6jcnfTf0jSAbmvLfhO3KBMt4px+1xaereWXp8Xg=
EOF
- name: Use alternative Nix binary caches if specified
if: ${{ (env.ATTIC_ENDPOINT != '') && (env.ATTIC_PUBLIC_KEY != '') }}
run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF
extra-substituters = ${{ env.ATTIC_ENDPOINT }}
extra-trusted-public-keys = ${{ env.ATTIC_PUBLIC_KEY }}
EOF
- name: Prepare build environment
run: |
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc"
nix profile install --impure --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv
direnv allow
nix develop .#all-features --command true
- name: Cache CI dependencies
run: |
bin/nix-build-and-cache ci
- name: Run CI tests
run: |
direnv exec . engage > >(tee -a test_output.log)
- name: Sync Complement repository
uses: https://github.com/actions/checkout@v4
with:
repository: 'matrix-org/complement'
path: complement_src
- name: Run Complement tests
run: |
direnv exec . bin/complement 'complement_src' 'complement_test_logs.jsonl' 'complement_test_results.jsonl'
cp -v -f result complement_oci_image.tar.gz
- name: Upload Complement OCI image
uses: https://github.com/actions/upload-artifact@v4
with:
name: complement_oci_image.tar.gz
path: complement_oci_image.tar.gz
if-no-files-found: error
- name: Upload Complement logs
uses: https://github.com/actions/upload-artifact@v4
with:
name: complement_test_logs.jsonl
path: complement_test_logs.jsonl
if-no-files-found: error
- name: Upload Complement results
uses: https://github.com/actions/upload-artifact@v4
with:
name: complement_test_results.jsonl
path: complement_test_results.jsonl
if-no-files-found: error
- name: Diff Complement results with checked-in repo results
run: |
diff -u --color=always tests/test_results/complement/test_results.jsonl complement_test_results.jsonl > >(tee -a complement_test_output.log)
echo '# Complement diff results' >> $GITHUB_STEP_SUMMARY
echo '```diff' >> $GITHUB_STEP_SUMMARY
tail -n 100 complement_test_output.log | sed 's/\x1b\[[0-9;]*m//g' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
- name: Update Job Summary
if: success() || failure()
run: |
if [ ${{ job.status }} == 'success' ]; then
echo '# ✅ completed suwuccessfully' >> $GITHUB_STEP_SUMMARY
else
echo '```' >> $GITHUB_STEP_SUMMARY
tail -n 40 test_output.log | sed 's/\x1b\[[0-9;]*m//g' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
build:
name: Build
runs-on: ubuntu-latest
needs: tests
strategy:
matrix:
include:
- target: aarch64-unknown-linux-musl
- target: x86_64-unknown-linux-musl
steps:
- name: Sync repository
uses: https://github.com/actions/checkout@v4
- name: Install Nix
uses: https://github.com/DeterminateSystems/nix-installer-action@main
with:
diagnostic-endpoint: ""
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
- name: Install and enable Cachix binary cache
run: |
nix profile install nixpkgs#cachix
cachix use crane
cachix use nix-community
- name: Configure Magic Nix Cache
uses: https://github.com/DeterminateSystems/magic-nix-cache-action@main
with:
diagnostic-endpoint: ""
upstream-cache: "https://attic.kennel.juneis.dog/conduwuit"
- name: Apply Nix binary cache configuration
run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit https://cache.lix.systems https://conduwuit.cachix.org
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o= conduwuit.cachix.org-1:MFRm6jcnfTf0jSAbmvLfhO3KBMt4px+1xaereWXp8Xg=
EOF
- name: Use alternative Nix binary caches if specified
if: ${{ (env.ATTIC_ENDPOINT != '') && (env.ATTIC_PUBLIC_KEY != '') }}
run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF
extra-substituters = ${{ env.ATTIC_ENDPOINT }}
extra-trusted-public-keys = ${{ env.ATTIC_PUBLIC_KEY }}
EOF
- name: Prepare build environment
run: |
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc"
nix profile install --impure --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv
direnv allow
nix develop .#all-features --command true
- name: Build static ${{ matrix.target }}
run: |
CARGO_DEB_TARGET_TUPLE=$(echo ${{ matrix.target }} | grep -o -E '^([^-]*-){3}[^-]*')
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
bin/nix-build-and-cache just .#static-${{ matrix.target }}
mkdir -v -p target/release/
mkdir -v -p target/$CARGO_DEB_TARGET_TUPLE/release/
cp -v -f result/bin/conduit target/release/conduwuit
cp -v -f result/bin/conduit target/$CARGO_DEB_TARGET_TUPLE/release/conduwuit
# -p conduit is the main crate name
direnv exec . cargo deb --verbose --no-build --no-strip -p conduit --target=$CARGO_DEB_TARGET_TUPLE --output target/release/${{ matrix.target }}.deb
mv -v target/release/conduwuit static-${{ matrix.target }}
mv -v target/release/${{ matrix.target }}.deb ${{ matrix.target }}.deb
- name: Upload static-${{ matrix.target }}
uses: https://github.com/actions/upload-artifact@v4
with:
name: static-${{ matrix.target }}
path: static-${{ matrix.target }}
if-no-files-found: error
- name: Upload deb ${{ matrix.target }}
uses: https://github.com/actions/upload-artifact@v4
with:
name: deb-${{ matrix.target }}
path: ${{ matrix.target }}.deb
if-no-files-found: error
compression-level: 0
- name: Build OCI image ${{ matrix.target }}
run: |
bin/nix-build-and-cache just .#oci-image-${{ matrix.target }}
cp -v -f result oci-image-${{ matrix.target }}.tar.gz
- name: Upload OCI image ${{ matrix.target }}
uses: https://github.com/actions/upload-artifact@v4
with:
name: oci-image-${{ matrix.target }}
path: oci-image-${{ matrix.target }}.tar.gz
if-no-files-found: error
compression-level: 0
+38 -70
View File
@@ -30,15 +30,11 @@ env:
TERM: ansi TERM: ansi
# Publishing to my nix binary cache # Publishing to my nix binary cache
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }} ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
# conduwuit.cachix.org
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
# Just in case incremental is still being set to true, speeds up CI # Just in case incremental is still being set to true, speeds up CI
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
# Custom nix binary cache if fork is being used # Custom nix binary cache if fork is being used
ATTIC_ENDPOINT: ${{ vars.ATTIC_ENDPOINT }} ATTIC_ENDPOINT: ${{ vars.ATTIC_ENDPOINT }}
ATTIC_PUBLIC_KEY: ${{ vars.ATTIC_PUBLIC_KEY }} ATTIC_PUBLIC_KEY: ${{ vars.ATTIC_PUBLIC_KEY }}
# Get error output from nix that we can actually use
NIX_CONFIG: show-trace = true
permissions: permissions:
packages: write packages: write
@@ -53,7 +49,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Tag comparison check - name: Tag comparison check
if: startsWith(github.ref, 'refs/tags/v') if: startsWith('refs/tags/v', github.ref)
run: | run: |
# Tag mismatch with latest repo tag check to prevent potential downgrades # Tag mismatch with latest repo tag check to prevent potential downgrades
LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`) LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)
@@ -66,11 +62,6 @@ jobs:
- name: Install Nix - name: Install Nix
uses: DeterminateSystems/nix-installer-action@main uses: DeterminateSystems/nix-installer-action@main
with:
diagnostic-endpoint: ""
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
- name: Enable Cachix binary cache - name: Enable Cachix binary cache
run: | run: |
@@ -80,15 +71,12 @@ jobs:
- name: Configure Magic Nix Cache - name: Configure Magic Nix Cache
uses: DeterminateSystems/magic-nix-cache-action@main uses: DeterminateSystems/magic-nix-cache-action@main
with:
diagnostic-endpoint: ""
upstream-cache: "https://attic.kennel.juneis.dog/conduwuit"
- name: Apply Nix binary cache configuration - name: Apply Nix binary cache configuration
run: | run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit https://cache.lix.systems https://conduwuit.cachix.org extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o= conduwuit.cachix.org-1:MFRm6jcnfTf0jSAbmvLfhO3KBMt4px+1xaereWXp8Xg= extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg= conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=
EOF EOF
- name: Use alternative Nix binary caches if specified - name: Use alternative Nix binary caches if specified
@@ -104,11 +92,7 @@ jobs:
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc" echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc"
nix profile install --impure --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv nix profile install --impure --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv
direnv allow direnv allow
nix develop .#all-features --command true nix develop --command true
- name: Cache CI dependencies
run: |
bin/nix-build-and-cache ci
- name: Run CI tests - name: Run CI tests
run: | run: |
@@ -123,14 +107,6 @@ jobs:
- name: Run Complement tests - name: Run Complement tests
run: | run: |
direnv exec . bin/complement 'complement_src' 'complement_test_logs.jsonl' 'complement_test_results.jsonl' direnv exec . bin/complement 'complement_src' 'complement_test_logs.jsonl' 'complement_test_results.jsonl'
cp -v -f result complement_oci_image.tar.gz
- name: Upload Complement OCI image
uses: actions/upload-artifact@v4
with:
name: complement_oci_image.tar.gz
path: complement_oci_image.tar.gz
if-no-files-found: error
- name: Upload Complement logs - name: Upload Complement logs
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -147,11 +123,16 @@ jobs:
if-no-files-found: error if-no-files-found: error
- name: Diff Complement results with checked-in repo results - name: Diff Complement results with checked-in repo results
# TODO: figure out why our complement results are not 100% consistent so we don't need to allow failures
continue-on-error: true
run: |
diff -u --color=always complement_test_results.jsonl tests/test_results/complement/test_results.jsonl > >(tee -a complement_test_output.log)
- name: Add Complement diff result to Job Summary
run: | run: |
diff -u --color=always tests/test_results/complement/test_results.jsonl complement_test_results.jsonl > >(tee -a complement_test_output.log)
echo '# Complement diff results' >> $GITHUB_STEP_SUMMARY echo '# Complement diff results' >> $GITHUB_STEP_SUMMARY
echo '```diff' >> $GITHUB_STEP_SUMMARY echo '```diff' >> $GITHUB_STEP_SUMMARY
tail -n 100 complement_test_output.log | sed 's/\x1b\[[0-9;]*m//g' >> $GITHUB_STEP_SUMMARY tail -n 50 complement_test_output.log | sed 's/\x1b\[[0-9;]*m//g' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY
- name: Update Job Summary - name: Update Job Summary
@@ -161,7 +142,7 @@ jobs:
echo '# ✅ completed suwuccessfully' >> $GITHUB_STEP_SUMMARY echo '# ✅ completed suwuccessfully' >> $GITHUB_STEP_SUMMARY
else else
echo '```' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY
tail -n 40 test_output.log | sed 's/\x1b\[[0-9;]*m//g' >> $GITHUB_STEP_SUMMARY tail -n 20 test_output.log | sed 's/\x1b\[[0-9;]*m//g' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY
fi fi
@@ -169,22 +150,20 @@ jobs:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: tests needs: tests
if: startsWith('refs/tags/v', github.ref) || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)
strategy: strategy:
matrix: matrix:
include: include:
- target: aarch64-unknown-linux-musl - target: aarch64-unknown-linux-musl
- target: aarch64-unknown-linux-musl-jemalloc
- target: x86_64-unknown-linux-musl - target: x86_64-unknown-linux-musl
- target: x86_64-unknown-linux-musl-jemalloc
steps: steps:
- name: Sync repository - name: Sync repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Nix - name: Install Nix
uses: DeterminateSystems/nix-installer-action@main uses: DeterminateSystems/nix-installer-action@main
with:
diagnostic-endpoint: ""
extra-conf: |
experimental-features = nix-command flakes
accept-flake-config = true
- name: Install and enable Cachix binary cache - name: Install and enable Cachix binary cache
run: | run: |
@@ -194,15 +173,12 @@ jobs:
- name: Configure Magic Nix Cache - name: Configure Magic Nix Cache
uses: DeterminateSystems/magic-nix-cache-action@main uses: DeterminateSystems/magic-nix-cache-action@main
with:
diagnostic-endpoint: ""
upstream-cache: "https://attic.kennel.juneis.dog/conduwuit"
- name: Apply Nix binary cache configuration - name: Apply Nix binary cache configuration
run: | run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit https://cache.lix.systems https://conduwuit.cachix.org extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o= conduwuit.cachix.org-1:MFRm6jcnfTf0jSAbmvLfhO3KBMt4px+1xaereWXp8Xg= extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg= conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=
EOF EOF
- name: Use alternative Nix binary caches if specified - name: Use alternative Nix binary caches if specified
@@ -218,22 +194,15 @@ jobs:
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc" echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc"
nix profile install --impure --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv nix profile install --impure --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv
direnv allow direnv allow
nix develop .#all-features --command true nix develop --command true
- name: Build static ${{ matrix.target }} - name: Build static ${{ matrix.target }}
run: | run: |
CARGO_DEB_TARGET_TUPLE=$(echo ${{ matrix.target }} | grep -o -E '^([^-]*-){3}[^-]*')
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
bin/nix-build-and-cache just .#static-${{ matrix.target }} bin/nix-build-and-cache just .#static-${{ matrix.target }}
mkdir -v -p target/release/ mkdir -p target/release
mkdir -v -p target/$CARGO_DEB_TARGET_TUPLE/release/ cp -v -f result/bin/conduit target/release/
cp -v -f result/bin/conduit target/release/conduwuit direnv exec . cargo deb --no-build --no-strip --output target/debian/${{ matrix.target }}.deb
cp -v -f result/bin/conduit target/$CARGO_DEB_TARGET_TUPLE/release/conduwuit mv target/release/conduit static-${{ matrix.target }}
# -p conduit is the main crate name
direnv exec . cargo deb --verbose --no-build --no-strip -p conduit --target=$CARGO_DEB_TARGET_TUPLE --output target/release/${{ matrix.target }}.deb
mv -v target/release/conduwuit static-${{ matrix.target }}
mv -v target/release/${{ matrix.target }}.deb ${{ matrix.target }}.deb
- name: Upload static-${{ matrix.target }} - name: Upload static-${{ matrix.target }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -246,9 +215,8 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: deb-${{ matrix.target }} name: deb-${{ matrix.target }}
path: ${{ matrix.target }}.deb path: target/debian/${{ matrix.target }}.deb
if-no-files-found: error if-no-files-found: error
compression-level: 0
- name: Build OCI image ${{ matrix.target }} - name: Build OCI image ${{ matrix.target }}
run: | run: |
@@ -267,20 +235,20 @@ jobs:
name: Docker publish name: Docker publish
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build 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' if: startsWith('refs/tags/v', github.ref) || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)
env: env:
DOCKER_ARM64: docker.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}-${{ github.sha }}-arm64v8 DOCKER_ARM64: docker.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}-${{ github.sha }}-arm64v8
DOCKER_AMD64: docker.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}-${{ github.sha }}-amd64 DOCKER_AMD64: docker.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}-${{ github.sha }}-amd64
DOCKER_TAG: docker.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}-${{ github.sha }} DOCKER_TAG: docker.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}-${{ github.sha }}
DOCKER_BRANCH: docker.io/${{ github.repository }}:${{ (startsWith(github.ref, 'refs/tags/v') && 'latest') || (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }} DOCKER_BRANCH: docker.io/${{ github.repository }}:${{ (startsWith('refs/tags/v', github.ref) && 'latest') || (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}
GHCR_ARM64: ghcr.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}-${{ github.sha }}-arm64v8 GHCR_ARM64: ghcr.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}-${{ github.sha }}-arm64v8
GHCR_AMD64: ghcr.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}-${{ github.sha }}-amd64 GHCR_AMD64: ghcr.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}-${{ github.sha }}-amd64
GHCR_TAG: ghcr.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}-${{ github.sha }} GHCR_TAG: ghcr.io/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}-${{ github.sha }}
GHCR_BRANCH: ghcr.io/${{ github.repository }}:${{ (startsWith(github.ref, 'refs/tags/v') && 'latest') || (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }} GHCR_BRANCH: ghcr.io/${{ github.repository }}:${{ (startsWith('refs/tags/v', github.ref) && 'latest') || (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}
GLCR_ARM64: registry.gitlab.com/conduwuit/conduwuit:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}-${{ github.sha }}-arm64v8 GLCR_ARM64: registry.gitlab.com/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}-${{ github.sha }}-arm64v8
GLCR_AMD64: registry.gitlab.com/conduwuit/conduwuit:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}-${{ github.sha }}-amd64 GLCR_AMD64: registry.gitlab.com/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}-${{ github.sha }}-amd64
GLCR_TAG: registry.gitlab.com/conduwuit/conduwuit:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}-${{ github.sha }} GLCR_TAG: registry.gitlab.com/${{ github.repository }}:${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}-${{ github.sha }}
GLCR_BRANCH: registry.gitlab.com/conduwuit/conduwuit:${{ (startsWith(github.ref, 'refs/tags/v') && 'latest') || (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }} GLCR_BRANCH: registry.gitlab.com/${{ github.repository }}:${{ (startsWith('refs/tags/v', github.ref) && 'latest') || (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.head_ref)) || github.ref_name }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }} GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
@@ -313,8 +281,8 @@ jobs:
- name: Move OCI images into position - name: Move OCI images into position
run: | run: |
mv -v oci-image-x86_64-*/*.tar.gz oci-image-amd64.tar.gz mv oci-image-x86_64-*-jemalloc/*.tar.gz oci-image-amd64.tar.gz
mv -v oci-image-aarch64-*/*.tar.gz oci-image-arm64v8.tar.gz mv oci-image-aarch64-*-jemalloc/*.tar.gz oci-image-arm64v8.tar.gz
- name: Load and push amd64 image - name: Load and push amd64 image
if: ${{ (vars.DOCKER_USERNAME != '') && (env.DOCKERHUB_TOKEN != '') }} if: ${{ (vars.DOCKER_USERNAME != '') && (env.DOCKERHUB_TOKEN != '') }}
+33 -38
View File
@@ -16,13 +16,9 @@ env:
TERM: ansi TERM: ansi
# Publishing to my nix binary cache # Publishing to my nix binary cache
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }} ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
# conduwuit.cachix.org
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
# Custom nix binary cache if fork is being used # Custom nix binary cache if fork is being used
ATTIC_ENDPOINT: ${{ vars.ATTIC_ENDPOINT }} ATTIC_ENDPOINT: ${{ vars.ATTIC_ENDPOINT }}
ATTIC_PUBLIC_KEY: ${{ vars.ATTIC_PUBLIC_KEY }} ATTIC_PUBLIC_KEY: ${{ vars.ATTIC_PUBLIC_KEY }}
# Get error output from nix that we can actually use
NIX_CONFIG: show-trace = true
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
@@ -33,6 +29,7 @@ concurrency:
jobs: jobs:
docs: docs:
name: Documentation and GitHub Pages name: Documentation and GitHub Pages
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -51,51 +48,49 @@ jobs:
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: actions/configure-pages@v5 uses: actions/configure-pages@v5
- name: Install Nix - name: Install Nix (with flakes and nix-command enabled)
uses: DeterminateSystems/nix-installer-action@main uses: cachix/install-nix-action@v26
with: with:
diagnostic-endpoint: "" nix_path: nixpkgs=channel:nixos-unstable
extra-conf: |
# Add `nix-community`, Crane, upstream Conduit, and conduwuit binary caches
extra_nix_config: |
experimental-features = nix-command flakes experimental-features = nix-command flakes
accept-flake-config = true extra-substituters = https://nix-community.cachix.org
extra-trusted-public-keys = nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=
extra-substituters = https://crane.cachix.org
extra-trusted-public-keys = crane.cachix.org-1:8Scfpmn9w+hGdXH/Q9tTLiYAE/2dnJYRJP7kl80GuRk=
extra-substituters = https://attic.kennel.juneis.dog/conduit
extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg=
extra-substituters = https://attic.kennel.juneis.dog/conduwuit
extra-trusted-public-keys = conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=
- name: Enable Cachix binary cache - name: Add alternative Nix binary caches if specified
run: |
nix profile install nixpkgs#cachix
cachix use crane
cachix use nix-community
- name: Configure Magic Nix Cache
uses: DeterminateSystems/magic-nix-cache-action@main
with:
diagnostic-endpoint: ""
upstream-cache: "https://attic.kennel.juneis.dog/conduwuit"
- name: Apply Nix binary cache configuration
run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit https://cache.lix.systems https://conduwuit.cachix.org
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wkconduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTEcache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o= conduwuit.cachix.org-1:MFRm6jcnfTf0jSAbmvLfhO3KBMt4px+1xaereWXp8Xg=
EOF
- name: Use alternative Nix binary caches if specified
if: ${{ (env.ATTIC_ENDPOINT != '') && (env.ATTIC_PUBLIC_KEY != '') }} if: ${{ (env.ATTIC_ENDPOINT != '') && (env.ATTIC_PUBLIC_KEY != '') }}
run: | run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF echo "extra-substituters = ${{ env.ATTIC_ENDPOINT }}" >> /etc/nix/nix.conf
extra-substituters = ${{ env.ATTIC_ENDPOINT }} echo "extra-trusted-public-keys = ${{ env.ATTIC_PUBLIC_KEY }}" >> /etc/nix/nix.conf
extra-trusted-public-keys = ${{ env.ATTIC_PUBLIC_KEY }}
EOF
- name: Prepare build environment - name: Pop/push Magic Nix Cache
uses: DeterminateSystems/magic-nix-cache-action@main
- name: Configure `nix-direnv`
run: | run: |
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc" echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc"
nix profile install --impure --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv
direnv allow - name: Install `direnv` and `nix-direnv`
nix develop --command true run: nix-env -f "<nixpkgs>" -iA direnv -iA nix-direnv
# Do this to shorten the logs for the real CI step
- name: Populate `/nix/store`
run: nix develop --command true
- name: Allow direnv
run: direnv allow
- name: Cache CI dependencies - name: Cache CI dependencies
run: | run: |
bin/nix-build-and-cache ci ./bin/nix-build-and-cache ci
- name: Build documentation (book) - name: Build documentation (book)
run: | run: |
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Run Trivy code and vulnerability scanner on repo - name: Run Trivy code and vulnerability scanner on repo
uses: aquasecurity/trivy-action@0.23.0 uses: aquasecurity/trivy-action@0.19.0
with: with:
scan-type: repo scan-type: repo
format: sarif format: sarif
@@ -34,7 +34,7 @@ jobs:
severity: CRITICAL,HIGH,MEDIUM,LOW severity: CRITICAL,HIGH,MEDIUM,LOW
- name: Run Trivy code and vulnerability scanner on filesystem - name: Run Trivy code and vulnerability scanner on filesystem
uses: aquasecurity/trivy-action@0.23.0 uses: aquasecurity/trivy-action@0.19.0
with: with:
scan-type: fs scan-type: fs
format: sarif format: sarif
+4 -8
View File
@@ -26,19 +26,15 @@ before_script:
# Add conduwuit binary cache # Add conduwuit binary cache
- if command -v nix > /dev/null; then echo "extra-substituters = https://attic.kennel.juneis.dog/conduwuit" >> /etc/nix/nix.conf; fi - if command -v nix > /dev/null; then echo "extra-substituters = https://attic.kennel.juneis.dog/conduwuit" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE=" >> /etc/nix/nix.conf; fi - if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-substituters = https://attic.kennel.juneis.dog/conduit" >> /etc/nix/nix.conf; fi - if command -v nix > /dev/null; then echo "extra-substituters = https://attic.kennel.juneis.dog/conduit" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk=" >> /etc/nix/nix.conf; fi - if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg=" >> /etc/nix/nix.conf; fi
# Add alternate binary cache # Add alternate binary cache
- if command -v nix > /dev/null && [ -n "$ATTIC_ENDPOINT" ]; then echo "extra-substituters = $ATTIC_ENDPOINT" >> /etc/nix/nix.conf; fi - if command -v nix > /dev/null && [ -n "$ATTIC_ENDPOINT" ]; then echo "extra-substituters = $ATTIC_ENDPOINT" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null && [ -n "$ATTIC_PUBLIC_KEY" ]; then echo "extra-trusted-public-keys = $ATTIC_PUBLIC_KEY" >> /etc/nix/nix.conf; fi - if command -v nix > /dev/null && [ -n "$ATTIC_PUBLIC_KEY" ]; then echo "extra-trusted-public-keys = $ATTIC_PUBLIC_KEY" >> /etc/nix/nix.conf; fi
# Add Lix binary cache
- if command -v nix > /dev/null; then echo "extra-substituters = https://cache.lix.systems" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o=" >> /etc/nix/nix.conf; fi
# Add crane binary cache # Add crane binary cache
- if command -v nix > /dev/null; then echo "extra-substituters = https://crane.cachix.org" >> /etc/nix/nix.conf; fi - if command -v nix > /dev/null; then echo "extra-substituters = https://crane.cachix.org" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = crane.cachix.org-1:8Scfpmn9w+hGdXH/Q9tTLiYAE/2dnJYRJP7kl80GuRk=" >> /etc/nix/nix.conf; fi - if command -v nix > /dev/null; then echo "extra-trusted-public-keys = crane.cachix.org-1:8Scfpmn9w+hGdXH/Q9tTLiYAE/2dnJYRJP7kl80GuRk=" >> /etc/nix/nix.conf; fi
@@ -58,7 +54,7 @@ before_script:
ci: ci:
stage: ci stage: ci
image: nixos/nix:2.23.0 image: nixos/nix:2.22.0
script: script:
# Cache CI dependencies # Cache CI dependencies
- ./bin/nix-build-and-cache ci - ./bin/nix-build-and-cache ci
@@ -83,7 +79,7 @@ ci:
artifacts: artifacts:
stage: artifacts stage: artifacts
image: nixos/nix:2.23.0 image: nixos/nix:2.22.0
script: script:
- ./bin/nix-build-and-cache just .#static-x86_64-unknown-linux-musl - ./bin/nix-build-and-cache just .#static-x86_64-unknown-linux-musl
- cp result/bin/conduit x86_64-unknown-linux-musl - cp result/bin/conduit x86_64-unknown-linux-musl
+1 -3
View File
@@ -52,8 +52,6 @@ conduwuit's website uses [`mdbook`][mdbook] and deployed via CI using GitHub Pag
To build the documentation using Nix, run: `bin/nix-build-and-cache just .#book` To build the documentation using Nix, run: `bin/nix-build-and-cache just .#book`
The output of the mdbook generation is in `result/`. mdbooks can be opened in your browser from the individual HTML files without any web server needed.
### Inclusivity and Diversity ### Inclusivity and Diversity
All **MUST** code and write with inclusivity and diversity in mind. See the [following page by Google on writing inclusive code and documentation](https://developers.google.com/style/inclusive-documentation). All **MUST** code and write with inclusivity and diversity in mind. See the [following page by Google on writing inclusive code and documentation](https://developers.google.com/style/inclusive-documentation).
@@ -70,7 +68,7 @@ Rust's default style and standards with regards to [function names, variable nam
### Creating pull requests ### Creating pull requests
Please try to keep contributions to the GitHub. While the mirrors of conduwuit allow for pull/merge requests, there is no guarantee I will see them in a timely manner. Additionally, please mark WIP or unfinished or incomplete PRs as drafts. This prevents me from having to ping once in a while to double check the status of it, especially when the CI completed successfully and everything so it *looks* done. Please try to keep contributions to the GitHub. While the mirrors of conduwuit allow for pull/merge requests, there is no guarantee I will see them in a timely manner.
If you open a pull request on one of the mirrors, it is your responsibility to inform me about its existence. In the future I may try to solve this with more repo bots in the conduwuit Matrix room. There is no mailing list or email-patch support on the sr.ht mirror, but if you'd like to email me a git patch you can do so at `strawberry@puppygock.gay`. If you open a pull request on one of the mirrors, it is your responsibility to inform me about its existence. In the future I may try to solve this with more repo bots in the conduwuit Matrix room. There is no mailing list or email-patch support on the sr.ht mirror, but if you'd like to email me a git patch you can do so at `strawberry@puppygock.gay`.
Generated
+493 -815
View File
File diff suppressed because it is too large Load Diff
+458 -637
View File
File diff suppressed because it is too large Load Diff
+1 -6
View File
@@ -26,10 +26,6 @@ friends or company.
An official conduwuit server ran by me is available at transfem.dev ([element.transfem.dev](https://element.transfem.dev) / [cinny.transfem.dev](https://cinny.transfem.dev)) An official conduwuit server ran by me is available at transfem.dev ([element.transfem.dev](https://element.transfem.dev) / [cinny.transfem.dev](https://cinny.transfem.dev))
transfem.dev is a public homeserver that can be used, it is not a "test only homeserver". This means there are rules, so please read the rules: [https://transfem.dev/homeserver_rules.txt](https://transfem.dev/homeserver_rules.txt)
transfem.dev is also listed at [servers.joinmatrix.org](https://servers.joinmatrix.org/)
#### What is the current status? #### What is the current status?
conduwuit is a hard fork of Conduit which is in beta, meaning you can join and participate in most conduwuit is a hard fork of Conduit which is in beta, meaning you can join and participate in most
@@ -63,8 +59,7 @@ Both, but I prefer conduwuit.
#### Mirrors of conduwuit #### Mirrors of conduwuit
- GitHub: <https://github.com/girlbossceo/conduwuit> - GitHub: <https://github.com/girlbossceo/conduwuit>
- GitLab: <https://gitlab.com/conduwuit/conduwuit> - GitLab: <https://gitlab.com/girlbossceo/conduwuit>
- git.girlcock.ceo: <https://git.girlcock.ceo/strawberry/conduwuit>
- git.gay: <https://git.gay/june/conduwuit> - git.gay: <https://git.gay/june/conduwuit>
- Codeberg: <https://codeberg.org/girlbossceo/conduwuit> - Codeberg: <https://codeberg.org/girlbossceo/conduwuit>
- sourcehut: <https://git.sr.ht/~girlbossceo/conduwuit> - sourcehut: <https://git.sr.ht/~girlbossceo/conduwuit>
-60
View File
@@ -1,60 +0,0 @@
[Unit]
Description=conduwuit Matrix homeserver
After=network.target
Documentation=https://conduwuit.puppyirl.gay/
RequiresMountsFor=/var/lib/private/conduwuit
[Service]
DynamicUser=yes
Type=notify
AmbientCapabilities=
CapabilityBoundingSet=
DevicePolicy=closed
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
ProcSubset=pid
ProtectClock=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectProc=invisible
ProtectSystem=strict
PrivateDevices=yes
PrivateMounts=yes
PrivateTmp=yes
PrivateUsers=yes
PrivateIPC=yes
RemoveIPC=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service @resources
SystemCallFilter=~@clock @debug @module @mount @reboot @swap @cpu-emulation @obsolete @timer @chown @setuid @privileged @keyring @ipc
SystemCallErrorNumber=EPERM
StateDirectory=conduwuit
RuntimeDirectory=conduwuit
RuntimeDirectoryMode=0750
Environment="CONDUWUIT_CONFIG=/etc/conduwuit/conduwuit.toml"
ExecStart=/usr/bin/conduwuit
Restart=on-failure
RestartSec=5
TimeoutStopSec=4m
TimeoutStartSec=4m
StartLimitInterval=1m
StartLimitBurst=5
[Install]
WantedBy=multi-user.target
+2
View File
@@ -0,0 +1,2 @@
[advisories]
ignore = ["RUSTSEC-2020-0016"]
+3 -9
View File
@@ -15,19 +15,13 @@ LOG_FILE="$2"
# A `.jsonl` file to write test results to # A `.jsonl` file to write test results to
RESULTS_FILE="$3" RESULTS_FILE="$3"
OCI_IMAGE="complement-conduit:main" OCI_IMAGE="complement-conduit:dev"
# Complement tests that are skipped due to flakiness/reliability issues (likely
# Complement itself induced based on various open issues)
#
# According to Go docs, these are separated by forward slashes and not pipes (why)
SKIPPED_COMPLEMENT_TESTS='-skip=TestJumpToDateEndpoint.*|TestJoinFederatedRoomFromApplicationServiceBridgeUser.*|TestFederationRoomsInvite.*|TestClientSpacesSummary.*'
toplevel="$(git rev-parse --show-toplevel)" toplevel="$(git rev-parse --show-toplevel)"
pushd "$toplevel" > /dev/null pushd "$toplevel" > /dev/null
bin/nix-build-and-cache just .#static-complement bin/nix-build-and-cache just .#complement
docker load < result docker load < result
popd > /dev/null popd > /dev/null
@@ -37,7 +31,7 @@ set +o pipefail
env \ env \
-C "$COMPLEMENT_SRC" \ -C "$COMPLEMENT_SRC" \
COMPLEMENT_BASE_IMAGE="$OCI_IMAGE" \ COMPLEMENT_BASE_IMAGE="$OCI_IMAGE" \
go test -tags="conduwuit_blacklist" "$SKIPPED_COMPLEMENT_TESTS" -v -timeout 1h -json ./tests | tee "$LOG_FILE" go test -vet=off -timeout 1h -json ./tests | tee "$LOG_FILE"
set -o pipefail set -o pipefail
# Post-process the results into an easy-to-compare format, sorted by Test name for reproducible results # Post-process the results into an easy-to-compare format, sorted by Test name for reproducible results
+1 -9
View File
@@ -52,7 +52,7 @@ just() {
"${ATTIC_ENDPOINT:-https://attic.kennel.juneis.dog/conduwuit}" \ "${ATTIC_ENDPOINT:-https://attic.kennel.juneis.dog/conduwuit}" \
"$ATTIC_TOKEN" "$ATTIC_TOKEN"
# Upload them to Attic (conduwuit store) and Cachix # Upload them to Attic (conduwuit store)
# #
# Use `xargs` and a here-string because something would probably explode if # Use `xargs` and a here-string because something would probably explode if
# several thousand arguments got passed to a command at once. Hopefully no # several thousand arguments got passed to a command at once. Hopefully no
@@ -61,12 +61,6 @@ just() {
IFS=$'\n' IFS=$'\n'
nix shell --inputs-from "$toplevel" attic -c xargs \ nix shell --inputs-from "$toplevel" attic -c xargs \
attic push conduwuit <<< "${cache[*]}" attic push conduwuit <<< "${cache[*]}"
# push to cachix if available
if [ "$CACHIX_AUTH_TOKEN" ]; then
nix shell --inputs-from "$toplevel" cachix -c xargs \
cachix push conduwuit <<< "${cache[*]}"
fi
) )
} }
@@ -77,9 +71,7 @@ ci() {
# Keep sorted # Keep sorted
"$toplevel#devShells.x86_64-linux.default" "$toplevel#devShells.x86_64-linux.default"
"$toplevel#devShells.x86_64-linux.all-features"
attic#default attic#default
cachix#default
nixpkgs#direnv nixpkgs#direnv
nixpkgs#jq nixpkgs#jq
nixpkgs#nix-direnv nixpkgs#nix-direnv
+1 -7
View File
@@ -1,19 +1,13 @@
[book] [book]
title = "conduwuit 🏳️‍⚧️ 💜 🦴" title = "conduwuit"
description = "conduwuit, which is a well-maintained fork of Conduit, is a simple, fast and reliable chat server for the Matrix protocol" description = "conduwuit, which is a well-maintained fork of Conduit, is a simple, fast and reliable chat server for the Matrix protocol"
language = "en" language = "en"
authors = ["strawberry (June)"]
text-direction = "ltr"
multilingual = false multilingual = false
src = "docs" src = "docs"
[build] [build]
build-dir = "public" build-dir = "public"
create-missing = true create-missing = true
extra-watch-dirs = ["debian", "docs"]
[rust]
edition = "2021"
[output.html] [output.html]
git-repository-url = "https://github.com/girlbossceo/conduwuit" git-repository-url = "https://github.com/girlbossceo/conduwuit"
+1 -7
View File
@@ -1,7 +1 @@
array-size-threshold = 4096 too-many-lines-threshold = 700
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
too-many-lines-threshold = 700 # TODO reduce me to <= 100
type-complexity-threshold = 250 # reduce me to ~200
+16 -64
View File
@@ -60,9 +60,8 @@
### Database configuration ### Database configuration
# This is the only directory where conduwuit will save its data, including media. # This is the only directory where conduwuit will save its data, including media
# Note: this was previously "/var/lib/matrix-conduit" database_path = "/var/lib/matrix-conduit/"
database_path = "/var/lib/conduwuit"
# Database backend: Only rocksdb and sqlite are supported. Please note that sqlite # Database backend: Only rocksdb and sqlite are supported. Please note that sqlite
# will perform significantly worse than rocksdb as it is not intended to be used the # will perform significantly worse than rocksdb as it is not intended to be used the
@@ -77,16 +76,11 @@ database_backend = "rocksdb"
# forwarded to the conduwuit instance running on this port # forwarded to the conduwuit instance running on this port
# Docker users: Don't change this, you'll need to map an external port to this. # Docker users: Don't change this, you'll need to map an external port to this.
# To listen on multiple ports, specify a vector e.g. [8080, 8448] # To listen on multiple ports, specify a vector e.g. [8080, 8448]
#
# default if unspecified is 8008
port = 6167 port = 6167
# default address (IPv4 or IPv6) conduwuit will listen on. Generally you want this to be # default address (IPv4 or IPv6) conduwuit will listen on. Generally you want this to be
# localhost (127.0.0.1 / ::1). If you are using Docker or a container NAT networking setup, you # localhost (127.0.0.1 / ::1). If you are using Docker or a container NAT networking setup, you
# likely need this to be 0.0.0.0. # likely need this to be 0.0.0.0.
# To listen multiple addresses, specify a vector e.g. ["127.0.0.1", "::1"]
#
# default if unspecified is both IPv4 and IPv6 localhost: ["127.0.0.1", "::1"]
address = "127.0.0.1" address = "127.0.0.1"
# Max request size for file uploads # Max request size for file uploads
@@ -198,11 +192,6 @@ registration_token = "change this token for something specific to your server"
# defaults to false # defaults to false
# block_non_admin_invites = false # block_non_admin_invites = false
# Allows admins to enter commands in rooms other than #admins by prefixing with \!admin. The reply
# will be publicly visible to the room, originating from the sender.
# defaults to true
#admin_escape_commands = true
# List of forbidden username patterns/strings. Values in this list are matched as *contains*. # List of forbidden username patterns/strings. Values in this list are matched as *contains*.
# This is checked upon username availability check, registration, and startup as warnings if any local users in your database # This is checked upon username availability check, registration, and startup as warnings if any local users in your database
# have a forbidden username. # have a forbidden username.
@@ -280,19 +269,6 @@ url_preview_check_root_domain = false
# Defaults to true # Defaults to true
allow_profile_lookup_federation_requests = true allow_profile_lookup_federation_requests = true
# Config option to automatically deactivate the account of any user who attempts to join a:
# - banned room
# - forbidden room alias
# - room alias or ID with a forbidden server name
#
# This may be useful if all your banned lists consist of toxic rooms or servers that no good faith user would ever attempt to join, and
# to automatically remediate the problem without any admin user intervention.
#
# This will also make the user leave all rooms. Federation (e.g. remote room invites) are ignored here.
#
# Defaults to false as rooms can be banned for non-moderation-related reasons
#auto_deactivate_banned_room_attempts = false
### Misc ### Misc
@@ -385,6 +361,15 @@ allow_profile_lookup_federation_requests = true
# Defaults to 256.0 # Defaults to 256.0
#db_cache_capacity_mb = 256.0 #db_cache_capacity_mb = 256.0
# Interval in seconds when conduwuit will run database cleanup operations.
#
# For SQLite: this will flush the WAL by executing `PRAGMA wal_checkpoint(RESTART)` (https://www.sqlite.org/pragma.html#pragma_wal_checkpoint)
# For RocksDB: this will run `flush_opt` to flush database memtables to SST files on disk (https://docs.rs/rocksdb/latest/rocksdb/struct.DBCommon.html#method.flush_opt)
# These operations always run on shutdown.
#
# Defaults to 30 minutes (1800 seconds) to avoid IO amplification from too frequent cleanups
#cleanup_second_interval = 1800
### RocksDB options ### RocksDB options
@@ -493,6 +478,11 @@ allow_profile_lookup_federation_requests = true
# Defaults to 1 (TolerateCorruptedTailRecords) # Defaults to 1 (TolerateCorruptedTailRecords)
#rocksdb_recovery_mode = 1 #rocksdb_recovery_mode = 1
# Controls whether memory buffers are written to storage at the fixed interval set by `cleanup_period_interval`
# even when they are not full. Setting this will increase load on the storage backplane and is never advised
# under normal circumstances.
#rocksdb_periodic_cleanup = false
### Domain Name Resolution and Caching ### Domain Name Resolution and Caching
@@ -707,44 +697,6 @@ allow_profile_lookup_federation_requests = true
#typing_client_timeout_max_s = 45 #typing_client_timeout_max_s = 45
### TURN / VoIP
# vector list of TURN URIs/servers to use
#
# No default
#turn_uris = ["turn:example.turn.uri?transport=udp", "turn:example.turn.uri?transport=tcp"]
# TURN secret to use for generating the HMAC-SHA1 hash apart of username and password generation
#
# this is more secure, but if needed you can use traditional username/password below.
#
# no default
#turn_secret = ""
# TURN username to provide the client
#
# no default
#turn_username = ""
# TURN password to provide the client
#
# no default
#turn_password = ""
# TURN TTL
#
# Default is 86400 seconds
#turn_ttl = 86400
# allow guests/unauthenticated users to access TURN credentials
#
# this is the equivalent of Synapse's `turn_allow_guests` config option. this allows
# any unauthenticated user to call `/_matrix/client/v3/voip/turnServer`.
#
# defaults to false
#turn_allow_guests = false
# Other options not in [global]: # Other options not in [global]:
# #
# #
+26 -14
View File
@@ -1,22 +1,34 @@
# conduwuit for Debian conduwuit for Debian
==================
Information about downloading and deploying the Debian package. This may also be referenced for other `apt`-based distros such as Ubuntu. Installation
------------
### Installation Information about downloading, building and deploying the Debian package, see
the "Installing conduwuit" section in the Deploying docs.
All following sections until "Setting up the Reverse Proxy" be ignored because
this is handled automatically by the packaging.
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
-------------
### Configuration When installed, Debconf generates the configuration of the homeserver
(host)name, the address and port it listens on. This configuration ends up in
`/etc/conduwuit/conduwuit.toml`.
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. You can tweak more detailed settings by uncommenting and setting the variables
in `/etc/conduwuit/conduwuit.toml`. This involves settings such as the maximum
file size for download/upload, enabling federation, etc.
You can tweak more detailed settings by uncommenting and setting the config options Running
in `/etc/conduwuit/conduwuit.toml`. -------
### Running The package uses the `conduwuit.service` systemd unit file to start and
stop conduwuit. It loads the configuration file mentioned above to set up the
environment before running the server.
The package uses the [`conduwuit.service`](../configuration.md#example-systemd-unit-file) systemd unit file to start and stop conduwuit. The binary is installed at `/usr/sbin/conduwuit`. This package assumes by default that conduwuit will be placed behind a reverse
proxy. This default deployment entails just listening
This package assumes by default that conduwuit will be placed behind a reverse proxy. The default config options apply (listening on `localhost` and TCP port `6167`). Matrix federation requires a valid domain name and TLS, so you will need to set up TLS certificates and renewal for it to work properly if you intend to federate. on `127.0.0.1` and the free port `6167` and is reachable via a client using the URL
<http://localhost:6167>. Matrix federation requires TLS, so you will need to set up
Consult various online documentation and guides on setting up a reverse proxy and TLS. Caddy is documented at the [generic deployment guide](../deploying/generic.md#setting-up-the-reverse-proxy) as it's the easiest and most user friendly. some certificates and renewal, for it to work properly.
+9 -13
View File
@@ -1,20 +1,13 @@
[Unit] [Unit]
Description=conduwuit Matrix homeserver Description=conduwuit Matrix homeserver
Documentation=https://conduwuit.puppyirl.gay/
After=network-online.target After=network-online.target
[Service] [Service]
DynamicUser=yes DynamicUser=yes
User=conduwuit User=_conduwuit
Group=conduwuit Group=_conduwuit
Type=notify Type=notify
Environment="CONDUWUIT_CONFIG=/etc/conduwuit/conduwuit.toml"
ExecStart=/usr/sbin/conduwuit
ReadWritePaths=/var/lib/conduwuit /etc/conduwuit
AmbientCapabilities= AmbientCapabilities=
CapabilityBoundingSet= CapabilityBoundingSet=
@@ -46,16 +39,19 @@ SystemCallArchitectures=native
SystemCallFilter=@system-service @resources SystemCallFilter=@system-service @resources
SystemCallFilter=~@clock @debug @module @mount @reboot @swap @cpu-emulation @obsolete @timer @chown @setuid @privileged @keyring @ipc SystemCallFilter=~@clock @debug @module @mount @reboot @swap @cpu-emulation @obsolete @timer @chown @setuid @privileged @keyring @ipc
SystemCallErrorNumber=EPERM SystemCallErrorNumber=EPERM
#StateDirectory=conduwuit StateDirectory=matrix-conduit
RuntimeDirectory=conduwuit RuntimeDirectory=conduit
RuntimeDirectoryMode=0750 RuntimeDirectoryMode=0750
Environment="CONDUIT_CONFIG=/etc/conduwuit/conduwuit.toml"
ExecStart=/usr/sbin/conduwuit
Restart=on-failure Restart=on-failure
RestartSec=5 RestartSec=5
TimeoutStopSec=2m TimeoutStopSec=4m
TimeoutStartSec=2m TimeoutStartSec=4m
StartLimitInterval=1m StartLimitInterval=1m
StartLimitBurst=5 StartLimitBurst=5
+11 -12
View File
@@ -1,18 +1,17 @@
#!/bin/sh #!/bin/sh
set -e set -e
# TODO: implement debconf support that is maintainable without duplicating the config
# Source debconf library. # Source debconf library.
#. /usr/share/debconf/confmodule . /usr/share/debconf/confmodule
#
## Ask for the Matrix homeserver name, address and port. # Ask for the Matrix homeserver name, address and port.
#db_input high conduwuit/hostname || true db_input high conduwuit/hostname || true
#db_go db_go
#
#db_input low conduwuit/address || true db_input low conduwuit/address || true
#db_go db_go
#
#db_input medium conduwuit/port || true db_input medium conduwuit/port || true
#db_go db_go
exit 0 exit 0
+10 -27
View File
@@ -1,45 +1,28 @@
#!/bin/sh #!/bin/sh
set -e set -e
# TODO: implement debconf support that is maintainable without duplicating the config . /usr/share/debconf/confmodule
#. /usr/share/debconf/confmodule
CONDUWUIT_DATABASE_PATH=/var/lib/conduwuit CONDUWUIT_DATABASE_PATH=/var/lib/conduwuit/
CONDUWUIT_CONFIG_PATH=/etc/conduwuit
CONDUWUIT_CONFIG_FILE="${CONDUWUIT_CONFIG_PATH}/conduwuit.toml"
case "$1" in case "$1" in
configure) configure)
# Create the `conduwuit` user if it does not exist yet. # Create the `_conduwuit` user if it does not exist yet.
if ! getent passwd conduwuit > /dev/null ; then if ! getent passwd _conduwuit > /dev/null ; then
echo 'Adding system user for the conduwuit Matrix homeserver' 1>&2 echo 'Adding system user for the conduwuit Matrix homeserver' 1>&2
adduser --system --group --quiet \ adduser --system --group --quiet \
--home "$CONDUWUIT_DATABASE_PATH" \ --home "$CONDUWUIT_DATABASE_PATH" \
--disabled-login \ --disabled-login \
--shell "/usr/sbin/nologin" \ --shell "/usr/sbin/nologin" \
--verbose \ --force-badname \
conduwuit _conduwuit
fi fi
# Create the database path if it does not exist yet and fix up ownership # Create the database path if it does not exist yet and fix up ownership
# and permissions for the config. # and permissions.
mkdir -v -p "$CONDUWUIT_DATABASE_PATH" mkdir -p "$CONDUWUIT_DATABASE_PATH"
chown _conduwuit:_conduwuit -R "$CONDUWUIT_DATABASE_PATH"
# symlink the previous location for compatibility if it does not exist yet. chmod 700 "$CONDUWUIT_DATABASE_PATH"
if ! test -L "/var/lib/matrix-conduit" ; then
ln -s -v "$CONDUWUIT_DATABASE_PATH" "/var/lib/matrix-conduit"
fi
chown -v conduwuit:conduwuit -R "$CONDUWUIT_DATABASE_PATH"
chown -v conduwuit:conduwuit -R "$CONDUWUIT_CONFIG_PATH"
chmod -v 740 "$CONDUWUIT_DATABASE_PATH"
echo ''
echo 'Make sure you edit the example config at /etc/conduwuit/conduwuit.toml before starting!'
echo 'To start the server, run: systemctl start conduwuit.service'
echo ''
;; ;;
esac esac
+3 -8
View File
@@ -1,11 +1,10 @@
#!/bin/sh #!/bin/sh
set -e set -e
#. /usr/share/debconf/confmodule . /usr/share/debconf/confmodule
CONDUWUIT_CONFIG_PATH=/etc/conduwuit CONDUWUIT_CONFIG_PATH=/etc/conduwuit
CONDUWUIT_DATABASE_PATH=/var/lib/conduwuit CONDUWUIT_DATABASE_PATH=/var/lib/conduwuit
CONDUWUIT_DATABASE_PATH_SYMLINK=/var/lib/matrix-conduit
case $1 in case $1 in
purge) purge)
@@ -16,15 +15,11 @@ case $1 in
# "configuration files must be preserved when the package is removed, and # "configuration files must be preserved when the package is removed, and
# only deleted when the package is purged." # only deleted when the package is purged."
if [ -d "$CONDUWUIT_CONFIG_PATH" ]; then if [ -d "$CONDUWUIT_CONFIG_PATH" ]; then
rm -v -r "$CONDUWUIT_CONFIG_PATH" rm -r "$CONDUWUIT_CONFIG_PATH"
fi fi
if [ -d "$CONDUWUIT_DATABASE_PATH" ]; then if [ -d "$CONDUWUIT_DATABASE_PATH" ]; then
rm -v -r "$CONDUWUIT_DATABASE_PATH" rm -r "$CONDUWUIT_DATABASE_PATH"
fi
if [ -d "$CONDUWUIT_DATABASE_PATH_SYMLINK" ]; then
rm -v -r "$CONDUWUIT_DATABASE_PATH_SYMLINK"
fi fi
;; ;;
esac esac
+21
View File
@@ -0,0 +1,21 @@
Template: conduwuit/hostname
Type: string
Default: localhost
Description: The server (host)name of the Matrix homeserver
This is the hostname the homeserver will be reachable at via a client.
.
If set to "localhost", you can connect with a client locally and clients
from other hosts and also other homeservers will not be able to reach you!
Template: conduwuit/address
Type: string
Default: 127.0.0.1
Description: The listen address of the Matrix homeserver
This is the address the homeserver will listen on. Leave it set to 127.0.0.1
when using a reverse proxy.
Template: conduwuit/port
Type: string
Default: 6167
Description: The port of the Matrix homeserver
This port is most often just accessed by a reverse proxy.
-42
View File
@@ -1,42 +0,0 @@
[package]
name = "rust-rocksdb-uwu"
categories.workspace = true
description = "dylib wrapper for rust-rocksdb"
edition = "2021"
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
version = "0.0.1"
[features]
default = ["snappy", "lz4", "zstd", "zlib", "bzip2"]
jemalloc = ["rust-rocksdb/jemalloc"]
io-uring = ["rust-rocksdb/io-uring"]
valgrind = ["rust-rocksdb/valgrind"]
snappy = ["rust-rocksdb/snappy"]
lz4 = ["rust-rocksdb/lz4"]
zstd = ["rust-rocksdb/zstd"]
zlib = ["rust-rocksdb/zlib"]
bzip2 = ["rust-rocksdb/bzip2"]
rtti = ["rust-rocksdb/rtti"]
mt_static = ["rust-rocksdb/mt_static"]
multi-threaded-cf = ["rust-rocksdb/multi-threaded-cf"]
serde1 = ["rust-rocksdb/serde1"]
malloc-usable-size = ["rust-rocksdb/malloc-usable-size"]
[dependencies.rust-rocksdb]
git = "https://github.com/zaidoon1/rust-rocksdb"
rev = "e9e1cb5ba92a44ea225fe8d13b31aa23621b9035"
#branch = "master"
default-features = false
[lib]
path = "lib.rs"
crate-type = [
"rlib",
# "dylib"
]
[lints]
workspace = true
-61
View File
@@ -1,61 +0,0 @@
pub use rust_rocksdb::*;
#[cfg_attr(not(conduit_mods), link(name = "rocksdb"))]
#[cfg_attr(conduit_mods, link(name = "rocksdb", kind = "static"))]
extern "C" {
pub fn rocksdb_list_column_families();
pub fn rocksdb_logger_create_stderr_logger();
pub fn rocksdb_options_set_info_log();
pub fn rocksdb_get_options_from_string();
pub fn rocksdb_writebatch_create();
pub fn rocksdb_writebatch_destroy();
pub fn rocksdb_writebatch_put_cf();
pub fn rocksdb_writebatch_delete_cf();
pub fn rocksdb_iter_value();
pub fn rocksdb_iter_seek_to_last();
pub fn rocksdb_iter_seek_for_prev();
pub fn rocksdb_iter_seek_to_first();
pub fn rocksdb_iter_next();
pub fn rocksdb_iter_prev();
pub fn rocksdb_iter_seek();
pub fn rocksdb_iter_valid();
pub fn rocksdb_iter_get_error();
pub fn rocksdb_iter_key();
pub fn rocksdb_iter_destroy();
pub fn rocksdb_livefiles();
pub fn rocksdb_livefiles_count();
pub fn rocksdb_livefiles_destroy();
pub fn rocksdb_livefiles_column_family_name();
pub fn rocksdb_livefiles_name();
pub fn rocksdb_livefiles_size();
pub fn rocksdb_livefiles_level();
pub fn rocksdb_livefiles_smallestkey();
pub fn rocksdb_livefiles_largestkey();
pub fn rocksdb_livefiles_entries();
pub fn rocksdb_livefiles_deletions();
pub fn rocksdb_put_cf();
pub fn rocksdb_delete_cf();
pub fn rocksdb_get_pinned_cf();
pub fn rocksdb_create_column_family();
pub fn rocksdb_get_latest_sequence_number();
pub fn rocksdb_batched_multi_get_cf();
pub fn rocksdb_cancel_all_background_work();
pub fn rocksdb_repair_db();
pub fn rocksdb_list_column_families_destroy();
pub fn rocksdb_flush();
pub fn rocksdb_flush_wal();
pub fn rocksdb_open_column_families();
pub fn rocksdb_open_for_read_only_column_families();
pub fn rocksdb_open_as_secondary_column_families();
pub fn rocksdb_open_column_families_with_ttl();
pub fn rocksdb_open();
pub fn rocksdb_open_for_read_only();
pub fn rocksdb_open_with_ttl();
pub fn rocksdb_open_as_secondary();
pub fn rocksdb_write();
pub fn rocksdb_create_iterator_cf();
pub fn rocksdb_backup_engine_create_new_backup_flush();
pub fn rocksdb_backup_engine_options_create();
pub fn rocksdb_write_buffer_manager_destroy();
pub fn rocksdb_options_set_ttl();
}
+4 -8
View File
@@ -2,19 +2,15 @@
- [Introduction](introduction.md) - [Introduction](introduction.md)
- [Differences from upstream Conduit](differences.md) - [Differences from upstream Conduit](differences.md)
- [Example configuration](configuration.md) - [Example configuration](configuration.md)
- [Deploying](deploying.md) - [Deploying](deploying.md)
- [Generic](deploying/generic.md) - [Generic](deploying/generic.md)
- [NixOS](deploying/nixos.md)
- [Docker](deploying/docker.md)
- [Arch Linux](deploying/arch-linux.md)
- [Debian](deploying/debian.md) - [Debian](deploying/debian.md)
- [Docker](deploying/docker.md)
- [NixOS](deploying/nixos.md)
- [TURN](turn.md) - [TURN](turn.md)
- [Appservices](appservices.md) - [Appservices](appservices.md)
- [Maintenance](maintenance.md)
- [Troubleshooting](troubleshooting.md)
- [Development](development.md) - [Development](development.md)
- [Contributing](contributing.md)
- [Testing](development/testing.md) - [Testing](development/testing.md)
- [Hot Reloading ("Live" Development)](development/hot_reload.md) - [Contributing](contributing.md)
- [conduwuit Community Code of Conduct](conduwuit_coc.md)
+18 -8
View File
@@ -12,13 +12,13 @@ and later starting it.
At some point the appservice guide should ask you to add a registration yaml At some point the appservice guide should ask you to add a registration yaml
file to the homeserver. In Synapse you would do this by adding the path to the file to the homeserver. In Synapse you would do this by adding the path to the
homeserver.yaml, but in conduwuit you can do this from within Matrix: homeserver.yaml, but in Conduit you can do this from within Matrix:
First, go into the `#admins` room of your homeserver. The first person that First, go into the #admins room of your homeserver. The first person that
registered on the homeserver automatically joins it. Then send a message into registered on the homeserver automatically joins it. Then send a message into
the room like this: the room like this:
!admin appservices register @conduit:your.server.name: register-appservice
``` ```
paste paste
the the
@@ -31,13 +31,13 @@ the room like this:
``` ```
You can confirm it worked by sending a message like this: You can confirm it worked by sending a message like this:
`!admin appservices list` `@conduit:your.server.name: appservices list`
The server bot should answer with `Appservices (1): your-bridge` The `@conduit` bot should answer with `Appservices (1): your-bridge`
Then you are done. conduwuit will send messages to the appservices and the Then you are done. Conduit will send messages to the appservices and the
appservice can send requests to the homeserver. You don't need to restart appservice can send requests to the homeserver. You don't need to restart
conduwuit, but if it doesn't work, restarting while the appservice is running Conduit, but if it doesn't work, restarting while the appservice is running
could help. could help.
## Appservice-specific instructions ## Appservice-specific instructions
@@ -46,6 +46,16 @@ could help.
To remove an appservice go to your admin room and execute To remove an appservice go to your admin room and execute
`!admin appservices unregister <name>` `@conduit:your.server.name: appservices unregister <name>`
where `<name>` one of the output of `appservices list`. where `<name>` one of the output of `appservices list`.
### Tested appservices
These appservices have been tested and work with Conduit without any extra steps:
- [matrix-appservice-discord](https://github.com/Half-Shot/matrix-appservice-discord)
- [mautrix-hangouts](https://github.com/mautrix/hangouts/)
- [mautrix-telegram](https://github.com/mautrix/telegram/)
- [mautrix-signal](https://github.com/mautrix/signal/) from version `0.2.2` forward.
- [heisenbridge](https://github.com/hifi/heisenbridge/)
-77
View File
@@ -1,77 +0,0 @@
# conduwuit Community Code of Conduct
Welcome to the conduwuit community! Were excited to have you here. conduwuit is a hard-fork of the Conduit homeserver,
aimed at making Matrix more accessible and inclusive for everyone.
This space is dedicated to fostering a positive, supportive, and inclusive environment for everyone. This Code of
Conduct applies to all conduwuit spaces, including any further community rooms that reference this CoC. Here are our
guidelines to help maintain the welcoming atmosphere that sets conduwuit apart.
For the foundational rules, please refer to the [Matrix.org Code of Conduct](https://matrix.org/legal/code-of-conduct/)
and the [Contributor's Covenant](https://github.com/girlbossceo/conduwuit/blob/main/CODE_OF_CONDUCT.md). Below are
additional guidelines specific to the conduwuit community.
## Our Values and Guidelines
1. **Respect and Inclusivity**: We are committed to maintaining a community where everyone feels safe and respected.
Discrimination, harassment, or hate speech of any kind will not be tolerated. Recognise that each community member
experiences the world differently based on their past experiences, background, and identity. Share your own
experiences and be open to learning about others' diverse perspectives.
2. **Positivity and Constructiveness**: Engage in constructive discussions and support each other. If you feel angry,
negative, or aggressive, take a break until you can participate in a positive and constructive manner. Process
intense feelings with a friend or in a private setting before engaging in community conversations to help maintain
a supportive and focused environment.
3. **Clarity and Understanding**: Our community includes neurodivergent individuals and those who may not appreciate
sarcasm or subtlety. Communicate clearly and kindly, avoiding sarcasm and ensuring your messages are easily
understood by all. Additionally, avoid putting the burden of education on marginalized groups by doing your own
research before asking for explanations.
4. **Be Open to Inclusivity**: Actively engage in conversations about making our community more inclusive. Report
discriminatory behavior to the moderators and be open to constructive feedback that aims to improve our community.
Understand that discussing discrimination and negative experiences can be emotionally taxing, so focus on the
message rather than critiquing the tone used.
5. **Commit to Inclusivity**: Building an inclusive community requires time, energy, and resources. Recognise that
addressing discrimination and bias is an ongoing process that necessitates commitment and action from all community
members.
## Matrix Community
This Code of Conduct applies to the entire [conduwuit Matrix Space](https://matrix.to/#/#conduwuit-space:puppygock.gay)
and its rooms, including:
### [#conduwuit:puppygock.gay](https://matrix.to/#/#conduwuit:puppygock.gay)
This room is for support and discussions about conduwuit. Ask questions, share insights, and help each other out.
### [#conduwuit-offtopic:girlboss.ceo](https://matrix.to/#/#conduwuit-offtopic:girlboss.ceo)
For off-topic community conversations about any subject. While this room allows for a wide range of topics, the same
CoC applies. Keep discussions respectful and inclusive, and avoid divisive subjects like country/world politics.
General topics, such as world events, are welcome as long as they follow the CoC.
### [#conduwuit-dev:puppygock.gay](https://matrix.to/#/#conduwuit-dev:puppygock.gay)
This room is dedicated to discussing active development of conduwuit. Posting requires an elevated power level, which
can be requested in one of the other rooms. Use this space to collaborate and innovate.
## Enforcement
We have a zero-tolerance policy for violations of this Code of Conduct. If someones behavior makes you uncomfortable,
please report it to the moderators. Actions we may take include:
1. **Warning**: A warning given directly in the room or via a private message from the moderators, identifying
the violation and requesting corrective action.
2. **Temporary Mute**: Temporary restriction from participating in discussions for a specified period to allow for
reflection and cooling off.
3. **Kick or Ban**: Egregious behavior may result in an immediate kick or ban to protect other community members.
Bans are considered permanent and will only be reversed in exceptional circumstances after proven good behavior.
Please highlight issues directly in rooms when possible, but if you don't feel comfortable doing that, then please send
a DM to one of the moderators directly.
Together, lets build a community where everyone feels valued and respected.
- The conduwuit Moderation Team
+1 -28
View File
@@ -1,32 +1,5 @@
## Example configuration # Example configuration
<details>
<summary>Example configuration</summary>
``` toml ``` toml
{{#include ../conduwuit-example.toml}} {{#include ../conduwuit-example.toml}}
``` ```
</details>
## Debian systemd unit file
<details>
<summary>Debian systemd unit file</summary>
```
{{#include ../debian/conduwuit.service}}
```
</details>
## Arch Linux systemd unit file
<details>
<summary>Arch Linux systemd unit file</summary>
```
{{#include ../arch/conduwuit.service}}
```
</details>
+1 -1
View File
@@ -1,3 +1,3 @@
# Deploying # Deploying
This chapter describes various ways to deploy conduwuit. This chapter describes various ways to deploy Conduwuit.
-9
View File
@@ -1,9 +0,0 @@
# conduwuit for Arch Linux
Currently conduwuit is only on the Arch User Repository (AUR).
The conduwuit AUR packages are community maintained and are not maintained by conduwuit development team, but the AUR package maintainers are in the Matrix room. Please attempt to verify your AUR package's PKGBUILD file looks fine before asking for support.
- [conduwuit](https://aur.archlinux.org/packages/conduwuit) - latest tagged conduwuit
- [conduwuit-git](https://aur.archlinux.org/packages/conduwuit-git) - latest git conduwuit from `main` branch
- [conduwuit-bin](https://aur.archlinux.org/packages/conduwuit-bin) - latest tagged conduwuit static binary
+27 -17
View File
@@ -1,30 +1,40 @@
# conduwuit - Behind Traefik Reverse Proxy # Conduit - Behind Traefik Reverse Proxy
version: '2.4' # uses '2.4' for cpuset version: '2.4' # uses '2.4' for cpuset
services: services:
homeserver: homeserver:
### If you already built the conduduwit image with 'docker build' or want to use the Docker Hub image, ### If you already built the Conduit image with 'docker build' or want to use the Docker Hub image,
### then you are ready to go. ### then you are ready to go.
image: girlbossceo/conduwuit:latest image: girlbossceo/conduwuit:latest
### If you want to build a fresh image from the sources, then comment the image line and uncomment the
### build lines. If you want meaningful labels in your built Conduit image, you should run docker compose like this:
### CREATED=$(date -u +'%Y-%m-%dT%H:%M:%SZ') VERSION=$(grep -m1 -o '[0-9].[0-9].[0-9]' Cargo.toml) docker compose up -d
# build:
# context: .
# args:
# CREATED: '2021-03-16T08:18:27Z'
# VERSION: '0.1.0'
# LOCAL: 'false'
# GIT_REF: origin/master
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- db:/var/lib/conduwuit - db:/var/lib/matrix-conduit
#- ./conduwuit.toml:/etc/conduwuit.toml #- ./conduwuit.toml:/etc/conduit.toml
networks: networks:
- proxy - proxy
environment: environment:
CONDUWUIT_SERVER_NAME: your.server.name # EDIT THIS CONDUIT_SERVER_NAME: your.server.name # EDIT THIS
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit CONDUIT_DATABASE_PATH: /var/lib/matrix-conduit
CONDUWUIT_DATABASE_BACKEND: rocksdb CONDUIT_DATABASE_BACKEND: rocksdb
CONDUWUIT_PORT: 6167 CONDUIT_PORT: 6167
CONDUWUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
CONDUWUIT_ALLOW_REGISTRATION: 'true' CONDUIT_ALLOW_REGISTRATION: 'true'
CONDUWUIT_ALLOW_FEDERATION: 'true' CONDUIT_ALLOW_FEDERATION: 'true'
CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true' CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
CONDUWUIT_TRUSTED_SERVERS: '["matrix.org"]' CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
#CONDUWUIT_LOG: warn,state_res=warn #CONDUIT_LOG: warn,state_res=warn
CONDUWUIT_ADDRESS: 0.0.0.0 CONDUIT_ADDRESS: 0.0.0.0
#CONDUWUIT_CONFIG: './conduwuit.toml' # Uncomment if you mapped config toml above #CONDUIT_CONFIG: './conduwuit.toml' # Uncomment if you mapped config toml above
#cpuset: "0-4" # Uncomment to limit to specific CPU cores #cpuset: "0-4" # Uncomment to limit to specific CPU cores
# We need some way to server the client and server .well-known json. The simplest way is to use a nginx container # We need some way to server the client and server .well-known json. The simplest way is to use a nginx container
@@ -38,7 +48,7 @@ services:
- ./nginx/www:/var/www/ # location of the client and server .well-known-files - ./nginx/www:/var/www/ # location of the client and server .well-known-files
### Uncomment if you want to use your own Element-Web App. ### Uncomment if you want to use your own Element-Web App.
### Note: You need to provide a config.json for Element and you also need a second ### Note: You need to provide a config.json for Element and you also need a second
### Domain or Subdomain for the communication between Element and conduwuit ### Domain or Subdomain for the communication between Element and Conduit
### Config-Docs: https://github.com/vector-im/element-web/blob/develop/docs/config.md ### Config-Docs: https://github.com/vector-im/element-web/blob/develop/docs/config.md
# element-web: # element-web:
# image: vectorim/element-web:latest # image: vectorim/element-web:latest
+5 -5
View File
@@ -1,4 +1,4 @@
# conduwuit - Traefik Reverse Proxy Labels # Conduit - Traefik Reverse Proxy Labels
version: '2.4' # uses '2.4' for cpuset version: '2.4' # uses '2.4' for cpuset
services: services:
@@ -7,10 +7,10 @@ services:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.docker.network=proxy" # Change this to the name of your Traefik docker proxy network - "traefik.docker.network=proxy" # Change this to the name of your Traefik docker proxy network
- "traefik.http.routers.to-conduwuit.rule=Host(`<SUBDOMAIN>.<DOMAIN>`)" # Change to the address on which conduwuit is hosted - "traefik.http.routers.to-conduit.rule=Host(`<SUBDOMAIN>.<DOMAIN>`)" # Change to the address on which Conduit is hosted
- "traefik.http.routers.to-conduwuit.tls=true" - "traefik.http.routers.to-conduit.tls=true"
- "traefik.http.routers.to-conduwuit.tls.certresolver=letsencrypt" - "traefik.http.routers.to-conduit.tls.certresolver=letsencrypt"
- "traefik.http.routers.to-conduwuit.middlewares=cors-headers@docker" - "traefik.http.routers.to-conduit.middlewares=cors-headers@docker"
- "traefik.http.middlewares.cors-headers.headers.accessControlAllowOriginList=*" - "traefik.http.middlewares.cors-headers.headers.accessControlAllowOriginList=*"
- "traefik.http.middlewares.cors-headers.headers.accessControlAllowHeaders=Origin, X-Requested-With, Content-Type, Accept, Authorization" - "traefik.http.middlewares.cors-headers.headers.accessControlAllowHeaders=Origin, X-Requested-With, Content-Type, Accept, Authorization"
+30 -19
View File
@@ -1,33 +1,44 @@
# conduwuit - Behind Traefik Reverse Proxy # Conduit - Behind Traefik Reverse Proxy
version: '2.4' # uses '2.4' for cpuset version: '2.4' # uses '2.4' for cpuset
services: services:
homeserver: homeserver:
### If you already built the conduwuit image with 'docker build' or want to use the Docker Hub image, ### If you already built the Conduit image with 'docker build' or want to use the Docker Hub image,
### then you are ready to go. ### then you are ready to go.
image: girlbossceo/conduwuit:latest image: girlbossceo/conduwuit:latest
### If you want to build a fresh image from the sources, then comment the image line and uncomment the
### build lines. If you want meaningful labels in your built Conduit image, you should run docker compose like this:
### CREATED=$(date -u +'%Y-%m-%dT%H:%M:%SZ') VERSION=$(grep -m1 -o '[0-9].[0-9].[0-9]' Cargo.toml) docker compose up -d
# build:
# context: .
# args:
# CREATED: '2021-03-16T08:18:27Z'
# VERSION: '0.1.0'
# LOCAL: 'false'
# GIT_REF: origin/master
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- db:/srv/conduwuit/.local/share/conduwuit - db:/srv/conduit/.local/share/conduit
#- ./conduwuit.toml:/etc/conduwuit.toml #- ./conduwuit.toml:/etc/conduit.toml
networks: networks:
- proxy - proxy
environment: environment:
CONDUWUIT_SERVER_NAME: your.server.name # EDIT THIS CONDUIT_SERVER_NAME: your.server.name # EDIT THIS
CONDUWUIT_TRUSTED_SERVERS: '["matrix.org"]' CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
CONDUWUIT_ALLOW_REGISTRATION : 'true' CONDUIT_ALLOW_REGISTRATION : 'true'
#CONDUWUIT_CONFIG: './conduwuit.toml' # Uncomment if you mapped config toml above #CONDUIT_CONFIG: './conduwuit.toml' # Uncomment if you mapped config toml above
### Uncomment and change values as desired ### Uncomment and change values as desired
# CONDUWUIT_ADDRESS: 0.0.0.0 # CONDUIT_ADDRESS: 0.0.0.0
# CONDUWUIT_PORT: 6167 # CONDUIT_PORT: 6167
# CONDUWUIT_LOG: info # default is: "warn,state_res=warn" # Available levels are: error, warn, info, debug, trace - more info at: https://docs.rs/env_logger/*/env_logger/#enabling-logging
# CONDUWUIT_ALLOW_JAEGER: 'false' # CONDUIT_LOG: info # default is: "warn,state_res=warn"
# CONDUWUIT_ALLOW_ENCRYPTION: 'true' # CONDUIT_ALLOW_JAEGER: 'false'
# CONDUWUIT_ALLOW_FEDERATION: 'true' # CONDUIT_ALLOW_ENCRYPTION: 'true'
# CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true' # CONDUIT_ALLOW_FEDERATION: 'true'
# CONDUWUIT_DATABASE_PATH: /srv/conduwuit/.local/share/conduwuit # CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
# CONDUWUIT_WORKERS: 10 # CONDUIT_DATABASE_PATH: /srv/conduit/.local/share/conduit
# CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB # CONDUIT_WORKERS: 10
# CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
#cpuset: "0-4" # Uncomment to limit to specific CPU cores #cpuset: "0-4" # Uncomment to limit to specific CPU cores
# We need some way to server the client and server .well-known json. The simplest way is to use a nginx container # We need some way to server the client and server .well-known json. The simplest way is to use a nginx container
@@ -42,7 +53,7 @@ services:
### Uncomment if you want to use your own Element-Web App. ### Uncomment if you want to use your own Element-Web App.
### Note: You need to provide a config.json for Element and you also need a second ### Note: You need to provide a config.json for Element and you also need a second
### Domain or Subdomain for the communication between Element and conduwuit ### Domain or Subdomain for the communication between Element and Conduit
### Config-Docs: https://github.com/vector-im/element-web/blob/develop/docs/config.md ### Config-Docs: https://github.com/vector-im/element-web/blob/develop/docs/config.md
# element-web: # element-web:
# image: vectorim/element-web:latest # image: vectorim/element-web:latest
+27 -17
View File
@@ -1,35 +1,45 @@
# conduwuit # Conduit
version: '2.4' # uses '2.4' for cpuset version: '2.4' # uses '2.4' for cpuset
services: services:
homeserver: homeserver:
### If you already built the conduwuit image with 'docker build' or want to use a registry image, ### If you already built the Conduit image with 'docker build' or want to use a registry image,
### then you are ready to go. ### then you are ready to go.
image: girlbossceo/conduwuit:latest image: girlbossceo/conduwuit:latest
### If you want to build a fresh image from the sources, then comment the image line and uncomment the
### build lines. If you want meaningful labels in your built Conduit image, you should run docker compose like this:
### CREATED=$(date -u +'%Y-%m-%dT%H:%M:%SZ') VERSION=$(grep -m1 -o '[0-9].[0-9].[0-9]' Cargo.toml) docker compose up -d
# build:
# context: .
# args:
# CREATED: '2021-03-16T08:18:27Z'
# VERSION: '0.1.0'
# LOCAL: 'false'
# GIT_REF: origin/master
restart: unless-stopped restart: unless-stopped
ports: ports:
- 8448:6167 - 8448:6167
volumes: volumes:
- db:/var/lib/conduwuit - db:/var/lib/matrix-conduit
#- ./conduwuit.toml:/etc/conduwuit.toml #- ./conduwuit.toml:/etc/conduit.toml
environment: environment:
CONDUWUIT_SERVER_NAME: your.server.name # EDIT THIS CONDUIT_SERVER_NAME: your.server.name # EDIT THIS
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit CONDUIT_DATABASE_PATH: /var/lib/matrix-conduit
CONDUWUIT_DATABASE_BACKEND: rocksdb CONDUIT_DATABASE_BACKEND: rocksdb
CONDUWUIT_PORT: 6167 CONDUIT_PORT: 6167
CONDUWUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
CONDUWUIT_ALLOW_REGISTRATION: 'true' CONDUIT_ALLOW_REGISTRATION: 'true'
CONDUWUIT_ALLOW_FEDERATION: 'true' CONDUIT_ALLOW_FEDERATION: 'true'
CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true' CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
CONDUWUIT_TRUSTED_SERVERS: '["matrix.org"]' CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
#CONDUWUIT_LOG: warn,state_res=warn #CONDUIT_LOG: warn,state_res=warn
CONDUWUIT_ADDRESS: 0.0.0.0 CONDUIT_ADDRESS: 0.0.0.0
#CONDUWUIT_CONFIG: './conduwuit.toml' # Uncomment if you mapped config toml above #CONDUIT_CONFIG: './conduwuit.toml' # Uncomment if you mapped config toml above
#cpuset: "0-4" # Uncomment to limit to specific CPU cores #cpuset: "0-4" # Uncomment to limit to specific CPU cores
# #
### Uncomment if you want to use your own Element-Web App. ### Uncomment if you want to use your own Element-Web App.
### Note: You need to provide a config.json for Element and you also need a second ### Note: You need to provide a config.json for Element and you also need a second
### Domain or Subdomain for the communication between Element and conduwuit ### Domain or Subdomain for the communication between Element and Conduit
### Config-Docs: https://github.com/vector-im/element-web/blob/develop/docs/config.md ### Config-Docs: https://github.com/vector-im/element-web/blob/develop/docs/config.md
# element-web: # element-web:
# image: vectorim/element-web:latest # image: vectorim/element-web:latest
+101 -9
View File
@@ -4,6 +4,7 @@
To run conduwuit with Docker you can either build the image yourself or pull it from a registry. To run conduwuit with Docker you can either build the image yourself or pull it from a registry.
### Use a registry ### Use a registry
OCI images for conduwuit are available in the registries listed below. OCI images for conduwuit are available in the registries listed below.
@@ -11,24 +12,26 @@ OCI images for conduwuit are available in the registries listed below.
| Registry | Image | Size | Notes | | Registry | Image | Size | Notes |
| --------------- | --------------------------------------------------------------- | ----------------------------- | ---------------------- | | --------------- | --------------------------------------------------------------- | ----------------------------- | ---------------------- |
| GitHub Registry | [ghcr.io/girlbossceo/conduwuit:latest][gh] | ![Image Size][shield-latest] | Stable tagged image. | | 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. | | GitLab Registry | [registry.gitlab.com/girlbossceo/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. | | Docker Hub | [docker.io/girlbossceo/conduwuit:latest][dh] | ![Image Size][shield-latest] | Stable tagged image. |
| GitHub Registry | [ghcr.io/girlbossceo/conduwuit:main][gh] | ![Image Size][shield-main] | Stable main branch. | | 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. | | GitLab Registry | [registry.gitlab.com/girlbossceo/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. | | Docker Hub | [docker.io/girlbossceo/conduwuit:main][dh] | ![Image Size][shield-main] | Stable main branch. |
[dh]: https://hub.docker.com/repository/docker/girlbossceo/conduwuit [dh]: https://hub.docker.com/repository/docker/girlbossceo/conduwuit
[gh]: https://github.com/girlbossceo/conduwuit/pkgs/container/conduwuit [gh]: https://github.com/girlbossceo/conduwuit/pkgs/container/conduwuit
[gl]: https://gitlab.com/conduwuit/conduwuit/container_registry/6351657 [gl]: https://gitlab.com/girlbossceo/conduwuit/container_registry/6351657
[shield-latest]: https://img.shields.io/docker/image-size/girlbossceo/conduwuit/latest [shield-latest]: https://img.shields.io/docker/image-size/girlbossceo/conduwuit/latest
[shield-main]: https://img.shields.io/docker/image-size/girlbossceo/conduwuit/main [shield-main]: https://img.shields.io/docker/image-size/girlbossceo/conduwuit/main
Use Use
```bash ```bash
docker image pull <link> docker image pull <link>
``` ```
to pull it to your machine. to pull it to your machine.
### Run ### Run
When you have the image you can simply run it with When you have the image you can simply run it with
@@ -36,17 +39,21 @@ When you have the image you can simply run it with
```bash ```bash
docker run -d -p 8448:6167 \ docker run -d -p 8448:6167 \
-v db:/var/lib/conduwuit/ \ -v db:/var/lib/conduwuit/ \
-e CONDUWUIT_SERVER_NAME="your.server.name" \ -e CONDUIT_SERVER_NAME="your.server.name" \
-e CONDUWUIT_DATABASE_BACKEND="rocksdb" \ -e CONDUIT_DATABASE_BACKEND="rocksdb" \
-e CONDUWUIT_ALLOW_REGISTRATION=false \ -e CONDUIT_ALLOW_REGISTRATION=false \
-e CONDUIT_ALLOW_FEDERATION=true \
-e CONDUIT_MAX_REQUEST_SIZE="40000000" \
-e CONDUIT_TRUSTED_SERVERS="[\"matrix.org\"]" \
-e CONDUIT_LOG="warn,ruma_state_res=warn" \
--name conduit <link> --name conduit <link>
``` ```
or you can use [docker compose](#docker-compose). or you can use [docker compose](#docker-compose).
The `-d` flag lets the container run in detached mode. You may supply an optional `conduwuit.toml` config file, the example config can be found [here](../configuration.md). The `-d` flag lets the container run in detached mode. You may supply an optional `conduwuit.toml` config file, an example can be found [here](../configuration.md).
You can pass in different env vars to change config values on the fly. You can even configure conduwuit completely by using env vars. For an overview of possible You can pass in different env vars to change config values on the fly. You can even configure conduwuit completely by using env vars. For an overview of possible
values, please take a look at the [`docker-compose.yml`](docker-compose.yml) file. values, please take a look at the `docker-compose.yml` file.
If you just want to test conduwuit for a short time, you can use the `--rm` flag, which will clean up everything related to your container after you stop it. If you just want to test conduwuit for a short time, you can use the `--rm` flag, which will clean up everything related to your container after you stop it.
@@ -100,7 +107,92 @@ either expose ports `443` and `8448` or serve two endpoints `.well-known/matrix/
With the service `well-known` we use a single `nginx` container that will serve those two files. With the service `well-known` we use a single `nginx` container that will serve those two files.
So...step by step:
1. Copy [`docker-compose.for-traefik.yml`](docker-compose.for-traefik.yml) (or
[`docker-compose.with-traefik.yml`](docker-compose.with-traefik.yml)) and [`docker-compose.override.yml`](docker-compose.override.yml) from the repository and remove `.for-traefik` (or `.with-traefik`) from the filename.
2. Open both files and modify/adjust them to your needs. Meaning, change the `CONDUIT_SERVER_NAME` and the volume host mappings according to your needs.
3. Create the `conduwuit.toml` config file, an example can be found [here](../configuration.md), or set `CONDUIT_CONFIG=""` and configure conduwuit per env vars.
4. Uncomment the `element-web` service if you want to host your own Element Web Client and create a `element_config.json`.
5. Create the files needed by the `well-known` service.
- `./nginx/matrix.conf` (relative to the compose file, you can change this, but then also need to change the volume mapping)
```nginx
server {
server_name <SUBDOMAIN>.<DOMAIN>;
listen 80 default_server;
location /.well-known/matrix/server {
return 200 '{"m.server": "<SUBDOMAIN>.<DOMAIN>:443"}';
types { } default_type "application/json; charset=utf-8";
}
location /.well-known/matrix/client {
return 200 '{"m.homeserver": {"base_url": "https://<SUBDOMAIN>.<DOMAIN>"}}';
types { } default_type "application/json; charset=utf-8";
add_header "Access-Control-Allow-Origin" *;
}
location / {
return 404;
}
}
```
6. Run `docker compose up -d`
7. Connect to your homeserver with your preferred client and create a user. You should do this immediately after starting Conduit, because the first created user is the admin.
## Voice communication ## Voice communication
See the [TURN](../turn.md) page. In order to make or receive calls, a TURN server is required. conduwuit suggests using [Coturn](https://github.com/coturn/coturn) for this purpose, which is also available as a Docker image. Before proceeding with the software installation, it is essential to have the necessary configurations in place.
### Configuration
Create a configuration file called `coturn.conf` containing:
```conf
use-auth-secret
static-auth-secret=<a secret key>
realm=<your server domain>
```
A common way to generate a suitable alphanumeric secret key is by using `pwgen -s 64 1`.
These same values need to be set in conduwuit. You can either modify conduwuit.toml to include these lines:
```
turn_uris = ["turn:<your server domain>?transport=udp", "turn:<your server domain>?transport=tcp"]
turn_secret = "<secret key from coturn configuration>"
```
or append the following to the docker environment variables dependig on which configuration method you used earlier:
```yml
CONDUIT_TURN_URIS: '["turn:<your server domain>?transport=udp", "turn:<your server domain>?transport=tcp"]'
CONDUIT_TURN_SECRET: "<secret key from coturn configuration>"
```
Restart Conduit to apply these changes.
### Run
Run the [Coturn](https://hub.docker.com/r/coturn/coturn) image using
```bash
docker run -d --network=host -v $(pwd)/coturn.conf:/etc/coturn/turnserver.conf coturn/coturn
```
or docker-compose. For the latter, paste the following section into a file called `docker-compose.yml`
and run `docker compose up -d` in the same directory.
```yml
version: 3
services:
turn:
container_name: coturn-server
image: docker.io/coturn/coturn
restart: unless-stopped
network_mode: "host"
volumes:
- ./coturn.conf:/etc/coturn/turnserver.conf
```
To understand why the host networking mode is used and explore alternative configuration options, please visit the following link: https://github.com/coturn/coturn/blob/master/docker/coturn/README.md.
For security recommendations see Synapse's [Coturn documentation](https://github.com/matrix-org/synapse/blob/develop/docs/setup/turn/coturn.md#configuration).
+50 -22
View File
@@ -1,5 +1,7 @@
# Generic deployment documentation # Generic deployment documentation
### Please note that this documentation is not fully representative of conduwuit at the moment. Assume majority of it is outdated.
> ## Getting help > ## Getting help
> >
> If you run into any problems while setting up conduwuit, ask us > If you run into any problems while setting up conduwuit, ask us
@@ -11,16 +13,23 @@ You may simply download the binary that fits your machine. Run `uname -m` to see
Prebuilt binaries can be downloaded from the latest tagged release [here](https://github.com/girlbossceo/conduwuit/releases/latest). Prebuilt binaries can be downloaded from the latest tagged release [here](https://github.com/girlbossceo/conduwuit/releases/latest).
The latest tagged release also includes the Debian packages. Alternatively, you may compile the binary yourself. First, install any dependencies:
Alternatively, you may compile the binary yourself. We recommend using [Lix](https://lix.systems) to build conduwuit as this has the most guaranteed ```bash
reproducibiltiy and easiest to get a build environment and output going. # Debian
$ sudo apt install libclang-dev build-essential
Otherwise, follow standard Rust project build guides (installing git and cloning the repo, getting the Rust toolchain via rustup, installing LLVM toolchain + libclang, installing liburing for io_uring and RocksDB, etc). # RHEL
$ sudo dnf install clang
```
Then, `cd` into the source tree of conduwuit and run:
```bash
$ cargo build --release
```
## Adding a conduwuit user ## Adding a conduwuit user
While conduwuit can run as any user it is better to use dedicated users for different services. This also allows While conduwuit can run as any user it is usually better to use dedicated users for different services. This also allows
you to make sure that the file permissions are correctly set up. you to make sure that the file permissions are correctly set up.
In Debian or RHEL, you can use this command to create a conduwuit user: In Debian or RHEL, you can use this command to create a conduwuit user:
@@ -29,12 +38,6 @@ In Debian or RHEL, you can use this command to create a conduwuit user:
sudo adduser --system conduwuit --group --disabled-login --no-create-home sudo adduser --system conduwuit --group --disabled-login --no-create-home
``` ```
For distros without `adduser`:
```bash
sudo useradd -r --shell /usr/bin/nologin --no-create-home conduwuit
```
## Forwarding ports in the firewall or the router ## 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. conduwuit uses the ports 443 and 8448 both of which need to be open in the firewall.
@@ -43,18 +46,45 @@ If conduwuit runs behind a router or in a container and has a different public I
## Setting up a systemd service ## Setting up a systemd service
The systemd unit for conduwuit can be found [here](../configuration.md#example-systemd-unit-file). You may need to change the `ExecStart=` path to where you placed the conduwuit binary. Now we'll set up a systemd service for conduwuit, so it's easy to start/stop conduwuit and set it to autostart when your
server reboots. Simply paste the default systemd service you can find below into
`/etc/systemd/system/conduwuit.service`.
```systemd
[Unit]
Description=conduwuit Matrix Server
After=network.target
[Service]
Environment="CONDUWUIT_CONFIG=/etc/conduwuit/conduwuit.toml"
User=conduwuit
Group=conduwuit
RuntimeDirectory=conduwuit
RuntimeDirectoryMode=0750
Restart=always
ExecStart=/usr/local/bin/conduwuit
[Install]
WantedBy=multi-user.target
```
Finally, run
```bash
$ sudo systemctl daemon-reload
```
## Creating the conduwuit configuration file ## Creating the conduwuit configuration file
Now we need to create the conduwuit's config file in `/etc/conduwuit/conduwuit.toml`. The example config can be found at [conduwuit-example.toml](../configuration.md).**Please take a moment to read it. You need to change at least the server name.** Now we need to create the conduwuit's config file in `/etc/conduwuit/conduwuit.toml`. Paste this in **and take a moment
to read it. You need to change at least the server name.**
RocksDB (`rocksdb`) is the only supported database backend. SQLite only exists for historical reasons and is not recommended. Any performance issues, storage issues, database issues, etc will not be assisted if using SQLite and you will be asked to migrate to RocksDB first.
RocksDB is the only supported database backend. SQLite only exists for historical reasons, is not recommended, and will be removed soon (likely in v0.5.0). Any performance issues, storage issues, database issues, etc will not be assisted if using SQLite and you will be asked to migrate to RocksDB first. See the following example config at [conduwuit-example.toml](../configuration.md)
## Setting the correct file permissions ## Setting the correct file permissions
If you are using a dedicated user for conduwuit, you will need to allow it to read the config. To do that you can run this command on As we are using a conduwuit specific user we need to allow it to read the config. To do that you can run this command on
Debian or RHEL: Debian or RHEL:
```bash ```bash
@@ -72,9 +102,7 @@ sudo chmod 700 /var/lib/conduwuit/
## Setting up the Reverse Proxy ## Setting up the Reverse Proxy
Refer to the documentation or various guides online of your chosen reverse proxy software. A [Caddy](https://caddyserver.com/) example will be provided as this is the recommended reverse proxy for new users and is very trivial to use (handles TLS, reverse proxy headers, etc transparently with proper defaults). Refer to the documentation or various guides online of your chosen reverse proxy software. A Caddy example will be provided as this is the recommended reverse proxy for new users and is very trivial.
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` to prevent this.
### Caddy ### Caddy
@@ -90,10 +118,10 @@ your.server.name, your.server.name:8448 {
} }
``` ```
That's it! Just start and enable the service and you're set. That's it! Just start or enable the service and you're set.
```bash ```bash
$ sudo systemctl enable --now caddy $ sudo systemctl enable caddy
``` ```
## You're done! ## You're done!
@@ -114,7 +142,7 @@ $ sudo systemctl enable conduwuit
You can open [a Matrix client](https://matrix.org/ecosystem/clients), enter your homeserver and try to register. You can open [a Matrix client](https://matrix.org/ecosystem/clients), enter your homeserver and try to register.
You can also use these commands as a quick health check (replace `your.server.name`). You can also use these commands as a quick health check.
```bash ```bash
$ curl https://your.server.name/_conduwuit/server_version $ curl https://your.server.name/_conduwuit/server_version
+3 -12
View File
@@ -1,6 +1,6 @@
# conduwuit for NixOS # conduwuit for NixOS
conduwuit can be acquired by [Lix][lix] from various places: conduwuit can be acquired by Nix from various places:
* The `flake.nix` at the root of the repo * The `flake.nix` at the root of the repo
* The `default.nix` at the root of the repo * The `default.nix` at the root of the repo
@@ -10,17 +10,9 @@ A binary cache for conduwuit that the CI/CD publishes to is available at the
following places (both are the same just different names): following places (both are the same just different names):
``` ```
https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduit
conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk=
https://attic.kennel.juneis.dog/conduwuit
conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE=
```
The binary caches have been recreated recently due to attic issues. The old public keys were:
```
conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg= conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg=
https://attic.kennel.juneis.dog/conduwuit
conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw= conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=
``` ```
@@ -30,10 +22,9 @@ The `flake.nix` and `default.nix` do not (currently) provide a NixOS module, so
(for now) [`services.matrix-conduit`][module] from Nixpkgs should be used to (for now) [`services.matrix-conduit`][module] from Nixpkgs should be used to
configure conduwuit. configure conduwuit.
If you want to run the latest code, you should get conduwuit from the `flake.nix` 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] or `default.nix` and set [`services.matrix-conduit.package`][package]
appropriately. appropriately.
[lix]: https://lix.systems/
[module]: https://search.nixos.org/options?channel=unstable&query=services.matrix-conduit [module]: https://search.nixos.org/options?channel=unstable&query=services.matrix-conduit
[package]: https://search.nixos.org/options?channel=unstable&query=services.matrix-conduit.package [package]: https://search.nixos.org/options?channel=unstable&query=services.matrix-conduit.package
+3 -2
View File
@@ -7,7 +7,7 @@ it, you can safely ignore this section. If you plan on contributing, see the
## Debugging with `tokio-console` ## Debugging with `tokio-console`
[`tokio-console`][1] can be a useful tool for debugging and profiling. To make [`tokio-console`][1] can be a useful tool for debugging and profiling. To make
a `tokio-console`-enabled build of conduwuit, enable the `tokio_console` feature, a `tokio-console`-enabled build of Conduwuit, enable the `tokio_console` feature,
disable the default `release_max_log_level` feature, and set the disable the default `release_max_log_level` feature, and set the
`--cfg tokio_unstable` flag to enable experimental tokio APIs. A build might `--cfg tokio_unstable` flag to enable experimental tokio APIs. A build might
look like this: look like this:
@@ -16,7 +16,8 @@ look like this:
RUSTFLAGS="--cfg tokio_unstable" cargo build \ RUSTFLAGS="--cfg tokio_unstable" cargo build \
--release \ --release \
--no-default-features \ --no-default-features \
--features=rocksdb,systemd,element_hacks,gzip_compression,brotli_compression,zstd_compression,tokio_console --features
backend_rocksdb,systemd,element_hacks,sentry_telemetry,gzip_compression,brotli_compression,zstd_compression,tokio_console
``` ```
[1]: https://docs.rs/tokio-console/latest/tokio_console/ [1]: https://docs.rs/tokio-console/latest/tokio_console/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

-93
View File
@@ -1,93 +0,0 @@
# Hot Reloading ("Live" Development)
### Summary
When developing in debug-builds with the nightly toolchain, conduwuit is modular using dynamic libraries and various parts of the application are hot-reloadable while the server is running: http api handlers, admin commands, services, database, etc. These are all split up into individual workspace crates as seen in the `src/` directory. Changes to sourcecode in a crate rebuild that crate and subsequent crates depending on it. Reloading then occurs for the changed crates.
Release builds still produce static binaries which are unaffected. Rust's soundness guarantees are in full force. Thus you cannot hot-reload release binaries.
### Requirements
Currently, this development setup only works on x86_64 and aarch64 Linux glibc. [musl explicitly does not support hot reloadable libraries, and does not implement `dlclose`][2]. macOS does not fully support our usage of `RTLD_GLOBAL` possibly due to some thread-local issues. [This Rust issue][3] may be of relevance, specifically [this comment][4]. It may be possible to get it working on only very modern macOS versions such as at least Sonoma, as currently loading dylibs is supported, but not unloading them in our setup, and the cited comment mentions an Apple WWDC confirming there have been TLS changes to somewhat make this possible.
As mentioned above this requires the nightly toolchain. This is due to reliance on various Cargo.toml features that are only available on nightly, most specifically `RUSTFLAGS` in Cargo.toml. Some of the implementation could also be simpler based on other various nightly features. We hope lots of nightly features start making it out of nightly sooner as there have been dozens of very helpful features that have been stuck in nightly ("unstable") for at least 5+ years that would make this simpler. We encourage greater community consensus to move these features into stability.
This currently only works on x86_64/aarch64 Linux with a glibc C library. musl C library, macOS, and likely other host architectures are not supported (if other architectures work, feel free to let us know and/or make a PR updating this). This should work on GNU ld and lld (rust-lld) and gcc/clang, however if you happen to have linker issues it's recommended to try using `mold` or `gold` linkers, and please let us know in the [conduwuit Matrix room][7] the linker error and what linker solved this issue so we can figure out a solution. Ideally there should be minimal friction to using this, and in the future a build script (`build.rs`) may be suitable to making this easier to use if the capabilities allow us.
### Usage
As of 19 May 2024, the instructions for using this are:
0. Have patience. Don't hesitate to join the [conduwuit Matrix room][7] to receive help using this. As indicated by the various rustflags used and some of the interesting issues linked at the bottom, this is definitely not something the Rust ecosystem or toolchain is used to doing.
1. Install the nightly toolchain using rustup. You may need to use `rustup override set nightly` in your local conduwuit directory, or use `cargo +nightly` for all actions.
2. Uncomment `cargo-features` at the top level / root Cargo.toml
3. Scroll down to the `# Developer profile` section and uncomment ALL the rustflags for each dev profile and their respective packages.
4. In each workspace crate's Cargo.toml (everything under `src/*` AND `deps/rust-rocksdb/Cargo.toml`), uncomment the `dylib` crate type under `[lib]`.
5. Due to [this rpath issue][5], you must export the `LD_LIBRARY_PATH` environment variable to your nightly Rust toolchain library directory. If using rustup (hopefully), use this: `export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/`
6. Start the server. You can use `cargo +nightly run` for this along with the standard.
7. Make some changes where you need to.
8. In a separate terminal window in the same directory (or using a terminal multiplexer like tmux), run the *build* Cargo command `cargo +nightly build`. Cargo should only rebuild what was changed / what's necessary, so it should not be rebuilding all the crates.
9. In your conduwuit server terminal, hit/send `CTRL+C` signal. This will tell conduwuit to find which libraries need to be reloaded, and reloads them as necessary.
10. If there were no errors, it will tell you it successfully reloaded `#` modules, and your changes should now be visible. Repeat 7 - 9 as needed.
To shutdown conduwuit in this setup, hit/send `CTRL+\`. Normal builds still shutdown with `CTRL+C` as usual.
Steps 1 - 5 are the initial first-time steps for using this. To remove the hot reload setup, revert/comment all the Cargo.toml changes.
As mentioned in the requirements section, if you happen to have some linker issues, try using the `-fuse-ld=` rustflag and specify mold or gold in all the `rustflags` definitions in the top level Cargo.toml, and please let us know in the [conduwuit Matrix room][7] the problem. mold can be installed typically through your distro, and gold is provided by the binutils package.
It's possible a helper script can be made to do all of this, or most preferably a specially made build script (build.rs). `cargo watch` support will be implemented soon which will eliminate the need to manually run `cargo build` all together.
### Addendum
Conduit was inherited as a single crate without modularity or reloading in its design. Reasonable partitioning and abstraction allowed a split into several crates, though many circular dependencies had to be corrected. The resulting crates now form a directed graph as depicted in figures below. The interfacing between these crates is still extremely broad which is not mitigable.
Initially [hot_lib_reload][6] was investigated but found appropriate for a project designed with modularity through limited interfaces, not a large and complex existing codebase. Instead a bespoke solution built directly on [libloading][8] satisfied our constraints. This required relatively minimal modifications and zero maintenance burden compared to what would be required otherwise. The technical difference lies with relocation processing: we leverage global bindings (`RTLD_GLOBAL`) in a very intentional way. Most libraries and off-the-shelf module systems (such as [hot_lib_reload][6]) restrict themselves to local bindings (`RTLD_LOCAL`). This allows them to release software to multiple platforms with much greater consistency, but at the cost of burdening applications to explicitly manage these bindings. In our case with an optional feature for developers, we shrug any such requirement to enjoy the cost/benefit on platforms where global relocations are properly cooperative.
To make use of `RTLD_GLOBAL` the application has to be oriented as a directed acyclic graph. The primary rule is simple and illustrated in the figure below: **no crate is allowed to call a function or use a variable from a crate below it.**
![conduwuit's dynamic library setup diagram - created by Jason Volk](assets/libraries.png)
When a symbol is referenced between crates they become bound: **crates cannot be unloaded until their calling crates are first unloaded.** Thus we start the reloading process from the crate which has no callers. There is a small problem though: the first crate is called by the base executable itself! This is solved by using an `RTLD_LOCAL` binding for just one link between the main executable and the first crate, freeing the executable from all modules as no global binding ever occurs between them.
![conduwuit's reload and load order diagram - created by Jason Volk](assets/reload_order.png)
Proper resource management is essential for reliable reloading to occur. This is a very basic ask in RAII-idiomatic Rust and the exposure to reloading hazards is remarkably low, generally stemming from poor patterns and practices. Unfortunately static analysis doesn't enforce reload-safety programmatically (though it could one day), for now hazards can be avoided by knowing a few basic do's and dont's:
1. Understand that code is memory. Just like one is forbidden from referencing free'd memory, one must not transfer control to free'd code. Exposure to this is primarily from two things:
- Callbacks, which this project makes very little use of.
- Async tasks, which are addressed below.
2. Tie all resources to a scope or object lifetime with greatest possible symmetry (locality). For our purposes this applies to code resources, which means async blocks and tokio tasks.
- **Never spawn a task without receiving and storing its JoinHandle**.
- **Always wait on join handles** before leaving a scope or in another cleanup function called by an owning scope.
3. Know any minor specific quirks documented in code or here:
- Don't use `tokio::spawn`, instead use our `Handle` in `core/server.rs`, which is reachable in most of the codebase via `services()` or other state. This is due to some bugs or assumptions made in tokio, as it happens in `unsafe {}` blocks, which are mitigated by circumventing some thread-local variables. Using runtime handles is good practice in any case.
The initial implementation PR is available [here][1].
### Interesting related issues/bugs
- [DT_RUNPATH produced in binary with rpath = true is wrong (cargo)][5]
- [Disabling MIR Optimization in Rust Compilation (cargo)](https://internals.rust-lang.org/t/disabling-mir-optimization-in-rust-compilation/19066/5)
- [Workspace-level metadata (cargo-deb)](https://github.com/kornelski/cargo-deb/issues/68)
[1]: https://github.com/girlbossceo/conduwuit/pull/387
[2]: https://wiki.musl-libc.org/functional-differences-from-glibc.html#Unloading-libraries
[3]: https://github.com/rust-lang/rust/issues/28794
[4]: https://github.com/rust-lang/rust/issues/28794#issuecomment-368693049
[5]: https://github.com/rust-lang/cargo/issues/12746
[6]: https://crates.io/crates/hot-lib-reloader/
[7]: https://matrix.to/#/#conduwuit:puppygock.gay
[8]: https://crates.io/crates/libloading
+4 -7
View File
@@ -5,16 +5,13 @@
Have a look at [Complement's repository][complement] for an explanation of what Have a look at [Complement's repository][complement] for an explanation of what
it is. it is.
To test against Complement, with [Lix][lix] and direnv installed and set up, you can: To test against Complement, with Nix and direnv installed and set up, you can
either:
* Run `./bin/complement "$COMPLEMENT_SRC" ./path/to/logs.jsonl ./path/to/results.jsonl` * Run `./bin/complement "$COMPLEMENT_SRC" ./path/to/logs.jsonl ./path/to/results.jsonl`
to build a Complement image, run the tests, and output the logs and results to build a Complement image, run the tests, and output the logs and results
to the specified paths. This will also output the OCI image at `result` to the specified paths
* Run `nix build .#complement` from the root of the repository to just build a * Run `nix build .#complement` from the root of the repository to just build a
Complement OCI image outputted to `result` (it's a `.tar.gz` file) Complement image
* Or download the latest Complement OCI image from the CI workflow artifacts output
from the commit/revision you want to test (e.g. from main) [here][ci-workflows]
[lix]: https://lix.systems/
[ci-workflows]: https://github.com/girlbossceo/conduwuit/actions/workflows/ci.yml?query=event%3Apush+is%3Asuccess+actor%3Agirlbossceo
[complement]: https://github.com/matrix-org/complement [complement]: https://github.com/matrix-org/complement
+31 -72
View File
@@ -5,56 +5,42 @@
Outgoing typing indicators, outgoing read receipts, **and** outgoing presence! Outgoing typing indicators, outgoing read receipts, **and** outgoing presence!
## Performance: ## Performance:
- Concurrency support for individual homeserver key fetching for faster remote room joins and room joins that will error less frequently - Concurrency support for key fetching for faster remote room joins and room joins that will error less frequently
- Send `Cache-Control` response header with `immutable` and 1 year cache length for all media requests (download and thumbnail) to instruct clients to cache media, and reduce server load from media requests that could be otherwise cached - Send `Cache-Control` response header with `immutable` and 1 year cache length for all media requests to instruct clients to cache media, and reduce server load from media requests that could be otherwise cached
- Add feature flags and config options to enable/build with zstd, brotli, and/or gzip HTTP body compression (response and request) - Add feature flags and config options to enable/build with zstd, brotli, and/or gzip HTTP body compression (response and request)
- Eliminate all usage of the thread-blocking `getaddrinfo(3)` call upon DNS queries, significantly improving federation latency/ping and cache DNS results (NXDOMAINs, successful queries, etc) using hickory-dns / hickory-resolver - Eliminate all usage of the thread-blocking `getaddrinfo(3)` call upon DNS queries, significantly improving federation latency/ping and cache DNS results (NXDOMAINs, successful queries, etc) using hickory-dns / hickory-resolver
- Enable HTTP/2 support on all requests
- Vastly improve RocksDB default settings to use new features that help with performance significantly, uses settings tailored to SSDs, various ways to tweak RocksDB, and a conduwuit setting to tell RocksDB to use settings that are tailored to HDDs or slow spinning rust storage or buggy filesystems. - Vastly improve RocksDB default settings to use new features that help with performance significantly, uses settings tailored to SSDs, various ways to tweak RocksDB, and a conduwuit setting to tell RocksDB to use settings that are tailored to HDDs or slow spinning rust storage or buggy filesystems.
- Add a Cargo build profile for aggressive build-time performance optimisations for release builds (1 codegen unit, no debug, fat LTO, etc, and optimise all crates with same)
- Implement database flush and cleanup conduwuit operations when using RocksDB - Implement database flush and cleanup conduwuit operations when using RocksDB
- Implement RocksDB write buffer corking and coalescing in database write-heavy areas - Implement RocksDB write buffer corking and coalescing in database write-heavy areas
- Perform connection pooling and keepalives where necessary to significantly improve federation performance and latency - Perform connection pooling and keepalives where necessary to significantly improve federation performance and latency
- Various config options to tweak connection pooling, request timeouts, connection timeouts, DNS timeouts and settings, etc with good defaults which also help huge with performance via reusing connections and retrying where needed - Various config options to tweak connection pooling, request timeouts, connection timeouts, DNS timeouts and settings, etc with good defaults which also help huge with performance via reusing connections and retrying where needed
- Properly get and use the amount of parallelism / tokio workers - Implement building conduwuit with jemalloc (which extends to the RocksDB jemalloc feature for maximum gains) or hardened_malloc light variant, and produce CI builds with jemalloc for performance (Nix doesn't seem to build [hardened_malloc-rs](https://github.com/girlbossceo/hardened_malloc-rs) properly)
- Implement building conduwuit with jemalloc (which extends to the RocksDB jemalloc feature for maximum gains) or hardened_malloc light variant, and io_uring support, and produce CI builds with jemalloc and io_uring by default for performance (Nix doesn't seem to build [hardened_malloc-rs](https://github.com/girlbossceo/hardened_malloc-rs) properly) - Add support for caching DNS results with hickory-dns / hickory-resolver in conduwuit (not a replacement for a proper resolver cache, but still far better than nothing)
- Add support for caching DNS results with hickory-dns / hickory-resolver in conduwuit (not a replacement for a proper resolver cache, but still far better than nothing), also properly falls back on TCP for UDP errors or if a SRV response is too large
- Add config option for using DNS over TCP, and config option for controlling A/AAAA record lookup strategy (e.g. don't query AAAA records if you don't have IPv6 connectivity) - Add config option for using DNS over TCP, and config option for controlling A/AAAA record lookup strategy (e.g. don't query AAAA records if you don't have IPv6 connectivity)
- Overall significant database, Client-Server, and federation performance and latency improvements (check out the ping room leaderboards if you don't believe me :>) - Overall significant database, Client-Server, and federation performance and latency improvements (check out the ping room leaderboards if you don't believe me :>)
- Add config options for RocksDB compression and bottommost compression, including choosing the algorithm and compression level - Add config options for RocksDB compression and bottommost compression, including choosing the algorithm and compression level
- Use [loole](https://github.com/mahdi-shojaee/loole) MPSC channels instead of tokio MPSC channels for huge performance boosts in sending channels (mainly relevant for federation) and presence channels - Use [loole](https://github.com/mahdi-shojaee/loole) MPSC channels instead of tokio MPSC channels for huge performance boosts in sending channels (mainly relevant for federation) and presence channels
- Use `tracing`/`log`'s `release_max_level_info` feature to improve performance, build speeds, binary size, and CPU usage in release builds by avoid compiling debug/trace log level macros that users will generally never use (can be disabled with a build-time feature flag) - Use `tracing`/`log`'s `release_max_level_info` feature to improve performance, build speeds, binary size, and CPU usage in release builds by avoid compiling debug/trace log level macros that users will generally never use (can be disabled with a build-time feature flag)
- Remove some unnecessary checks on EDU handling for incoming transactions, effectively speeding them up
- Simplify, dedupe, etc huge chunks of the codebase, including some that were unnecessary overhead, binary bloats, or preventing compiler/linker optimisations
## General Fixes/Features: ## General Fixes:
- Add legacy Element client hack fixing password changes and deactivations on legacy Element Android/iOS due to usage of an unspecced `user` field for UIAA
- Raise and improve all the various request timeouts making some things like room joins and client bugs error less or none at all than they should, and make them all user configurable - Raise and improve all the various request timeouts making some things like room joins and client bugs error less or none at all than they should, and make them all user configurable
- Add missing `reason` field to user ban events (`/ban`) - Add missing `reason` field to user ban events (`/ban`)
- Safer and cleaner shutdowns across incoming/outgoing requests (graceful shutdown) and the database - Fixed spec compliance issue with room version 8 - 11 joins (https://github.com/matrix-org/synapse/issues/16717 / https://github.com/matrix-org/matrix-spec/issues/1708)
- Safer and cleaner shutdowns on both database side as we run cleanup on shutdown and exits database loop better (no potential hanging issues in database loop), overall cleaner shutdown logic
- Stop sending `make_join` requests on room joins if 15 servers respond with `M_UNSUPPORTED_ROOM_VERSION` or `M_INVALID_ROOM_VERSION` - Stop sending `make_join` requests on room joins if 15 servers respond with `M_UNSUPPORTED_ROOM_VERSION` or `M_INVALID_ROOM_VERSION`
- Stop sending `make_join` requests if 50 servers cannot provide `make_join` for us - Stop sending `make_join` requests if 50 servers cannot provide `make_join` for us
- Respect *most* client parameters for `/media/` requests (`allow_redirect` still needs work) - Respect *most* client parameters for `/media/` requests (`allow_redirect` still needs work)
- Increased graceful shutdown timeout from a low 60 seconds to 180 seconds to avoid killing connections and let the remaining ones finish processing
- Return joined member count of rooms for push rules/conditions instead of a hardcoded value of 10 - Return joined member count of rooms for push rules/conditions instead of a hardcoded value of 10
- Make `CONDUIT_CONFIG` optional, relevant for container users that configure only by environment variables and no longer need to set `CONDUIT_CONFIG` to an empty string. - Make `CONDUIT_CONFIG` optional, relevant for container users that configure only by environment variables and no longer need to set `CONDUIT_CONFIG` to an empty string.
- Allow HEAD and PATCH (MSC4138) HTTP requests in CORS for clients (despite not being explicity mentioned in Matrix spec, HTTP spec says all HEAD requests need to behave the same as GET requests, Synapse supports HEAD requests) - Allow HEAD HTTP requests in CORS for clients (despite not being explicity mentioned in Matrix spec, HTTP spec says all HEAD requests need to behave the same as GET requests, Synapse supports HEAD requests)
- Fix using conduwuit with flake-compat on NixOS
- Resolve and remove some "features" from upstream that result in concurrency hazards, exponential backoff issues, or arbitrary performance limiters - Resolve and remove some "features" from upstream that result in concurrency hazards, exponential backoff issues, or arbitrary performance limiters
- Find more servers for outbound federation `/hierarchy` requests instead of just the room ID server name - Find more servers for outbound federation `/hierarchy` requests instead of just the room ID server name
- Support for suggesting servers to join through at `/_matrix/client/v3/directory/room/{roomAlias}` - Support for suggesting servers to join through at `/_matrix/client/v3/directory/room/{roomAlias}`
- Support for suggesting servers to join through us at `/_matrix/federation/v1/query/directory` - Support for suggesting servers to join through us at `/_matrix/federation/v1/query/directory`
- Misc edge-case search fixes (e.g. potentially missing some events) - Add workaround for [Out Of Your Element](https://gitdab.com/cadence/out-of-your-element) appservice bridge to make it functional on conduwuit (bug has already been reported)
- Misc `/sync` fixes (e.g. returning unnecessary data or incorrect/invalid responses)
- Add `replaces_state` and `prev_sender` in `unsigned` for state event changes which primarily makes Element's "See history" button on a state event functional
- Fix Conduit not allowing incoming federation requests for various world readable rooms
- Fix Conduit not respecting the client-requested file name on media requests
- Prevent sending junk / non-membership events to `/send_join` and `/send_leave` endpoints
- Only allow the requested membership type on `/send_join` and `/send_leave` endpoints (e.g. don't allow leave memberships on join endpoints)
- Prevent state key impersonation on `/send_join` and `/send_leave` endpoints
- Validate `X-Matrix` origin and request body `"origin"` field on incoming transactions
- Add `GET /_matrix/client/v1/register/m.login.registration_token/validity` endpoint
- Explicitly define support for sliding sync at `/_matrix/client/versions` (`org.matrix.msc3575`)
- Fix seeing empty status messages on user presences
## Moderation: ## Moderation:
@@ -63,15 +49,12 @@ Outgoing typing indicators, outgoing read receipts, **and** outgoing presence!
- Add support for serving `support` well-known from `[well_known.support]` (MSC1929) - Add support for serving `support` well-known from `[well_known.support]` (MSC1929)
- Config option to forbid publishing rooms to the room directory (`lockdown_public_room_directory`) except for admins - Config option to forbid publishing rooms to the room directory (`lockdown_public_room_directory`) except for admins
- Admin commands to delete room aliases and unpublish rooms from our room directory - Admin commands to delete room aliases and unpublish rooms from our room directory
- For all [`/report`](https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3roomsroomidreporteventid) requests: check if the reported event ID belongs to the reported room ID, raise report reasoning character limit to 750, fix broken formatting, make a small delayed random response per spec suggestion on privacy, and check if the sender user is in the reported room. - For all [`/report`](https://spec.matrix.org/v1.9/client-server-api/#post_matrixclientv3roomsroomidreporteventid) requests: check if the reported event ID belongs to the reported room ID, raise report reasoning character limit to 750, fix broken formatting, make a small delayed random response per spec suggestion on privacy, and check if the sender user is in the reported room.
- Support blocking servers from downloading remote media from, returning a 404 - Support blocking servers from downloading remote media from, returning a 404
- Don't allow `m.call.invite` events to be sent in public rooms (prevents calling the entire room) - Don't allow `m.call.invite` events to be sent in public rooms (prevents calling the entire room)
- On new public room creations, only allow moderators to send `m.call.invite`, `org.matrix.msc3401.call`, and `org.matrix.msc3401.call.member` events to prevent unprivileged users from calling the entire room - On new public room creations, only allow moderators to send `m.call.invite`, `org.matrix.msc3401.call`, and `org.matrix.msc3401.call.member` events
- Add support for a "global ACLs" feature (`forbidden_remote_server_names`) that blocks inbound remote room invites, room joins by room ID on server name, room joins by room alias on server name, incoming federated joins, and incoming federated room directory requests. This is very helpful for blocking servers that are purely toxic/bad and serve no value in allowing our users to suffer from things like room invite spam or such. Please note that this is not a substitute for room ACLs. - Add support for a "global ACLs" feature (`forbidden_remote_server_names`) that blocks inbound remote room invites, room joins by room ID on server name, room joins by room alias on server name, incoming federated joins, and incoming federated room directory requests. This is very helpful for blocking servers that are purely toxic/bad and serve no value in allowing our users to suffer from things like room invite spam or such. Please note that this is not a substitute for room ACLs.
- Add support for a config option to forbid our local users from sending federated room directory requests for (`forbidden_remote_room_directory_server_names`). Similar to above, useful for blocking servers that help prevent our users from wandering into bad areas of Matrix via room directories of those malicious servers. - Add support for a config option to forbid our local users from sending federated room directory requests for (`forbidden_remote_room_directory_server_names`). Similar to above, useful for blocking servers that help prevent our users from wandering into bad areas of Matrix via room directories of those malicious servers.
- Add config option for auto remediating/deactivating local non-admin users who attempt to join bad/forbidden rooms (`auto_deactivate_banned_room_attempts`)
- Deactivating users will remove their profile picture, blurhash, display name, and leave all rooms by default just like Synapse and for additional privacy
- Reject some EDUs from ACL'd users such as read receipts and typing indicators
## Privacy/Security: ## Privacy/Security:
@@ -84,10 +67,7 @@ Outgoing typing indicators, outgoing read receipts, **and** outgoing presence!
- Config option to block non-admin users from sending room invites or receiving remote room invites. Admin users are still allowed. - Config option to block non-admin users from sending room invites or receiving remote room invites. Admin users are still allowed.
- Config option to disable incoming and/or outgoing remote read receipts - Config option to disable incoming and/or outgoing remote read receipts
- Config option to disable incoming and/or outgoing remote typing indicators - Config option to disable incoming and/or outgoing remote typing indicators
- Config option to disable incoming, outgoing, and/or local presence and for timing out remote users - Config option to disable incoming, outgoing, and/or local presence
- Sanitise file names for the `Content-Disposition` header for all media requests (thumbnails, downloads, uploads)
- Media repository on handling `Content-Disposition` and `Content-Type` is fully spec compliant and secured
- Send secure default HTTP headers such as a strong restrictive CSP (see MSC4149), deny iframes, disable `X-XSS-Protection`, disable interest cohort in `Permission-Policy`, etc to mitigate any potential attack surface such as from untrusted media
## Administration/Logging: ## Administration/Logging:
@@ -96,45 +76,35 @@ Outgoing typing indicators, outgoing read receipts, **and** outgoing presence!
- Substantially clean up, improve, and fix logging (less noisy dead server logging, registration attempts, more useful troubleshooting logging, proper error propagation, etc) - Substantially clean up, improve, and fix logging (less noisy dead server logging, registration attempts, more useful troubleshooting logging, proper error propagation, etc)
- Configurable RocksDB logging (`LOG` files) with proper defaults (rotate, max size, verbosity, etc) to stop LOG files from accumulating so much - Configurable RocksDB logging (`LOG` files) with proper defaults (rotate, max size, verbosity, etc) to stop LOG files from accumulating so much
- Explicit startup error if your configuration allows open registration without a token or such like Synapse with a way to bypass it if needed - Explicit startup error if your configuration allows open registration without a token or such like Synapse with a way to bypass it if needed
- Replace the lightning bolt emoji option with support for setting any arbitrary text (e.g. another emoji) to suffix to all new user registrations, with a conduwuit default of "🏳️‍⚧️" - Replace the lightning bolt emoji option with support for setting any arbitrary text (e.g. another emoji) to suffix to all new user registrations, with a conduwuit default of 🏳️‍⚧️
- Implement config option to auto join rooms upon registration - Implement config option to auto join rooms upon registration
- Warn on unknown config options specified - Warn on unknown config options specified
- Add `/_conduwuit/server_version` route to return the version of conduwuit without relying on the federation API `/_matrix/federation/v1/version` - Add `/_conduwuit/server_version` route to return the version of conduwuit without relying on the federation API `/_matrix/federation/v1/version`
- Add `/_conduwuit/local_user_count` route to return the amount of registered active local users on your homeserver *if federation is enabled*
- Add configurable RocksDB recovery modes to aid in recovering corrupted RocksDB databases - Add configurable RocksDB recovery modes to aid in recovering corrupted RocksDB databases
- Support config options via `CONDUWUIT_` prefix and accessing non-global struct config options with the `__` split (e.g. `CONDUWUIT_WELL_KNOWN__SERVER`) - Support config options via `CONDUWUIT_` prefix
- Add support for listening on multiple TCP ports and multiple addresses - Add support for listening on multiple TCP ports
- Disable update check by default as it's not useful for conduwuit
- **Opt-in** Sentry.io telemetry and metrics, mainly used for crash reporting - **Opt-in** Sentry.io telemetry and metrics, mainly used for crash reporting
- Log the client IP on various requests such as registrations, banned room join attempts, logins, deactivations, federation transactions, etc
- Fix Conduit dropping some remote server federation response errors
## Maintenance/Stability: ## Maintenance/Stability:
- GitLab CI ported to GitHub Actions - GitLab CI ported to GitHub Actions
- Add support for the Matrix spec compliance test suite [Complement](https://github.com/matrix-org/complement/) via the Nix flake and various other fixes for it - Repo is mirrored to GitHub, GitLab, git.gay, sourcehut, and Codeberg (see README.md for their links)
- Implement running and diff'ing Complement results in CI and error if any mismatch occurs to prevent large cases of conduwuit regressions
- Repo is (officially) mirrored to GitHub, GitLab, git.gay, git.girlcock.ceo, sourcehut, and Codeberg (see README.md for their links)
- Docker container images published to GitLab Container Registry, GitHub Container Registry, and Dockerhub
- Extensively revamp the example config to be extremely helpful and useful to both new users and power users - Extensively revamp the example config to be extremely helpful and useful to 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 - 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 - 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 dependencies as up to date as possible - Has [Renovate](https://docs.renovatebot.com/), [Trivy](https://github.com/aquasecurity/trivy-action), and keeps ALL dependencies as up to date as possible
- Attempts and interest in removing extreme and unnecessary panics/unwraps/expects that can lead to denial of service or such (upstream and upstream contributors want this unusual behaviour for some reason) - Attempts and interest in removing extreme and unnecessary panics/unwraps/expects that can lead to denial of service or such (upstream and upstream contributors want this unusual behaviour for some reason)
- Purge unmaintained/irrelevant/broken database backends (heed, sled, persy) and other unnecessary code or overhead - Purge unmaintained/irrelevant/broken database backends (heed, sled, persy) and other unnecessary code or overhead
- webp support for images - webp support for images
- Add cargo audit support to CI - Add cargo audit support to CI
- CI tests for all sorts of feature matrixes (jemalloc, non-defaullt, all features, etc) - CI tests with all features
- Add static and dynamic linking smoke tests in CI to prevent any potential linking regressions for Complement, static binaries, Nix devshells, etc - Add timestamp by commit date support to building OCI images for keeping image build reproducibility and still have a meaningful "last modified date" for OCI image metadata
- Add timestamp by commit date when building OCI images for keeping image build reproducibility and still have a meaningful "last modified date" for OCI image - Update rusqlite/sqlite (not that you should be using it)
- Add timestamp by commit date via `SOURCE_DATE_EPOCH` for Debian packages
- Startup check if conduwuit running in a container and is listening on 127.0.0.1 (generally containers are using NAT networking and 0.0.0.0 is the intended listening address) - Startup check if conduwuit running in a container and is listening on 127.0.0.1 (generally containers are using NAT networking and 0.0.0.0 is the intended listening address)
- Add a panic catcher layer to return panic messages in HTTP responses if a panic occurs
## Admin Room: ## Admin Room:
- Add support for a console CLI interface that can issue admin commands and output them in your terminal
- Add support for an admin-user-only commandline admin room interface that can be issued in any room with the `\\!admin` or `\!admin` prefix and returns the response as yourself in the same room
- Add admin commands for uptime, server startup, server shutdown, and server restart
- Fix admin room handler to not panic/crash if the admin room command response fails (e.g. too large message) - Fix admin room handler to not panic/crash if the admin room command response fails (e.g. too large message)
- Add command to dynamically change conduwuit's tracing log level filter on the fly - Add command to dynamically change conduwuit's tracing log level filter on the fly
- Add admin command to fetch a server's `/.well-known/matrix/support` file - Add admin command to fetch a server's `/.well-known/matrix/support` file
@@ -148,26 +118,17 @@ Outgoing typing indicators, outgoing read receipts, **and** outgoing presence!
- Add admin command to bulk delete media via a codeblock list of MXC URLs. - Add admin command to bulk delete media via a codeblock list of MXC URLs.
- Add admin command to delete both the thumbnail and media MXC URLs from an event ID (e.g. from an abuse report) - Add admin command to delete both the thumbnail and media MXC URLs from an event ID (e.g. from an abuse report)
- Add admin command to list all the rooms a local user is joined in - Add admin command to list all the rooms a local user is joined in
- Add admin command to list joined members in a room - Add admin command to delete all remote media in the past X minutes as a form of deleting media that you don't want on your server that a remote user posted in a room
- Add admin command to view the room topic of a room - Add admin command to return a room's state
- Add admin command to delete all remote media in the past X minutes as a form of deleting media that you don't want on your server that a remote user posted in a room, a `--force` flag to ignore errors, and support for reading `last modified time` instead of `creation time` for filesystems that don't support file created metadata
- Add admin command to return a room's full/complete state
- Admin debug command to fetch a PDU from a remote server and inserts it into our database/timeline as backfill - Admin debug command to fetch a PDU from a remote server and inserts it into our database/timeline as backfill
- Add admin command to delete media via a specific MXC. This deletes the MXC from our database, and the file locally. - Add admin command to delete media via a specific MXC. This deletes the MXC from our database, and the file locally.
- Add admin commands for banning (blocking) room IDs from our local users joining (admins are always allowed) and evicts all our local users from that room, in addition to bulk room banning support, and blocks room invites (remote and local) to the banned room, as a moderation feature - Add admin commands for banning (blocking) room IDs from our local users joining (admins are always allowed) and evicts all our local users from that room, in addition to bulk room banning support, and blocks room invites (remote and local) to the banned room, as a moderation feature
- Add admin commands to output jemalloc memory stats and memory usage - Add admin commands to output jemalloc memory stats and memory usage
- Add admin command to get conduwuit's uptime
- Add admin command to get rooms a *remote* user shares with us - Add admin command to get rooms a *remote* user shares with us
- Add debug admin commands to get the earliest and latest PDU in a room
- Add debug admin command to echo a message
- Add admin command to insert rooms tags for a user, most useful for inserting the `m.server_notice` tag on your admin room to make it "persistent" in the "System Alerts" section of Element
- Add experimental admin debug command for Dendrite's `AdminDownloadState` (`/admin/downloadState/{serverName}/{roomID}`) admin API endpoint to download and use a remote server's room state in the room
- Disable URL previews by default in the admin room due to various command outputs having "URLs" in them that clients may needlessly render/request
- Extend memory usage admin server command to support showing memory allocator stats such as jemalloc's
- Add admin debug command to see memory allocator's full extended debug statistics such as jemalloc's
## Misc: ## Misc:
- Add guest support for accessing TURN servers via `turn_allow_guests` like Synapse
- Support for creating rooms with custom room IDs like Maunium Synapse (`room_id` request body field to `/createRoom`) - Support for creating rooms with custom room IDs like Maunium Synapse (`room_id` request body field to `/createRoom`)
- Query parameter `?format=event|content` for returning either the room state event's content (default) for the full room state event on ` /_matrix/client/v3/rooms/{roomId}/state/{eventType}[/{stateKey}]` requests (see https://github.com/matrix-org/matrix-spec/issues/1047) - Query parameter `?format=event|content` for returning either the room state event's content (default) for the full room state event on ` /_matrix/client/v3/rooms/{roomId}/state/{eventType}[/{stateKey}]` requests (see https://github.com/matrix-org/matrix-spec/issues/1047)
- Add **optional** feature flag to use SHA256 key names for media instead of base64 to overcome filesystem file name length limitations (OS error file name too long) - Add **optional** feature flag to use SHA256 key names for media instead of base64 to overcome filesystem file name length limitations (OS error file name too long)
@@ -179,15 +140,13 @@ Outgoing typing indicators, outgoing read receipts, **and** outgoing presence!
- Implement legacy Matrix `/v1/` media endpoints that some clients and servers may still call - Implement legacy Matrix `/v1/` media endpoints that some clients and servers may still call
- Config option to change Conduit's behaviour of homeserver key fetching (`query_trusted_key_servers_first`). This option sets whether conduwuit will query trusted notary key servers first before the individual homeserver(s), or vice versa which may help in joining certain rooms. - Config option to change Conduit's behaviour of homeserver key fetching (`query_trusted_key_servers_first`). This option sets whether conduwuit will query trusted notary key servers first before the individual homeserver(s), or vice versa which may help in joining certain rooms.
- Implement unstable MSC2666 support for querying mutual rooms with a user - Implement unstable MSC2666 support for querying mutual rooms with a user
- Implement unstable MSC4125 support for specifying servers to join via on federated invites - Assume well-knowns are broken if they exceed past 10000 characters.
- Make conduwuit build and be functional under Nix + macOS - Add support for the Matrix spec compliance test suite [Complement](https://github.com/matrix-org/complement/) via the Nix flake and various other fixes for it
- Log out all sessions after unsetting the emergency password
- Assume well-knowns are broken if they exceed past 12288 characters.
- Add support for listening on both HTTP and HTTPS if using direct TLS with conduwuit for usecases such as Complement - Add support for listening on both HTTP and HTTPS if using direct TLS with conduwuit for usecases such as Complement
- Implement running and diff'ing Complement results in CI
- Interest in supporting other operating systems such as macOS, BSDs, and Windows, and getting them added into CI and doing builds for them
- Add config option for disabling RocksDB Direct IO if needed - Add config option for disabling RocksDB Direct IO if needed
- Add various documentation on maintaining conduwuit, using RocksDB online backups, some troubleshooting, using admin commands, moderation documentation, etc
- (Developers): Add support for [hot reloadable/"live" modular development](development/hot_reload.md)
- (Developers): Add support for tokio-console - (Developers): Add support for tokio-console
- (Developers): Add support for tracing flame graphs - (Developers): Add support for tracing flame graphs
- Add `release-debuginfo` Cargo build profile
- No cryptocurrency donations allowed, conduwuit is fully maintained by independent queer maintainers, and with a strong priority on inclusitivity and comfort for protected groups 🏳️‍⚧️ - No cryptocurrency donations allowed, conduwuit is fully maintained by independent queer maintainers, and with a strong priority on inclusitivity and comfort for protected groups 🏳️‍⚧️
- [Add a community Code of Conduct for all conduwuit community spaces, primarily the Matrix space](https://conduwuit.puppyirl.gay/conduwuit_coc.html)
+2 -2
View File
@@ -1,4 +1,4 @@
# conduwuit # Conduwuit
{{#include ../README.md:catchphrase}} {{#include ../README.md:catchphrase}}
@@ -12,7 +12,7 @@ See the [differences](differences.md) page
- [Deployment options](deploying.md) - [Deployment options](deploying.md)
If you want to connect an appservice to conduwuit, take a look at the [appservices documentation](appservices.md). If you want to connect an Appservice to Conduwuit, take a look at the [appservices documentation](appservices.md).
#### How can I contribute? #### How can I contribute?
-63
View File
@@ -1,63 +0,0 @@
# Maintaining your conduwuit setup
## Moderation
conduwuit has moderation through admin room commands. "binary commands" (medium priority) and an admin API (low priority) is planned. Some moderation-related config options are available in the example config such as "global ACLs" and blocking media requests to certain servers. See the example config for the moderation config options under the "Moderation / Privacy / Security" section.
conduwuit has moderation admin commands for:
- managing room aliases (`!admin rooms alias`)
- managing room directory (`!admin rooms directory`)
- managing room banning/blocking and user removal (`!admin rooms moderation`)
- managing user accounts (`!admin users`)
- fetching `/.well-known/matrix/support` from servers (`!admin federation`)
- blocking incoming federation for certain rooms (not the same as room banning) (`!admin federation`)
- deleting media (see [the media section](#media))
Any commands with `-list` in them will require a codeblock in the message with each object being newline delimited. An example of doing this is:
````
!admin rooms moderation ban-list-of-rooms
```
!roomid1:server.name
!roomid2:server.name
!roomid3:server.name
```
````
## Database
If using RocksDB, there's very little you need to do. Compaction is ran automatically based on various defined thresholds tuned for conduwuit to be high performance with the least I/O amplifcation or overhead. Manually running compaction is not recommended, or compaction via a timer. RocksDB is built with io_uring support via liburing for async read I/O.
Some RocksDB settings can be adjusted such as the compression method chosen. See the RocksDB section in the [example config](configuration.md). btrfs users may benefit from disabling compression on RocksDB if CoW is in use.
RocksDB troubleshooting can be found [in the RocksDB section of troubleshooting](troubleshooting.md).
## Backups
Currently only RocksDB supports online backups. If you'd like to backup your database online without any downtime, see the `!admin server` command for the backup commands and the `database_backup_path` config options in the example config. Please note that the format of the database backup is not the exact same. This is unfortunately a bad design choice by Facebook as we are using the database backup engine API from RocksDB, however the data is still there and can still be joined together.
To restore a backup from an online RocksDB backup:
- shutdown conduwuit
- create a new directory for merging together the data
- in the online backup created, copy all `.sst` files in `$DATABASE_BACKUP_PATH/shared_checksum` to your new directory
- trim all the strings so instead of `######_sxxxxxxxxx.sst`, it reads `######.sst`. A way of doing this with sed and bash is `for file in *.sst; do mv "$file" "$(echo "$file" | sed 's/_s.*/.sst/')"; done`
- copy all the files in `$DATABASE_BACKUP_PATH/1` (or the latest backup number if you have multiple) to your new directory
- set your `database_path` config option to your new directory, or replace your old one with the new one you crafted
- start up conduwuit again and it should open as normal
If you'd like to do an offline backup, shutdown conduwuit and copy your `database_path` directory elsewhere. This can be restored with no modifications needed.
Backing up media is also just copying the `media/` directory from your database directory.
## Media
Media still needs various work, however conduwuit implements media deletion via:
- MXC URI or Event ID (unencrypted and attempts to find the MXC URI in the event)
- Delete list of MXC URIs
- Delete remote media in the past `N` seconds/minutes via filesystem metadata on the file created time (`btime`) or file modified time (`mtime`)
See the `!admin media` command for further information. All media in conduwuit is stored at `$DATABASE_DIR/media`. This will be configurable soon.
If you are finding yourself needing extensive granular control over media, we recommend looking into [Matrix Media Repo](https://github.com/t2bot/matrix-media-repo). conduwuit intends to implement various utilities for media, but MMR is dedicated to extensive media management.
Built-in S3 support is also planned, but for now using a "S3 filesystem" on `media/` works. conduwuit also sends a `Cache-Control` header of 1 year and immutable for all media requests (download and thumbnail) to reduce unnecessary media requests from browsers, reduce bandwidth usage, and reduce load.
-64
View File
@@ -1,64 +0,0 @@
# Troubleshooting conduwuit
> ## Docker users ⚠️
>
> Docker is extremely UX unfriendly. Because of this, a ton of issues or support is actually Docker support, not conduwuit support. We also cannot document the ever-growing list of Docker issues here.
>
> If you intend on asking for support and you are using Docker, **PLEASE** triple validate your issues are **NOT** because you have a misconfiguration in your Docker setup.
>
> If there are things like Compose file issues or Dockerhub image issues, those can still be mentioned as long as they're something we can fix.
## Rocksdb / database issues
#### Direct IO
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.md) for disabling it if needed. Issues from Direct IO on unsupported filesystems are usually shown as startup errors.
#### Database corruption
If your database is corrupted *and* is failing to start (e.g. checksum mismatch), it may be recoverable but careful steps must be taken, and there is no guarantee it may be recoverable.
The first thing that can be done is launching conduwuit with the `rocksdb_repair` config option set to true. This will tell RocksDB to attempt to repair itself at launch. If this does not work, disable the option and continue reading.
RocksDB has the following recovery modes:
- `TolerateCorruptedTailRecords`
- `AbsoluteConsistency`
- `PointInTime`
- `SkipAnyCorruptedRecord`
By default, conduwuit uses `TolerateCorruptedTailRecords` as generally these may be due to bad federation and we can re-fetch the correct data over federation. The RocksDB default is `PointInTime` which will attempt to restore a "snapshot" of the data when it was last known to be good. This data can be either a few seconds old, or multiple minutes prior. `PointInTime` may not be suitable for default usage due to clients and servers possibly not being able to handle sudden "backwards time travels", and `AbsoluteConsistency` may be too strict.
`AbsoluteConsistency` will fail to start the database if any sign of corruption is detected. `SkipAnyCorruptedRecord` will skip all forms of corruption unless it forbids the database from opening (e.g. too severe). Usage of `SkipAnyCorruptedRecord` voids any support as this may cause more damage and/or leave your database in a permanently inconsistent state, but it may do something if `PointInTime` does not work as a last ditch effort.
With this in mind:
- First start conduwuit with the `PointInTime` recovery method. See the [example config](configuration.md) for how to do this using `rocksdb_recovery_mode`
- If your database successfully opens, clients are recommended to clear their client cache to account for the rollback
- Leave your conduwuit running in `PointInTime` for at least 30-60 minutes so as much possible corruption is restored
- If all goes will, you should be able to restore back to using `TolerateCorruptedTailRecords` and you have successfully recovered your database
## Media
#### "File name too long"
If you are running into the "file name is too long" OS error for media requests, your filesystem cannot handle file name lengths >=255 characters. This is unfortuntely due to Conduit (upstream) using base64 for file name keys which is very problematic for some filesystems as the base64 input is untrusted and long file names or specific inputs can cause this. If you would like to avoid this, you may build conduwuit yourself with the `sha256_media` feature. **This will lose database compatibility with upstream**.
## Debugging
Note that users should not really be debugging things. If you find yourself debugging and find the issue, please let us know and/or how we can fix it. 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.
#### 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.
#### 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 `/_matrix/federation/v1/version`. Errors are outputted.
#### 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`
+14 -30
View File
@@ -1,41 +1,25 @@
# Setting up TURN/STURN # Setting up TURN/STURN
In order to make or receive calls, a TURN server is required. conduwuit suggests using [Coturn](https://github.com/coturn/coturn) for this purpose, which is also available as a Docker image. ## General instructions
### Configuration * It is assumed you have a [Coturn server](https://github.com/coturn/coturn) up and running. See [Synapse reference implementation](https://github.com/matrix-org/synapse/blob/develop/docs/turn-howto.md).
Create a configuration file called `coturn.conf` containing: ## Edit/Add a few settings to your existing conduit.toml
```conf
use-auth-secret
static-auth-secret=<a secret key>
realm=<your server domain>
``` ```
A common way to generate a suitable alphanumeric secret key is by using `pwgen -s 64 1`. # Refer to your Coturn settings.
# `your.turn.url` has to match the REALM setting of your Coturn as well as `transport`.
turn_uris = ["turn:your.turn.url?transport=udp", "turn:your.turn.url?transport=tcp"]
These same values need to be set in conduwuit. See the [example config](configuration.md) in the TURN section for configuring these and restart conduwuit after. # static-auth-secret of your turnserver
turn_secret = "ADD SECRET HERE"
### Run # If you have your TURN server configured to use a username and password
Run the [Coturn](https://hub.docker.com/r/coturn/coturn) image using # you can provide these information too. In this case comment out `turn_secret above`!
```bash #turn_username = ""
docker run -d --network=host -v $(pwd)/coturn.conf:/etc/coturn/turnserver.conf coturn/coturn #turn_password = ""
``` ```
or docker-compose. For the latter, paste the following section into a file called `docker-compose.yml` ## Apply settings
and run `docker compose up -d` in the same directory.
```yml Restart Conduit.
version: 3
services:
turn:
container_name: coturn-server
image: docker.io/coturn/coturn
restart: unless-stopped
network_mode: "host"
volumes:
- ./coturn.conf:/etc/coturn/turnserver.conf
```
To understand why the host networking mode is used and explore alternative configuration options, please visit [Coturn's Docker documentation](https://github.com/coturn/coturn/blob/master/docker/coturn/README.md).
For security recommendations see Synapse's [Coturn documentation](https://element-hq.github.io/synapse/latest/turn-howto.html).
+15 -46
View File
@@ -58,7 +58,7 @@ script = "lychee --version"
[[task]] [[task]]
name = "cargo-audit" name = "cargo-audit"
group = "security" group = "security"
script = "cargo audit -D warnings -D unmaintained -D unsound -D yanked" script = "cargo audit -D warnings -D unmaintained -D unsound -D yanked --ignore RUSTSEC-2020-0016"
[[task]] [[task]]
name = "cargo-fmt" name = "cargo-fmt"
@@ -69,10 +69,7 @@ script = "cargo fmt --check -- --color=always"
name = "cargo-doc" name = "cargo-doc"
group = "lints" group = "lints"
script = """ script = """
env DIRENV_DEVSHELL=all-features \ RUSTDOCFLAGS="-D warnings" cargo doc \
RUSTDOCFLAGS="-D warnings" \
direnv exec . \
cargo doc \
--workspace \ --workspace \
--all-features \ --all-features \
--no-deps \ --no-deps \
@@ -96,8 +93,6 @@ cargo clippy \
name = "clippy/all" name = "clippy/all"
group = "lints" group = "lints"
script = """ script = """
env DIRENV_DEVSHELL=all-features \
direnv exec . \
cargo clippy \ cargo clippy \
--workspace \ --workspace \
--all-targets \ --all-targets \
@@ -120,18 +115,18 @@ cargo clippy \
-D warnings -D warnings
""" """
#[[task]] [[task]]
#name = "clippy/hardened_malloc" name = "clippy/hardened_malloc"
#group = "lints" group = "lints"
#script = """ script = """
#cargo clippy \ cargo clippy \
# --workspace \ --workspace \
# --features hardened_malloc \ --features hardened_malloc \
# --all-targets \ --all-targets \
# --color=always \ --color=always \
# -- \ -- \
# -D warnings -D warnings
#""" """
[[task]] [[task]]
name = "lychee" name = "lychee"
@@ -139,11 +134,9 @@ group = "lints"
script = "lychee --verbose --offline docs *.md" script = "lychee --verbose --offline docs *.md"
[[task]] [[task]]
name = "cargo/all" name = "cargo"
group = "tests" group = "tests"
script = """ script = """
env DIRENV_DEVSHELL=all-features \
direnv exec . \
cargo test \ cargo test \
--workspace \ --workspace \
--all-targets \ --all-targets \
@@ -152,27 +145,3 @@ env DIRENV_DEVSHELL=all-features \
-- \ -- \
--color=always --color=always
""" """
[[task]]
name = "cargo/default"
group = "tests"
script = """
cargo test \
--workspace \
--all-targets \
--color=always \
-- \
--color=always
"""
# 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
# our other tests. We've had linking problems in the past with dynamic
# jemalloc builds that usually show up as an immediate segfault or "invalid free"
[[task]]
name = "nix-default"
group = "tests"
script = """
nix run .#default -- --help
"""
Generated
+22 -389
View File
@@ -23,36 +23,14 @@
"type": "github" "type": "github"
} }
}, },
"cachix": {
"inputs": {
"devenv": "devenv",
"flake-compat": "flake-compat_3",
"nixpkgs": "nixpkgs_3",
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1717420532,
"narHash": "sha256-OCCmI69EMaA4BcxRKrXJsx5Ozua2f/PKEy4aJbE7ziM=",
"owner": "cachix",
"repo": "cachix",
"rev": "5727f0676f08a4b41ed13d403ec64dcce989f6e5",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "master",
"repo": "cachix",
"type": "github"
}
},
"complement": { "complement": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1715700731, "lastModified": 1714472853,
"narHash": "sha256-cie+b5N/TQAFD8vF/XbqfyFJkFU0qUPDbtJQDm/TfQc=", "narHash": "sha256-CNRHSZe3TE+3tFj2dHNyxTMjDqL0MKY3P/3jqUgA7YE=",
"owner": "matrix-org", "owner": "matrix-org",
"repo": "complement", "repo": "complement",
"rev": "8587fb3cbe746754b2c883ff6c818ca4d987d0a5", "rev": "891d18872c153d39a9ce63b545045efddb845738",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -90,11 +68,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1716569590, "lastModified": 1713738183,
"narHash": "sha256-5eDbq8TuXFGGO3mqJFzhUbt5zHVTf5zilQoyW5jnJwo=", "narHash": "sha256-qd/MuLm7OfKQKyd4FAMqV4H6zYyOfef5lLzRrmXwKJM=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "109987da061a1bf452f435f1653c47511587d919", "rev": "f6c6a2fb1b8bd9b65d65ca9342dd0eb180a63f11",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -104,35 +82,6 @@
"type": "github" "type": "github"
} }
}, },
"devenv": {
"inputs": {
"flake-compat": [
"cachix",
"flake-compat"
],
"nix": "nix",
"nixpkgs": "nixpkgs_2",
"poetry2nix": "poetry2nix",
"pre-commit-hooks": [
"cachix",
"pre-commit-hooks"
]
},
"locked": {
"lastModified": 1708704632,
"narHash": "sha256-w+dOIW60FKMaHI1q5714CSibk99JfYxm0CzTinYWr+Q=",
"owner": "cachix",
"repo": "devenv",
"rev": "2ee4450b0f4b95a1b90f2eb5ffea98b90e48c196",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "python-rewrite",
"repo": "devenv",
"type": "github"
}
},
"fenix": { "fenix": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@@ -141,11 +90,11 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1716359173, "lastModified": 1714544767,
"narHash": "sha256-pYcjP6Gy7i6jPWrjiWAVV0BCQp+DdmGaI/k65lBb/kM=", "narHash": "sha256-kF1bX+YFMedf1g0PAJYwGUkzh22JmULtj8Rm4IXAQKs=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "b6fc5035b28e36a98370d0eac44f4ef3fd323df6", "rev": "73124e1356bde9411b163d636b39fe4804b7ca45",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -172,54 +121,6 @@
} }
}, },
"flake-compat_2": { "flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_3": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_4": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_5": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1696426674, "lastModified": 1696426674,
@@ -255,42 +156,6 @@
"inputs": { "inputs": {
"systems": "systems" "systems": "systems"
}, },
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_4": {
"inputs": {
"systems": "systems_3"
},
"locked": { "locked": {
"lastModified": 1710146030, "lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
@@ -306,70 +171,6 @@
"type": "github" "type": "github"
} }
}, },
"gitignore": {
"inputs": {
"nixpkgs": [
"cachix",
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"liburing": {
"flake": false,
"locked": {
"lastModified": 1716565485,
"narHash": "sha256-4R19aJNQYs6vb0/Hz4bWT56YN1P1DkFL/sxdE4Yj0CE=",
"owner": "axboe",
"repo": "liburing",
"rev": "b90c0e670a93caabbebe2d9e24ff85cece4cfe0e",
"type": "github"
},
"original": {
"owner": "axboe",
"ref": "master",
"repo": "liburing",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": "flake-compat_2",
"nixpkgs": [
"cachix",
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1708577783,
"narHash": "sha256-92xq7eXlxIT5zFNccLpjiP7sdQqQI30Gyui2p/PfKZM=",
"owner": "domenkozar",
"repo": "nix",
"rev": "ecd0af0c1f56de32cbad14daa1d82a132bf298f8",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.21",
"repo": "nix",
"type": "github"
}
},
"nix-filter": { "nix-filter": {
"locked": { "locked": {
"lastModified": 1710156097, "lastModified": 1710156097,
@@ -386,29 +187,6 @@
"type": "github" "type": "github"
} }
}, },
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"cachix",
"devenv",
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688870561,
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1711401922, "lastModified": 1711401922,
@@ -425,22 +203,6 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-stable": { "nixpkgs-stable": {
"locked": { "locked": {
"lastModified": 1711460390, "lastModified": 1711460390,
@@ -457,45 +219,13 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-stable_2": {
"locked": {
"lastModified": 1710695816,
"narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "614b4613980a522ba49f0d194531beddbb7220d3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1692808169, "lastModified": 1713537308,
"narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=", "narHash": "sha256-XtTSSIB2DA6tOv+l0FhvfDMiyCmhoRbNB+0SeInZkbk=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "9201b5ff357e781bf014d0330d18555695df7ba8", "rev": "5c24cf2f0a12ad855f444c30b2421d044120c66f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1715534503,
"narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2057814051972fa1453ddfb0d98badbea9b83c06",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -505,84 +235,19 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_4": {
"locked": {
"lastModified": 1716330097,
"narHash": "sha256-8BO3B7e3BiyIDsaKA0tY8O88rClYRTjvAp66y+VBUeU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5710852ba686cc1fd0d3b8e22b3117d43ba374c2",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils_2",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"cachix",
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1692876271,
"narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat_4",
"flake-utils": "flake-utils_3",
"gitignore": "gitignore",
"nixpkgs": [
"cachix",
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable_2"
},
"locked": {
"lastModified": 1715609711,
"narHash": "sha256-/5u29K0c+4jyQ8x7dUIEUWlz2BoTSZWUP2quPwFCE7M=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "c182c876690380f8d3b9557c4609472ebfa1b141",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"rocksdb": { "rocksdb": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1716773462, "lastModified": 1714770052,
"narHash": "sha256-5kUH+XK+2lbFfUgbxuNy3YMLHbp6scfWPdtc8za1wDM=", "narHash": "sha256-NCPYF2wYBsB9OHEkZSOYoPlxjC9BBMhJp8EM5M1o3Mc=",
"owner": "girlbossceo", "owner": "girlbossceo",
"repo": "rocksdb", "repo": "rocksdb",
"rev": "c8a1450231e9c608edf535538dbe8ca1a8d2f3bc", "rev": "db6df0b185774778457dabfcbd822cb81760cade",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "girlbossceo", "owner": "girlbossceo",
"ref": "v9.2.1", "ref": "v9.1.1",
"repo": "rocksdb", "repo": "rocksdb",
"type": "github" "type": "github"
} }
@@ -590,26 +255,24 @@
"root": { "root": {
"inputs": { "inputs": {
"attic": "attic", "attic": "attic",
"cachix": "cachix",
"complement": "complement", "complement": "complement",
"crane": "crane_2", "crane": "crane_2",
"fenix": "fenix", "fenix": "fenix",
"flake-compat": "flake-compat_5", "flake-compat": "flake-compat_2",
"flake-utils": "flake-utils_4", "flake-utils": "flake-utils_2",
"liburing": "liburing",
"nix-filter": "nix-filter", "nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_4", "nixpkgs": "nixpkgs_2",
"rocksdb": "rocksdb" "rocksdb": "rocksdb"
} }
}, },
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1716107283, "lastModified": 1713628977,
"narHash": "sha256-NJgrwLiLGHDrCia5AeIvZUHUY7xYGVryee0/9D3Ir1I=", "narHash": "sha256-iN5QUlUq527lswmBC+RopfXdu6Xx7mmTaBSH2l59FtM=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "21ec8f523812b88418b2bfc64240c62b3dd967bd", "rev": "55d9a533b309119c8acd13061581b43ae8840823",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -633,36 +296,6 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",
+77 -90
View File
@@ -1,7 +1,6 @@
{ {
inputs = { inputs = {
attic.url = "github:zhaofengli/attic?ref=main"; attic.url = "github:zhaofengli/attic?ref=main";
cachix.url = "github:cachix/cachix?ref=master";
complement = { url = "github:matrix-org/complement?ref=main"; flake = false; }; complement = { url = "github:matrix-org/complement?ref=main"; flake = false; };
crane = { url = "github:ipetkov/crane?ref=master"; inputs.nixpkgs.follows = "nixpkgs"; }; crane = { url = "github:ipetkov/crane?ref=master"; inputs.nixpkgs.follows = "nixpkgs"; };
fenix = { url = "github:nix-community/fenix?ref=main"; inputs.nixpkgs.follows = "nixpkgs"; }; fenix = { url = "github:nix-community/fenix?ref=main"; inputs.nixpkgs.follows = "nixpkgs"; };
@@ -10,15 +9,13 @@
nix-filter.url = "github:numtide/nix-filter?ref=main"; nix-filter.url = "github:numtide/nix-filter?ref=main";
nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
# https://github.com/girlbossceo/rocksdb/commit/db6df0b185774778457dabfcbd822cb81760cade # https://github.com/girlbossceo/rocksdb/commit/db6df0b185774778457dabfcbd822cb81760cade
rocksdb = { url = "github:girlbossceo/rocksdb?ref=v9.2.1"; flake = false; }; rocksdb = { url = "github:girlbossceo/rocksdb?ref=v9.1.1"; flake = false; };
liburing = { url = "github:axboe/liburing?ref=master"; flake = false; };
}; };
outputs = inputs: outputs = inputs:
inputs.flake-utils.lib.eachDefaultSystem (system: inputs.flake-utils.lib.eachDefaultSystem (system:
let let
pkgsHost = inputs.nixpkgs.legacyPackages.${system}; pkgsHost = inputs.nixpkgs.legacyPackages.${system};
pkgsHostStatic = pkgsHost.pkgsStatic;
# The Rust toolchain to use # The Rust toolchain to use
toolchain = inputs.fenix.packages.${system}.fromToolchainFile { toolchain = inputs.fenix.packages.${system}.fromToolchainFile {
@@ -28,8 +25,7 @@
sha256 = "sha256-+syqAd2kX8KVa8/U2gz3blIQTTsYYt3U63xBWaGOSc8"; sha256 = "sha256-+syqAd2kX8KVa8/U2gz3blIQTTsYYt3U63xBWaGOSc8";
}; };
mkScope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: { scope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
inherit pkgs;
book = self.callPackage ./nix/pkgs/book {}; book = self.callPackage ./nix/pkgs/book {};
complement = self.callPackage ./nix/pkgs/complement {}; complement = self.callPackage ./nix/pkgs/complement {};
craneLib = ((inputs.crane.mkLib pkgs).overrideToolchain toolchain); craneLib = ((inputs.crane.mkLib pkgs).overrideToolchain toolchain);
@@ -43,87 +39,22 @@
(builtins.fromJSON (builtins.readFile ./flake.lock)) (builtins.fromJSON (builtins.readFile ./flake.lock))
.nodes.rocksdb.original.ref; .nodes.rocksdb.original.ref;
}); });
# TODO: remove once https://github.com/NixOS/nixpkgs/pull/314945 is available
liburing = pkgs.liburing.overrideAttrs (old: {
# the configure script doesn't support these, and unconditionally
# builds both static and dynamic libraries.
configureFlags = pkgs.lib.subtractLists
[ "--enable-static" "--disable-shared" ]
old.configureFlags;
postInstall = old.postInstall + ''
# we remove the extra outputs
#
# we need to do this to prevent rocksdb from trying to link the
# static library in a dynamic stdenv
rm $out/lib/liburing*${
if pkgs.stdenv.hostPlatform.isStatic then ".so*" else ".a"
}
'';
});
}); });
scopeHost = mkScope pkgsHost; scopeHost = (scope pkgsHost);
scopeHostStatic = mkScope pkgsHostStatic;
mkDevShell = scope: scope.pkgs.mkShell {
env = scope.main.env // {
# Rust Analyzer needs to be able to find the path to default crate
# sources, and it can read this environment variable to do so. The
# `rust-src` component is required in order for this to work.
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
# Convenient way to access a pinned version of Complement's source
# code.
COMPLEMENT_SRC = inputs.complement.outPath;
# Needed for Complement
CGO_CFLAGS = "-I${scope.pkgs.olm}/include";
CGO_LDFLAGS = "-L${scope.pkgs.olm}/lib";
};
# Development tools
packages = [
# Always use nightly rustfmt because most of its options are unstable
#
# This needs to come before `toolchain` in this list, otherwise
# `$PATH` will have stable rustfmt instead.
inputs.fenix.packages.${system}.latest.rustfmt
toolchain
]
++ (with pkgsHost.pkgs; [
engage
cargo-audit
# Needed for producing Debian packages
cargo-deb
# Needed for Complement
go
# Needed for our script for Complement
jq
# Needed for finding broken markdown links
lychee
# Useful for editing the book locally
mdbook
])
++ scope.main.buildInputs
++ scope.main.propagatedBuildInputs
++ scope.main.nativeBuildInputs;
meta.broken = scope.main.meta.broken;
};
in in
{ {
packages = { packages = {
default = scopeHost.main; default = scopeHost.main;
jemalloc = scopeHost.main.override { features = ["jemalloc"]; };
hmalloc = scopeHost.main.override { features = ["hardened_malloc"]; }; hmalloc = scopeHost.main.override { features = ["hardened_malloc"]; };
oci-image = scopeHost.oci-image; oci-image = scopeHost.oci-image;
oci-image-jemalloc = scopeHost.oci-image.override {
main = scopeHost.main.override {
features = ["jemalloc"];
};
};
oci-image-hmalloc = scopeHost.oci-image.override { oci-image-hmalloc = scopeHost.oci-image.override {
main = scopeHost.main.override { main = scopeHost.main.override {
features = ["hardened_malloc"]; features = ["hardened_malloc"];
@@ -133,7 +64,6 @@
book = scopeHost.book; book = scopeHost.book;
complement = scopeHost.complement; complement = scopeHost.complement;
static-complement = scopeHostStatic.complement;
} }
// //
builtins.listToAttrs builtins.listToAttrs
@@ -149,7 +79,7 @@
config = crossSystem; config = crossSystem;
}; };
}).pkgsStatic; }).pkgsStatic;
scopeCrossStatic = mkScope pkgsCrossStatic; scopeCrossStatic = scope pkgsCrossStatic;
in in
[ [
# An output for a statically-linked binary # An output for a statically-linked binary
@@ -158,6 +88,14 @@
value = scopeCrossStatic.main; value = scopeCrossStatic.main;
} }
# An output for a statically-linked binary with jemalloc
{
name = "${binaryName}-jemalloc";
value = scopeCrossStatic.main.override {
features = ["jemalloc"];
};
}
# An output for a statically-linked binary with hardened_malloc # An output for a statically-linked binary with hardened_malloc
{ {
name = "${binaryName}-hmalloc"; name = "${binaryName}-hmalloc";
@@ -172,6 +110,16 @@
value = scopeCrossStatic.oci-image; value = scopeCrossStatic.oci-image;
} }
# An output for an OCI image based on that binary with jemalloc
{
name = "oci-image-${crossSystem}-jemalloc";
value = scopeCrossStatic.oci-image.override {
main = scopeCrossStatic.main.override {
features = ["jemalloc"];
};
};
}
# An output for an OCI image based on that binary with hardened_malloc # An output for an OCI image based on that binary with hardened_malloc
{ {
name = "oci-image-${crossSystem}-hmalloc"; name = "oci-image-${crossSystem}-hmalloc";
@@ -190,15 +138,54 @@
) )
); );
devShells.default = mkDevShell scopeHostStatic; devShells.default = pkgsHost.mkShell {
devShells.all-features = mkDevShell env = scopeHost.main.env // {
(scopeHostStatic.overrideScope (final: prev: { # Rust Analyzer needs to be able to find the path to default crate
main = prev.main.override { all_features = true; }; # sources, and it can read this environment variable to do so. The
})); # `rust-src` component is required in order for this to work.
devShells.no-features = mkDevShell RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
(scopeHostStatic.overrideScope (final: prev: {
main = prev.main.override { default_features = false; }; # Convenient way to access a pinned version of Complement's source
})); # code.
devShells.dynamic = mkDevShell scopeHost; COMPLEMENT_SRC = inputs.complement.outPath;
};
# Development tools
packages = [
# Always use nightly rustfmt because most of its options are unstable
#
# This needs to come before `toolchain` in this list, otherwise
# `$PATH` will have stable rustfmt instead.
inputs.fenix.packages.${system}.latest.rustfmt
toolchain
]
++ (with pkgsHost; [
engage
cargo-audit
# Needed for producing Debian packages
cargo-deb
# Needed for Complement
go
olm
# Needed for our script for Complement
jq
# Needed for finding broken markdown links
lychee
# Useful for editing the book locally
mdbook
])
++ (if !pkgsHost.stdenv.isDarwin then [
# Needed for building with io_uring
pkgsHost.liburing
] else [])
++
scopeHost.main.nativeBuildInputs;
};
}); });
} }
+2 -5
View File
@@ -14,13 +14,9 @@ stdenv.mkDerivation {
include = [ include = [
"book.toml" "book.toml"
"conduwuit-example.toml" "conduwuit-example.toml"
"CONTRIBUTING.md"
"README.md" "README.md"
"debian/conduwuit.service"
"debian/README.md" "debian/README.md"
"arch/conduwuit.service"
"docs" "docs"
"theme"
]; ];
}; };
@@ -29,6 +25,7 @@ stdenv.mkDerivation {
]; ];
buildPhase = '' buildPhase = ''
mdbook build -d $out mdbook build
mv public $out
''; '';
} }
+5 -3
View File
@@ -44,14 +44,16 @@ let
-sha256 -sha256
${lib.getExe' coreutils "env"} \ ${lib.getExe' coreutils "env"} \
CONDUWUIT_SERVER_NAME="$SERVER_NAME" \ CONDUIT_SERVER_NAME="$SERVER_NAME" \
CONDUIT_WELL_KNOWN_SERVER="$SERVER_NAME:8448" \
CONDUIT_WELL_KNOWN_SERVER="$SERVER_NAME:8008" \
${lib.getExe main'} ${lib.getExe main'}
''; '';
in in
dockerTools.buildImage { dockerTools.buildImage {
name = "complement-${main.pname}"; name = "complement-${main.pname}";
tag = "main"; tag = "dev";
copyToRoot = buildEnv { copyToRoot = buildEnv {
name = "root"; name = "root";
@@ -79,7 +81,7 @@ dockerTools.buildImage {
Env = [ Env = [
"SSL_CERT_FILE=/complement/ca/ca.crt" "SSL_CERT_FILE=/complement/ca/ca.crt"
"CONDUWUIT_CONFIG=${./config.toml}" "CONDUIT_CONFIG=${./config.toml}"
]; ];
ExposedPorts = { ExposedPorts = {
+17 -92
View File
@@ -1,87 +1,27 @@
# Dependencies (keep sorted) # Dependencies (keep sorted)
{ craneLib { craneLib
, inputs , inputs
, jq
, lib , lib
, libiconv , libiconv
, liburing
, pkgsBuildHost , pkgsBuildHost
, rocksdb , rocksdb
, rust , rust
, rust-jemalloc-sys
, stdenv , stdenv
# Options (keep sorted) # Options (keep sorted)
, default_features ? true , default_features ? true
, disable_release_max_log_level ? false
, all_features ? false
, disable_features ? []
, features ? [] , features ? []
, profile ? "release" , profile ? "release"
}: }:
let let
# We perform default-feature unification in nix, because some of the dependencies
# on the nix side depend on feature values.
crateFeatures = path:
let manifest = lib.importTOML "${path}/Cargo.toml"; in
lib.remove "default" (lib.attrNames manifest.features) ++
lib.attrNames
(lib.filterAttrs
(_: dependency: dependency.optional or false)
manifest.dependencies);
crateDefaultFeatures = path:
(lib.importTOML "${path}/Cargo.toml").features.default;
allDefaultFeatures = crateDefaultFeatures "${inputs.self}/src/main";
allFeatures = crateFeatures "${inputs.self}/src/main";
features' = lib.unique
(features ++
lib.optionals default_features allDefaultFeatures ++
lib.optionals all_features allFeatures);
disable_features' = disable_features ++ lib.optionals disable_release_max_log_level ["release_max_log_level"];
features'' = lib.subtractLists disable_features' features';
featureEnabled = feature : builtins.elem feature features'';
enableLiburing = featureEnabled "io_uring" && stdenv.isLinux;
# This derivation will set the JEMALLOC_OVERRIDE variable, causing the
# tikv-jemalloc-sys crate to use the nixpkgs jemalloc instead of building it's
# own. In order for this to work, we need to set flags on the build that match
# whatever flags tikv-jemalloc-sys was going to use. These are dependent on
# which features we enable in tikv-jemalloc-sys.
rust-jemalloc-sys' = (rust-jemalloc-sys.override {
# tikv-jemalloc-sys/unprefixed_malloc_on_supported_platforms feature
unprefixed = true;
}).overrideAttrs (old: {
configureFlags = old.configureFlags ++
# tikv-jemalloc-sys/profiling feature
lib.optional (featureEnabled "jemalloc_prof") "--enable-prof";
});
buildDepsOnlyEnv = buildDepsOnlyEnv =
let let
rocksdb' = (rocksdb.override { rocksdb' = rocksdb.override {
jemalloc = rust-jemalloc-sys'; enableJemalloc = builtins.elem "jemalloc" features;
# rocksdb fails to build with prefixed jemalloc, which is required on };
# darwin due to [1]. In this case, fall back to building rocksdb with
# libc malloc. This should not cause conflicts, because all of the
# jemalloc symbols are prefixed.
#
# [1]: https://github.com/tikv/jemallocator/blob/ab0676d77e81268cd09b059260c75b38dbef2d51/jemalloc-sys/src/env.rs#L17
enableJemalloc = featureEnabled "jemalloc" && !stdenv.isDarwin;
}).overrideAttrs (old: {
# TODO: static rocksdb fails to build on darwin
# build log at <https://girlboss.ceo/~strawberry/pb/JjGH>
meta.broken = stdenv.hostPlatform.isStatic && stdenv.isDarwin;
# TODO: switch to enableUring option once https://github.com/NixOS/nixpkgs/pull/314945 is available
buildInputs = old.buildInputs ++ lib.optional enableLiburing liburing;
});
in in
{ {
# https://crane.dev/faq/rebuilds-bindgen.html
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
CARGO_PROFILE = profile; CARGO_PROFILE = profile;
ROCKSDB_INCLUDE_DIR = "${rocksdb'}/include"; ROCKSDB_INCLUDE_DIR = "${rocksdb'}/include";
ROCKSDB_LIB_DIR = "${rocksdb'}/lib"; ROCKSDB_LIB_DIR = "${rocksdb'}/lib";
@@ -98,14 +38,7 @@ buildDepsOnlyEnv =
buildPackageEnv = { buildPackageEnv = {
CONDUWUIT_VERSION_EXTRA = inputs.self.shortRev or inputs.self.dirtyShortRev or ""; CONDUWUIT_VERSION_EXTRA = inputs.self.shortRev or inputs.self.dirtyShortRev or "";
} // buildDepsOnlyEnv // { } // buildDepsOnlyEnv;
# Only needed in static stdenv because these are transitive dependencies of rocksdb
CARGO_BUILD_RUSTFLAGS = buildDepsOnlyEnv.CARGO_BUILD_RUSTFLAGS
+ lib.optionalString (enableLiburing && stdenv.hostPlatform.isStatic)
" -L${lib.getLib liburing}/lib -luring";
};
commonAttrs = { commonAttrs = {
inherit inherit
@@ -122,33 +55,18 @@ commonAttrs = {
include = [ include = [
"Cargo.lock" "Cargo.lock"
"Cargo.toml" "Cargo.toml"
"deps" "hot_lib"
"src" "src"
]; ];
}; };
buildInputs = lib.optional (featureEnabled "jemalloc") rust-jemalloc-sys';
nativeBuildInputs = [ nativeBuildInputs = [
# bindgen needs the build platform's libclang. Apparently due to "splicing # bindgen needs the build platform's libclang. Apparently due to "splicing
# weirdness", pkgs.rustPlatform.bindgenHook on its own doesn't quite do the # weirdness", pkgs.rustPlatform.bindgenHook on its own doesn't quite do the
# right thing here. # right thing here.
pkgsBuildHost.rustPlatform.bindgenHook pkgsBuildHost.rustPlatform.bindgenHook
# We don't actually depend on `jq`, but crane's `buildPackage` does, but
# its `buildDepsOnly` doesn't. This causes those two derivations to have
# differing values for `NIX_CFLAGS_COMPILE`, which contributes to spurious
# rebuilds of bindgen and its depedents.
jq
] ]
++ lib.optionals stdenv.isDarwin [ ++ lib.optionals stdenv.isDarwin [ libiconv ];
# https://github.com/NixOS/nixpkgs/issues/206242
libiconv
# https://stackoverflow.com/questions/69869574/properly-adding-darwin-apple-sdk-to-a-nix-shell
# https://discourse.nixos.org/t/compile-a-rust-binary-on-macos-dbcrossbar/8612
pkgsBuildHost.darwin.apple_sdk.frameworks.Security
];
}; };
in in
@@ -157,16 +75,23 @@ craneLib.buildPackage ( commonAttrs // {
env = buildDepsOnlyEnv; env = buildDepsOnlyEnv;
}); });
cargoExtraArgs = "--no-default-features " cargoExtraArgs = ""
+ lib.optionalString + lib.optionalString
(features'' != []) (!default_features)
"--features " + (builtins.concatStringsSep "," features''); "--no-default-features "
+ lib.optionalString
(features != [])
"--features " + (builtins.concatStringsSep "," features);
# This is redundant with CI # This is redundant with CI
cargoTestCommand = ""; cargoTestCommand = "";
cargoCheckCommand = "";
# This is redundant with CI
doCheck = false; doCheck = false;
# https://crane.dev/faq/rebuilds-bindgen.html
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
env = buildPackageEnv; env = buildPackageEnv;
passthru = { passthru = {
+1 -2
View File
@@ -11,6 +11,5 @@
}, },
"nix": { "nix": {
"enabled": true "enabled": true
}, }
"labels": ["dependencies", "github_actions"]
} }
-61
View File
@@ -1,61 +0,0 @@
[package]
name = "conduit_admin"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
[lib]
path = "mod.rs"
crate-type = [
"rlib",
# "dylib",
]
[features]
dev_release_log_level = []
release_max_log_level = [
"tracing/max_level_trace",
"tracing/release_max_level_info",
"log/max_level_trace",
"log/release_max_level_info",
]
rocksdb = [
"dep:rust-rocksdb",
]
jemalloc = [
"rust-rocksdb/jemalloc",
]
io_uring = [
"rust-rocksdb/io-uring",
]
zstd_compression = [
"rust-rocksdb/zstd",
]
[dependencies]
clap.workspace = true
conduit-api.workspace = true
conduit-core.workspace = true
conduit-database.workspace = true
conduit-service.workspace = true
futures-util.workspace = true
log.workspace = true
loole.workspace = true
regex.workspace = true
ruma.workspace = true
rust-rocksdb.optional = true
rust-rocksdb.workspace = true
serde_json.workspace = true
serde.workspace = true
serde_yaml.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
[lints]
workspace = true
-61
View File
@@ -1,61 +0,0 @@
use ruma::{api::appservice::Registration, events::room::message::RoomMessageEventContent};
use crate::{services, Result};
pub(super) async fn register(body: Vec<&str>) -> Result<RoomMessageEventContent> {
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
let appservice_config = body[1..body.len().checked_sub(1).unwrap()].join("\n");
let parsed_config = serde_yaml::from_str::<Registration>(&appservice_config);
match parsed_config {
Ok(yaml) => match services().appservice.register_appservice(yaml).await {
Ok(id) => Ok(RoomMessageEventContent::text_plain(format!(
"Appservice registered with ID: {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}"
))),
}
}
pub(super) async fn unregister(_body: Vec<&str>, appservice_identifier: String) -> Result<RoomMessageEventContent> {
match services()
.appservice
.unregister_appservice(&appservice_identifier)
.await
{
Ok(()) => Ok(RoomMessageEventContent::text_plain("Appservice unregistered.")),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Failed to unregister appservice: {e}"
))),
}
}
pub(super) async fn show(_body: Vec<&str>, appservice_identifier: String) -> Result<RoomMessageEventContent> {
match services()
.appservice
.get_registration(&appservice_identifier)
.await
{
Some(config) => {
let config_str = serde_yaml::to_string(&config).expect("config should've been validated on register");
let output = format!("Config for {appservice_identifier}:\n\n```yaml\n{config_str}\n```",);
Ok(RoomMessageEventContent::notice_markdown(output))
},
None => Ok(RoomMessageEventContent::text_plain("Appservice does not exist.")),
}
}
pub(super) async fn list(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
let appservices = services().appservice.iter_ids().await;
let output = format!("Appservices ({}): {}", appservices.len(), appservices.join(", "));
Ok(RoomMessageEventContent::text_plain(output))
}
-644
View File
@@ -1,644 +0,0 @@
use std::{
collections::{BTreeMap, HashMap},
sync::{Arc, Mutex},
time::Instant,
};
use api::client::validate_and_add_event_id;
use conduit::{
debug, info, log,
log::{capture, Capture},
warn, Error, Result,
};
use ruma::{
api::{client::error::ErrorKind, federation::event::get_room_state},
events::room::message::RoomMessageEventContent,
CanonicalJsonObject, EventId, RoomId, RoomVersionId, ServerName,
};
use service::{rooms::event_handler::parse_incoming_pdu, sending::resolve::resolve_actual_dest, services, PduEvent};
use tokio::sync::RwLock;
use tracing_subscriber::EnvFilter;
pub(super) async fn echo(_body: Vec<&str>, message: Vec<String>) -> Result<RoomMessageEventContent> {
let message = message.join(" ");
Ok(RoomMessageEventContent::notice_plain(message))
}
pub(super) async fn get_auth_chain(_body: Vec<&str>, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
let event_id = Arc::<EventId>::from(event_id);
if let Some(event) = 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 room_id = <&RoomId>::try_from(room_id_str)
.map_err(|_| Error::bad_database("Invalid room id field in event in database"))?;
let start = Instant::now();
let count = services()
.rooms
.auth_chain
.event_ids_iter(room_id, vec![event_id])
.await?
.count();
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."))
}
}
pub(super) async fn parse_pdu(body: Vec<&str>) -> Result<RoomMessageEventContent> {
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
Ok(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
Ok(hash) => {
let event_id = EventId::parse(format!("${hash}"));
match serde_json::from_value::<PduEvent>(serde_json::to_value(value).expect("value is json")) {
Ok(pdu) => Ok(RoomMessageEventContent::text_plain(format!("EventId: {event_id:?}\n{pdu:#?}"))),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"EventId: {event_id:?}\nCould not parse event: {e}"
))),
}
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Could not parse PDU JSON: {e:?}"))),
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Invalid json in command body: {e}"
))),
}
}
pub(super) async fn get_pdu(_body: Vec<&str>, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
let mut outlier = false;
let mut pdu_json = services()
.rooms
.timeline
.get_non_outlier_pdu_json(&event_id)?;
if pdu_json.is_none() {
outlier = true;
pdu_json = services().rooms.timeline.get_pdu_json(&event_id)?;
}
match pdu_json {
Some(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"
} else {
"PDU found in our database"
},
json_text
)))
},
None => Ok(RoomMessageEventContent::text_plain("PDU not found locally.")),
}
}
pub(super) async fn get_remote_pdu_list(
body: Vec<&str>, server: Box<ServerName>, force: bool,
) -> Result<RoomMessageEventContent> {
if !services().globals.config.allow_federation {
return Ok(RoomMessageEventContent::text_plain(
"Federation is disabled on this homeserver.",
));
}
if server == services().globals.server_name() {
return Ok(RoomMessageEventContent::text_plain(
"Not allowed to send federation requests to ourselves. Please use `get-pdu` for fetching local PDUs from \
the database.",
));
}
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
let list = body
.clone()
.drain(1..body.len().checked_sub(1).unwrap())
.filter_map(|pdu| EventId::parse(pdu).ok())
.collect::<Vec<_>>();
for pdu in list {
if force {
if let Err(e) = get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Failed to get remote PDU, ignoring error: {e}"
)))
.await;
warn!(%e, "Failed to get remote PDU, ignoring error");
}
} else {
get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await?;
}
}
Ok(RoomMessageEventContent::text_plain("Fetched list of remote PDUs."))
}
pub(super) async fn get_remote_pdu(
_body: Vec<&str>, event_id: Box<EventId>, server: Box<ServerName>,
) -> Result<RoomMessageEventContent> {
if !services().globals.config.allow_federation {
return Ok(RoomMessageEventContent::text_plain(
"Federation is disabled on this homeserver.",
));
}
if server == services().globals.server_name() {
return Ok(RoomMessageEventContent::text_plain(
"Not allowed to send federation requests to ourselves. Please use `get-pdu` for fetching local PDUs.",
));
}
match services()
.sending
.send_federation_request(
&server,
ruma::api::federation::event::get_event::v1::Request {
event_id: event_id.clone().into(),
},
)
.await
{
Ok(response) => {
let json: CanonicalJsonObject = serde_json::from_str(response.pdu.get()).map_err(|e| {
warn!(
"Requested event ID {event_id} from server but failed to convert from RawValue to \
CanonicalJsonObject (malformed event/response?): {e}"
);
Error::BadRequest(ErrorKind::Unknown, "Received response from server but failed to parse PDU")
})?;
debug!("Attempting to parse PDU: {:?}", &response.pdu);
let parsed_pdu = {
let parsed_result = parse_incoming_pdu(&response.pdu);
let (event_id, value, room_id) = match parsed_result {
Ok(t) => t,
Err(e) => {
warn!("Failed to parse PDU: {e}");
info!("Full PDU: {:?}", &response.pdu);
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to parse PDU remote server {server} sent us: {e}"
)));
},
};
vec![(event_id, value, room_id)]
};
let pub_key_map = RwLock::new(BTreeMap::new());
debug!("Attempting to fetch homeserver signing keys for {server}");
services()
.rooms
.event_handler
.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");
services()
.rooms
.timeline
.backfill_pdu(&server, response.pdu, &pub_key_map)
.await?;
let json_text = serde_json::to_string_pretty(&json).expect("canonical json is valid json");
Ok(RoomMessageEventContent::notice_markdown(format!(
"{}\n```json\n{}\n```",
"Got PDU from specified server and handled as backfilled PDU successfully. Event body:", json_text
)))
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Remote server did not have PDU or failed sending request to remote server: {e}"
))),
}
}
pub(super) async fn get_room_state(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
let room_state = services()
.rooms
.state_accessor
.room_state_full(&room_id)
.await?
.values()
.map(|pdu| pdu.to_state_event())
.collect::<Vec<_>>();
if room_state.is_empty() {
return Ok(RoomMessageEventContent::text_plain(
"Unable to find room state in our database (vector is empty)",
));
}
let json_text = serde_json::to_string_pretty(&room_state).map_err(|e| {
warn!("Failed converting room state vector in our database to pretty JSON: {e}");
Error::bad_database(
"Failed to convert room state events to pretty JSON, possible invalid room state events in our database",
)
})?;
Ok(RoomMessageEventContent::notice_markdown(format!(
"{}\n```json\n{}\n```",
"Found full room state", json_text
)))
}
pub(super) async fn ping(_body: Vec<&str>, server: Box<ServerName>) -> Result<RoomMessageEventContent> {
if server == services().globals.server_name() {
return Ok(RoomMessageEventContent::text_plain(
"Not allowed to send federation requests to ourselves.",
));
}
let timer = tokio::time::Instant::now();
match services()
.sending
.send_federation_request(&server, ruma::api::federation::discovery::get_server_version::v1::Request {})
.await
{
Ok(response) => {
let ping_time = timer.elapsed();
let json_text_res = serde_json::to_string_pretty(&response.server);
if let Ok(json) = json_text_res {
return Ok(RoomMessageEventContent::notice_markdown(format!(
"Got response which took {ping_time:?} time:\n```json\n{json}\n```"
)));
}
Ok(RoomMessageEventContent::text_plain(format!(
"Got non-JSON response which took {ping_time:?} time:\n{response:?}"
)))
},
Err(e) => {
warn!("Failed sending federation request to specified server from ping debug command: {e}");
Ok(RoomMessageEventContent::text_plain(format!(
"Failed sending federation request to specified server:\n\n{e}",
)))
},
}
}
pub(super) async fn force_device_list_updates(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
// Force E2EE device list updates for all users
for user_id in services().users.iter().filter_map(Result::ok) {
services().users.mark_device_key_update(&user_id)?;
}
Ok(RoomMessageEventContent::text_plain(
"Marked all devices for all users as having new keys to update",
))
}
pub(super) async fn change_log_level(
_body: Vec<&str>, filter: Option<String>, reset: bool,
) -> Result<RoomMessageEventContent> {
if reset {
let old_filter_layer = match EnvFilter::try_new(&services().globals.config.log) {
Ok(s) => s,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Log level from config appears to be invalid now: {e}"
)));
},
};
match services().server.log.reload.reload(&old_filter_layer) {
Ok(()) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Successfully changed log level back to config value {}",
services().globals.config.log
)));
},
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to modify and reload the global tracing log level: {e}"
)));
},
}
}
if let Some(filter) = filter {
let new_filter_layer = match EnvFilter::try_new(filter) {
Ok(s) => s,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Invalid log level filter specified: {e}"
)));
},
};
match services().server.log.reload.reload(&new_filter_layer) {
Ok(()) => {
return Ok(RoomMessageEventContent::text_plain("Successfully changed log level"));
},
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to modify and reload the global tracing log level: {e}"
)));
},
}
}
Ok(RoomMessageEventContent::text_plain("No log level was specified."))
}
pub(super) async fn sign_json(body: Vec<&str>) -> Result<RoomMessageEventContent> {
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
let string = body[1..body.len().checked_sub(1).unwrap()].join("\n");
match serde_json::from_str(&string) {
Ok(mut value) => {
ruma::signatures::sign_json(
services().globals.server_name().as_str(),
services().globals.keypair(),
&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))
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))),
}
}
pub(super) async fn verify_json(body: Vec<&str>) -> Result<RoomMessageEventContent> {
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
let string = body[1..body.len().checked_sub(1).unwrap()].join("\n");
match serde_json::from_str(&string) {
Ok(value) => {
let pub_key_map = RwLock::new(BTreeMap::new());
services()
.rooms
.event_handler
.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}"
))),
}
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))),
}
}
#[tracing::instrument(skip(_body))]
pub(super) async fn first_pdu_in_room(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
if !services()
.rooms
.state_cache
.server_in_room(&services().globals.config.server_name, &room_id)?
{
return Ok(RoomMessageEventContent::text_plain(
"We are not participating in the room / we don't know about the room ID.",
));
}
let first_pdu = services()
.rooms
.timeline
.first_pdu_in_room(&room_id)?
.ok_or_else(|| Error::bad_database("Failed to find the first PDU in database"))?;
Ok(RoomMessageEventContent::text_plain(format!("{first_pdu:?}")))
}
#[tracing::instrument(skip(_body))]
pub(super) async fn latest_pdu_in_room(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
if !services()
.rooms
.state_cache
.server_in_room(&services().globals.config.server_name, &room_id)?
{
return Ok(RoomMessageEventContent::text_plain(
"We are not participating in the room / we don't know about the room ID.",
));
}
let latest_pdu = services()
.rooms
.timeline
.latest_pdu_in_room(&room_id)?
.ok_or_else(|| Error::bad_database("Failed to find the latest PDU in database"))?;
Ok(RoomMessageEventContent::text_plain(format!("{latest_pdu:?}")))
}
#[tracing::instrument(skip(_body))]
pub(super) async fn force_set_room_state_from_server(
_body: Vec<&str>, server_name: Box<ServerName>, room_id: Box<RoomId>,
) -> Result<RoomMessageEventContent> {
if !services()
.rooms
.state_cache
.server_in_room(&services().globals.config.server_name, &room_id)?
{
return Ok(RoomMessageEventContent::text_plain(
"We are not participating in the room / we don't know about the room ID.",
));
}
let first_pdu = services()
.rooms
.timeline
.latest_pdu_in_room(&room_id)?
.ok_or_else(|| Error::bad_database("Failed to find the latest PDU in database"))?;
let room_version = services().rooms.state.get_room_version(&room_id)?;
let mut state: HashMap<u64, Arc<EventId>> = HashMap::new();
let pub_key_map = RwLock::new(BTreeMap::new());
let remote_state_response = services()
.sending
.send_federation_request(
&server_name,
get_room_state::v1::Request {
room_id: room_id.clone().into(),
event_id: first_pdu.event_id.clone().into(),
},
)
.await?;
let mut events = Vec::with_capacity(remote_state_response.pdus.len());
for pdu in remote_state_response.pdus.clone() {
events.push(match parse_incoming_pdu(&pdu) {
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");
services()
.rooms
.event_handler
.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(pdu, &room_version, &pub_key_map))
{
let Ok((event_id, value)) = result.await else {
continue;
};
let pdu = PduEvent::from_id_val(&event_id, value.clone()).map_err(|e| {
warn!("Invalid PDU in fetching remote room state PDUs response: {} {:?}", e, value);
Error::BadServerResponse("Invalid PDU in send_join response.")
})?;
services()
.rooms
.outlier
.add_pdu_outlier(&event_id, &value)?;
if let Some(state_key) = &pdu.state_key {
let shortstatekey = services()
.rooms
.short
.get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key)?;
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(pdu, &room_version, &pub_key_map))
{
let Ok((event_id, value)) = result.await else {
continue;
};
services()
.rooms
.outlier
.add_pdu_outlier(&event_id, &value)?;
}
let new_room_state = services()
.rooms
.event_handler
.resolve_state(room_id.clone().as_ref(), &room_version, state)
.await?;
info!("Forcing new room state");
let (short_state_hash, new, removed) = services()
.rooms
.state_compressor
.save_state(room_id.clone().as_ref(), new_room_state)?;
let state_lock = services().globals.roomid_mutex_state.lock(&room_id).await;
services()
.rooms
.state
.force_state(room_id.clone().as_ref(), short_state_hash, new, removed, &state_lock)
.await?;
info!(
"Updating joined counts for room just in case (e.g. we may have found a difference in the room's \
m.room.member state"
);
services().rooms.state_cache.update_joined_count(&room_id)?;
drop(state_lock);
Ok(RoomMessageEventContent::text_plain(
"Successfully forced the room state from the requested remote server.",
))
}
pub(super) async fn resolve_true_destination(
_body: Vec<&str>, server_name: Box<ServerName>, no_cache: bool,
) -> Result<RoomMessageEventContent> {
if !services().globals.config.allow_federation {
return Ok(RoomMessageEventContent::text_plain(
"Federation is disabled on this homeserver.",
));
}
if server_name == services().globals.config.server_name {
return Ok(RoomMessageEventContent::text_plain(
"Not allowed to send federation requests to ourselves. Please use `get-pdu` for fetching local PDUs.",
));
}
let filter: &capture::Filter = &|data| {
data.level() <= log::Level::DEBUG
&& data.mod_name().starts_with("conduit")
&& matches!(data.span_name(), "actual" | "well-known" | "srv")
};
let state = &services().server.log.capture;
let logs = Arc::new(Mutex::new(String::new()));
let capture = Capture::new(state, Some(filter), capture::fmt_markdown(logs.clone()));
let (actual_dest, hostname_uri);
{
let _capture_scope = capture.start();
(actual_dest, hostname_uri) = resolve_actual_dest(&server_name, !no_cache).await?;
};
let msg = format!(
"{}\nDestination: {actual_dest}\nHostname URI: {hostname_uri}",
logs.lock().expect("locked")
);
Ok(RoomMessageEventContent::text_markdown(msg))
}
#[must_use]
pub(super) fn memory_stats() -> RoomMessageEventContent {
let html_body = conduit::alloc::memory_stats();
if html_body.is_empty() {
return RoomMessageEventContent::text_plain("malloc stats are not supported on your compiled malloc.");
}
RoomMessageEventContent::text_html(
"This command's output can only be viewed by clients that render HTML.".to_owned(),
html_body,
)
}
-172
View File
@@ -1,172 +0,0 @@
use clap::Parser;
use conduit::trace;
use ruma::events::{
relation::InReplyTo,
room::message::{Relation::Reply, RoomMessageEventContent},
};
extern crate conduit_service as service;
use conduit::Result;
pub(crate) use service::admin::{Command, Service};
use service::admin::{CommandOutput, CommandResult, HandlerResult};
use self::{fsck::FsckCommand, tester::TesterCommands};
use crate::{
appservice, appservice::AppserviceCommand, debug, debug::DebugCommand, federation, federation::FederationCommand,
fsck, media, media::MediaCommand, query, query::QueryCommand, room, room::RoomCommand, server,
server::ServerCommand, services, tester, user, user::UserCommand,
};
pub(crate) const PAGE_SIZE: usize = 100;
#[cfg_attr(test, derive(Debug))]
#[derive(Parser)]
#[command(name = "admin", version = env!("CARGO_PKG_VERSION"))]
pub(crate) enum AdminCommand {
#[command(subcommand)]
/// - Commands for managing appservices
Appservices(AppserviceCommand),
#[command(subcommand)]
/// - Commands for managing local users
Users(UserCommand),
#[command(subcommand)]
/// - Commands for managing rooms
Rooms(RoomCommand),
#[command(subcommand)]
/// - Commands for managing federation
Federation(FederationCommand),
#[command(subcommand)]
/// - Commands for managing the server
Server(ServerCommand),
#[command(subcommand)]
/// - Commands for managing media
Media(MediaCommand),
#[command(subcommand)]
/// - Commands for debugging things
Debug(DebugCommand),
#[command(subcommand)]
/// - Query all the database getters and iterators
Query(QueryCommand),
#[command(subcommand)]
/// - Query all the database getters and iterators
Fsck(FsckCommand),
#[command(subcommand)]
Tester(TesterCommands),
}
#[must_use]
pub fn handle(command: Command) -> HandlerResult { Box::pin(handle_command(command)) }
#[tracing::instrument(skip_all, name = "admin")]
async fn handle_command(command: Command) -> CommandResult {
let Some(mut content) = process_admin_message(command.command).await else {
return Ok(None);
};
content.relates_to = command.reply_id.map(|event_id| Reply {
in_reply_to: InReplyTo {
event_id,
},
});
Ok(Some(content))
}
// Parse and process a message from the admin room
async fn process_admin_message(msg: String) -> CommandOutput {
let mut lines = msg.lines().filter(|l| !l.trim().is_empty());
let command_line = lines.next().expect("each string has at least one line");
let body = lines.collect::<Vec<_>>();
let admin_command = match parse_admin_command(command_line) {
Ok(command) => command,
Err(error) => {
let server_name = services().globals.server_name();
let message = error.replace("server.name", server_name.as_str());
return Some(RoomMessageEventContent::notice_markdown(message));
},
};
match process_admin_command(admin_command, body).await {
Ok(reply_message) => Some(reply_message),
Err(error) => {
let markdown_message = format!("Encountered an error while handling the command:\n```\n{error}\n```",);
Some(RoomMessageEventContent::notice_markdown(markdown_message))
},
}
}
// Parse chat messages from the admin room into an AdminCommand object
fn parse_admin_command(command_line: &str) -> Result<AdminCommand, String> {
let mut argv = command_line.split_whitespace().collect::<Vec<_>>();
// Remove any escapes that came with a server-side escape command
if !argv.is_empty() && argv[0].ends_with("admin") {
argv[0] = argv[0].trim_start_matches('\\');
}
// First indice has to be "admin" but for console convenience we add it here
let server_user = services().globals.server_user.as_str();
if !argv.is_empty() && !argv[0].ends_with("admin") && !argv[0].starts_with(server_user) {
argv.insert(0, "admin");
}
// Replace `help command` with `command --help`
// Clap has a help subcommand, but it omits the long help description.
if argv.len() > 1 && argv[1] == "help" {
argv.remove(1);
argv.push("--help");
}
// Backwards compatibility with `register_appservice`-style commands
let command_with_dashes_argv1;
if argv.len() > 1 && argv[1].contains('_') {
command_with_dashes_argv1 = argv[1].replace('_', "-");
argv[1] = &command_with_dashes_argv1;
}
// Backwards compatibility with `register_appservice`-style commands
let command_with_dashes_argv2;
if argv.len() > 2 && argv[2].contains('_') {
command_with_dashes_argv2 = argv[2].replace('_', "-");
argv[2] = &command_with_dashes_argv2;
}
// if the user is using the `query` command (argv[1]), replace the database
// function/table calls with underscores to match the codebase
let command_with_dashes_argv3;
if argv.len() > 3 && argv[1].eq("query") {
command_with_dashes_argv3 = argv[3].replace('_', "-");
argv[3] = &command_with_dashes_argv3;
}
trace!(?command_line, ?argv, "parse");
AdminCommand::try_parse_from(argv).map_err(|error| error.to_string())
}
#[tracing::instrument(skip_all, name = "command")]
async fn process_admin_command(command: AdminCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
let reply_message_content = match command {
AdminCommand::Appservices(command) => appservice::process(command, body).await?,
AdminCommand::Media(command) => media::process(command, body).await?,
AdminCommand::Users(command) => user::process(command, body).await?,
AdminCommand::Rooms(command) => room::process(command, body).await?,
AdminCommand::Federation(command) => federation::process(command, body).await?,
AdminCommand::Server(command) => server::process(command, body).await?,
AdminCommand::Debug(command) => debug::process(command, body).await?,
AdminCommand::Query(command) => query::process(command, body).await?,
AdminCommand::Fsck(command) => fsck::process(command, body).await?,
AdminCommand::Tester(command) => tester::process(command, body).await?,
};
Ok(reply_message_content)
}
-57
View File
@@ -1,57 +0,0 @@
#![allow(clippy::wildcard_imports)]
pub(crate) mod appservice;
pub(crate) mod debug;
pub(crate) mod federation;
pub(crate) mod fsck;
pub(crate) mod handler;
pub(crate) mod media;
pub(crate) mod query;
pub(crate) mod room;
pub(crate) mod server;
pub(crate) mod tester;
pub(crate) mod user;
pub(crate) mod utils;
extern crate conduit_api as api;
extern crate conduit_core as conduit;
extern crate conduit_service as service;
pub(crate) use conduit::{mod_ctor, mod_dtor, Result};
pub use handler::handle;
pub(crate) use service::{services, user_is_local};
pub(crate) use crate::{
handler::Service,
utils::{escape_html, get_room_info},
};
mod_ctor! {}
mod_dtor! {}
#[cfg(test)]
mod test {
use clap::Parser;
use crate::handler::AdminCommand;
#[test]
fn get_help_short() { get_help_inner("-h"); }
#[test]
fn get_help_long() { get_help_inner("--help"); }
#[test]
fn get_help_subcommand() { get_help_inner("help"); }
fn get_help_inner(input: &str) {
let error = AdminCommand::try_parse_from(["argv[0] doesn't matter", input])
.unwrap_err()
.to_string();
// Search for a handful of keywords that suggest the help printed properly
assert!(error.contains("Usage:"));
assert!(error.contains("Commands:"));
assert!(error.contains("Options:"));
}
}
-57
View File
@@ -1,57 +0,0 @@
use ruma::events::room::message::RoomMessageEventContent;
use super::Globals;
use crate::{services, Result};
/// All the getters and iterators from src/database/key_value/globals.rs
pub(super) async fn globals(subcommand: Globals) -> Result<RoomMessageEventContent> {
match subcommand {
Globals::DatabaseVersion => {
let timer = tokio::time::Instant::now();
let results = services().globals.db.database_version();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
Globals::CurrentCount => {
let timer = tokio::time::Instant::now();
let results = services().globals.db.current_count();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
Globals::LastCheckForUpdatesId => {
let timer = tokio::time::Instant::now();
let results = services().globals.db.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```"
)))
},
Globals::LoadKeypair => {
let timer = tokio::time::Instant::now();
let results = services().globals.db.load_keypair();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
Globals::SigningKeysFor {
origin,
} => {
let timer = tokio::time::Instant::now();
let results = services().globals.db.signing_keys_for(&origin);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
}
}
-233
View File
@@ -1,233 +0,0 @@
use ruma::events::room::message::RoomMessageEventContent;
use super::RoomStateCache;
use crate::{services, Result};
pub(super) async fn room_state_cache(subcommand: RoomStateCache) -> Result<RoomMessageEventContent> {
match subcommand {
RoomStateCache::ServerInRoom {
server,
room_id,
} => {
let timer = tokio::time::Instant::now();
let result = services()
.rooms
.state_cache
.server_in_room(&server, &room_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
},
RoomStateCache::RoomServers {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.room_servers(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::ServerRooms {
server,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services().rooms.state_cache.server_rooms(&server).collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::RoomMembers {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.room_members(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::LocalUsersInRoom {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services()
.rooms
.state_cache
.local_users_in_room(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::ActiveLocalUsersInRoom {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services()
.rooms
.state_cache
.active_local_users_in_room(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::RoomJoinedCount {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results = services().rooms.state_cache.room_joined_count(&room_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::RoomInvitedCount {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results = services().rooms.state_cache.room_invited_count(&room_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::RoomUserOnceJoined {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.room_useroncejoined(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::RoomMembersInvited {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.room_members_invited(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::GetInviteCount {
room_id,
user_id,
} => {
let timer = tokio::time::Instant::now();
let results = services()
.rooms
.state_cache
.get_invite_count(&room_id, &user_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::GetLeftCount {
room_id,
user_id,
} => {
let timer = tokio::time::Instant::now();
let results = services()
.rooms
.state_cache
.get_left_count(&room_id, &user_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::RoomsJoined {
user_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.rooms_joined(&user_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::RoomsInvited {
user_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.rooms_invited(&user_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::RoomsLeft {
user_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services().rooms.state_cache.rooms_left(&user_id).collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
RoomStateCache::InviteState {
user_id,
room_id,
} => {
let timer = tokio::time::Instant::now();
let results = services()
.rooms
.state_cache
.invite_state(&user_id, &room_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
}
}
-69
View File
@@ -1,69 +0,0 @@
use ruma::{events::room::message::RoomMessageEventContent, RoomId};
use service::services;
use super::RoomInfoCommand;
use crate::Result;
pub(super) async fn process(command: RoomInfoCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
match command {
RoomInfoCommand::ListJoinedMembers {
room_id,
} => list_joined_members(body, room_id).await,
RoomInfoCommand::ViewRoomTopic {
room_id,
} => view_room_topic(body, room_id).await,
}
}
async fn list_joined_members(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
let room_name = services()
.rooms
.state_accessor
.get_name(&room_id)
.ok()
.flatten()
.unwrap_or_else(|| room_id.to_string());
let members = services()
.rooms
.state_cache
.room_members(&room_id)
.filter_map(Result::ok);
let member_info = members
.into_iter()
.map(|user_id| {
(
user_id.clone(),
services()
.users
.displayname(&user_id)
.unwrap_or(None)
.unwrap_or_else(|| user_id.to_string()),
)
})
.collect::<Vec<_>>();
let output_plain = format!(
"{} Members in Room \"{}\":\n```\n{}\n```",
member_info.len(),
room_name,
member_info
.iter()
.map(|(mxid, displayname)| format!("{mxid} | {displayname}"))
.collect::<Vec<_>>()
.join("\n")
);
Ok(RoomMessageEventContent::notice_markdown(output_plain))
}
async fn view_room_topic(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
let Some(room_topic) = services().rooms.state_accessor.get_room_topic(&room_id)? else {
return Ok(RoomMessageEventContent::text_plain("Room does not have a room topic set."));
};
Ok(RoomMessageEventContent::notice_markdown(format!(
"Room topic:\n\n```{room_topic}\n```"
)))
}
-500
View File
@@ -1,500 +0,0 @@
use api::client::{get_alias_helper, leave_room};
use ruma::{
events::room::message::RoomMessageEventContent, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId,
};
use tracing::{debug, error, info, warn};
use super::{super::Service, RoomModerationCommand};
use crate::{get_room_info, services, user_is_local, Result};
pub(super) async fn process(command: RoomModerationCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
match command {
RoomModerationCommand::BanRoom {
force,
room,
disable_federation,
} => ban_room(body, force, room, disable_federation).await,
RoomModerationCommand::BanListOfRooms {
force,
disable_federation,
} => ban_list_of_rooms(body, force, disable_federation).await,
RoomModerationCommand::UnbanRoom {
room,
enable_federation,
} => unban_room(body, room, enable_federation).await,
RoomModerationCommand::ListBannedRooms => list_banned_rooms(body).await,
}
}
async fn ban_room(
_body: Vec<&str>, force: bool, room: Box<RoomOrAliasId>, disable_federation: bool,
) -> Result<RoomMessageEventContent> {
debug!("Got room alias or ID: {}", room);
let admin_room_alias = &services().globals.admin_alias;
if let Some(admin_room_id) = Service::get_admin_room()? {
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."));
}
}
let room_id = if room.is_room_id() {
let room_id = match RoomId::parse(&room) {
Ok(room_id) => room_id,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to parse room ID {room}. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
)))
},
};
debug!("Room specified is a room ID, banning room ID");
services().rooms.metadata.ban_room(&room_id, true)?;
room_id
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
Ok(room_alias) => room_alias,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to parse room ID {room}. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
)))
},
};
debug!(
"Room specified is not a room ID, attempting to resolve room alias to a room ID locally, if not using \
get_alias_helper to fetch room ID remotely"
);
let room_id = if let Some(room_id) = 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");
match get_alias_helper(room_alias, None).await {
Ok(response) => {
debug!("Got federation response fetching room ID for room {room}: {:?}", response);
response.room_id
},
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to resolve room alias {room} to a room ID: {e}"
)));
},
}
};
services().rooms.metadata.ban_room(&room_id, true)?;
room_id
} else {
return Ok(RoomMessageEventContent::text_plain(
"Room specified is not a room ID or room alias. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`)",
));
};
debug!("Making all users leave the room {}", &room);
if force {
for local_user in services()
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
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)
&& (user_is_local(local_user)
// since this is a force operation, assume user is an admin
// if somehow this fails
&& services()
.users
.is_admin(local_user)
.unwrap_or(true))
})
})
.collect::<Vec<OwnedUserId>>()
{
debug!(
"Attempting leave for user {} in room {} (forced, ignoring all errors, evicting admins too)",
&local_user, &room_id
);
if let Err(e) = leave_room(&local_user, &room_id, None).await {
warn!(%e, "Failed to leave room");
}
}
} else {
for local_user in services()
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == 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()
== services().globals.server_name()
&& !services()
.users
.is_admin(local_user)
.unwrap_or(false))
})
})
.collect::<Vec<OwnedUserId>>()
{
debug!("Attempting leave for user {} in room {}", &local_user, &room_id);
if let Err(e) = leave_room(&local_user, &room_id, None).await {
error!(
"Error attempting to make local user {} leave room {} during room banning: {}",
&local_user, &room_id, 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): {}\nIf you would like to ignore errors, use --force",
&local_user, &room_id, e
)));
}
}
}
if disable_federation {
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.",
));
}
Ok(RoomMessageEventContent::text_plain(
"Room banned and removed all our local users, use `!admin federation disable-room` to stop receiving new \
inbound federation events as well if needed.",
))
}
async fn ban_list_of_rooms(body: Vec<&str>, force: bool, disable_federation: bool) -> Result<RoomMessageEventContent> {
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
let rooms_s = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
let admin_room_alias = &services().globals.admin_alias;
let mut room_ban_count: usize = 0;
let mut room_ids: Vec<OwnedRoomId> = Vec::new();
for &room in &rooms_s {
match <&RoomOrAliasId>::try_from(room) {
Ok(room_alias_or_id) => {
if let Some(admin_room_id) = Service::get_admin_room()? {
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;
}
}
if room_alias_or_id.is_room_id() {
let room_id = match RoomId::parse(room_alias_or_id) {
Ok(room_id) => room_id,
Err(e) => {
if force {
// ignore rooms we failed to parse if we're force banning
warn!(
"Error parsing room \"{room}\" during bulk room banning, ignoring error and \
logging here: {e}"
);
continue;
}
return Ok(RoomMessageEventContent::text_plain(format!(
"{room} is not a valid room ID or room alias, please fix the list and try again: {e}"
)));
},
};
room_ids.push(room_id);
}
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) = 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"
);
match get_alias_helper(room_alias, None).await {
Ok(response) => {
debug!(
"Got federation response fetching room ID for room {room}: {:?}",
response
);
response.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}"
)));
},
}
};
room_ids.push(room_id);
},
Err(e) => {
if force {
// ignore rooms we failed to parse if we're force deleting
error!(
"Error parsing room \"{room}\" during bulk room banning, ignoring error and \
logging here: {e}"
);
continue;
}
return Ok(RoomMessageEventContent::text_plain(format!(
"{room} is not a valid room ID or room alias, please fix the list and try again: {e}"
)));
},
}
}
},
Err(e) => {
if force {
// ignore rooms we failed to parse if we're force deleting
error!(
"Error parsing room \"{room}\" during bulk room banning, ignoring error and logging here: {e}"
);
continue;
}
return Ok(RoomMessageEventContent::text_plain(format!(
"{room} is not a valid room ID or room alias, please fix the list and try again: {e}"
)));
},
}
}
for room_id in room_ids {
if services().rooms.metadata.ban_room(&room_id, true).is_ok() {
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 services()
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == 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()
== services().globals.server_name()
// since this is a force operation, assume user is an
// admin if somehow this fails
&& services()
.users
.is_admin(local_user)
.unwrap_or(true))
})
})
.collect::<Vec<OwnedUserId>>()
{
debug!(
"Attempting leave for user {} in room {} (forced, ignoring all errors, evicting admins too)",
&local_user, room_id
);
if let Err(e) = leave_room(&local_user, &room_id, None).await {
warn!(%e, "Failed to leave room");
}
}
} else {
for local_user in services()
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == 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()
== services().globals.server_name()
&& !services()
.users
.is_admin(local_user)
.unwrap_or(false))
})
})
.collect::<Vec<OwnedUserId>>()
{
debug!("Attempting leave for user {} in room {}", &local_user, &room_id);
if let Err(e) = leave_room(&local_user, &room_id, None).await {
error!(
"Error attempting to make local user {} leave room {} during bulk room banning: {}",
&local_user, &room_id, 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 \
like to ignore errors, use --force",
&local_user, &room_id, e
)));
}
}
}
if disable_federation {
services().rooms.metadata.disable_room(&room_id, true)?;
}
}
if disable_federation {
Ok(RoomMessageEventContent::text_plain(format!(
"Finished bulk room ban, banned {room_ban_count} total rooms, evicted all users, and disabled incoming \
federation with the room."
)))
} else {
Ok(RoomMessageEventContent::text_plain(format!(
"Finished bulk room ban, banned {room_ban_count} total rooms and evicted all users."
)))
}
}
async fn unban_room(
_body: Vec<&str>, room: Box<RoomOrAliasId>, enable_federation: bool,
) -> Result<RoomMessageEventContent> {
let room_id = if room.is_room_id() {
let room_id = match RoomId::parse(&room) {
Ok(room_id) => room_id,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to parse room ID {room}. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
)))
},
};
debug!("Room specified is a room ID, unbanning room ID");
services().rooms.metadata.ban_room(&room_id, false)?;
room_id
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
Ok(room_alias) => room_alias,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to parse room ID {room}. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
)))
},
};
debug!(
"Room specified is not a room ID, attempting to resolve room alias to a room ID locally, if not using \
get_alias_helper to fetch room ID remotely"
);
let room_id = if let Some(room_id) = 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");
match get_alias_helper(room_alias, None).await {
Ok(response) => {
debug!("Got federation response fetching room ID for room {room}: {:?}", response);
response.room_id
},
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to resolve room alias {room} to a room ID: {e}"
)));
},
}
};
services().rooms.metadata.ban_room(&room_id, false)?;
room_id
} else {
return Ok(RoomMessageEventContent::text_plain(
"Room specified is not a room ID or room alias. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`)",
));
};
if enable_federation {
services().rooms.metadata.disable_room(&room_id, false)?;
return Ok(RoomMessageEventContent::text_plain("Room unbanned."));
}
Ok(RoomMessageEventContent::text_plain(
"Room unbanned, you may need to re-enable federation with the room using enable-room if this is a remote room \
to make it fully functional.",
))
}
async fn list_banned_rooms(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
let rooms = services()
.rooms
.metadata
.list_banned_rooms()
.collect::<Result<Vec<_>, _>>();
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(&room_id))
.collect::<Vec<_>>();
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let output_plain = format!(
"Rooms Banned ({}):\n```\n{}```",
rooms.len(),
rooms
.iter()
.map(|(id, members, name)| 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}")))
},
}
}
-423
View File
@@ -1,423 +0,0 @@
use std::{collections::BTreeMap, fmt::Write as _};
use api::client::{join_room_by_id_helper, leave_all_rooms, update_avatar_url, update_displayname};
use conduit::{utils, Result};
use ruma::{
events::{
room::message::RoomMessageEventContent,
tag::{TagEvent, TagEventContent, TagInfo},
RoomAccountDataEventType,
},
OwnedRoomId, OwnedUserId, RoomId,
};
use tracing::{error, info, warn};
use crate::{
escape_html, get_room_info, services,
utils::{parse_active_local_user_id, parse_local_user_id},
};
const AUTO_GEN_PASSWORD_LENGTH: usize = 25;
pub(super) async fn list(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
match 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");
plain_msg += "\n```";
Ok(RoomMessageEventContent::notice_markdown(plain_msg))
},
Err(e) => Ok(RoomMessageEventContent::text_plain(e.to_string())),
}
}
pub(super) async fn create(
_body: Vec<&str>, username: String, password: Option<String>,
) -> Result<RoomMessageEventContent> {
// Validate user id
let user_id = parse_local_user_id(&username)?;
if services().users.exists(&user_id)? {
return Ok(RoomMessageEventContent::text_plain(format!("Userid {user_id} already exists")));
}
let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
// Create user
services().users.create(&user_id, Some(password.as_str()))?;
// Default to pretty displayname
let mut displayname = user_id.localpart().to_owned();
// If `new_user_displayname_suffix` is set, registration will push whatever
// content is set to the user's display name with a space before it
if !services()
.globals
.config
.new_user_displayname_suffix
.is_empty()
{
write!(displayname, " {}", services().globals.config.new_user_displayname_suffix)
.expect("should be able to write to string buffer");
}
services()
.users
.set_displayname(&user_id, Some(displayname))
.await?;
// Initial account data
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"),
)?;
if !services().globals.config.auto_join_rooms.is_empty() {
for room in &services().globals.config.auto_join_rooms {
if !services()
.rooms
.state_cache
.server_in_room(services().globals.server_name(), room)?
{
warn!("Skipping room {room} to automatically join as we have never joined before.");
continue;
}
if let Some(room_id_server_name) = room.server_name() {
match join_room_by_id_helper(
Some(&user_id),
room,
Some("Automatically joining this room upon registration".to_owned()),
&[room_id_server_name.to_owned(), services().globals.server_name().to_owned()],
None,
)
.await
{
Ok(_response) => {
info!("Automatically joined room {room} for user {user_id}");
},
Err(e) => {
// don't return this error so we don't fail registrations
error!("Failed to automatically join room {room} for user {user_id}: {e}");
},
};
}
}
}
// we dont add a device since we're not the user, just the creator
// Inhibit login does not work for guests
Ok(RoomMessageEventContent::text_plain(format!(
"Created user with user_id: {user_id} and password: `{password}`"
)))
}
pub(super) async fn deactivate(
_body: Vec<&str>, no_leave_rooms: bool, user_id: String,
) -> Result<RoomMessageEventContent> {
// Validate user id
let user_id = parse_local_user_id(&user_id)?;
// don't deactivate the server service account
if user_id == services().globals.server_user {
return Ok(RoomMessageEventContent::text_plain(
"Not allowed to deactivate the server service account.",
));
}
services().users.deactivate_account(&user_id)?;
if !no_leave_rooms {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Making {user_id} leave all rooms after deactivation..."
)))
.await;
let all_joined_rooms: Vec<OwnedRoomId> = services()
.rooms
.state_cache
.rooms_joined(&user_id)
.filter_map(Result::ok)
.collect();
update_displayname(user_id.clone(), None, all_joined_rooms.clone()).await?;
update_avatar_url(user_id.clone(), None, None, all_joined_rooms).await?;
leave_all_rooms(&user_id).await;
}
Ok(RoomMessageEventContent::text_plain(format!(
"User {user_id} has been deactivated"
)))
}
pub(super) async fn reset_password(_body: Vec<&str>, username: String) -> Result<RoomMessageEventContent> {
let user_id = parse_local_user_id(&username)?;
if user_id == services().globals.server_user {
return Ok(RoomMessageEventContent::text_plain(
"Not allowed to set the password for the server account. Please use the emergency password config option.",
));
}
let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH);
match services()
.users
.set_password(&user_id, Some(new_password.as_str()))
{
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!(
"Successfully reset the password for user {user_id}: `{new_password}`"
))),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Couldn't reset the password for user {user_id}: {e}"
))),
}
}
pub(super) async fn deactivate_all(
body: Vec<&str>, no_leave_rooms: bool, force: bool,
) -> Result<RoomMessageEventContent> {
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
let usernames = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
let mut user_ids: Vec<OwnedUserId> = Vec::with_capacity(usernames.len());
let mut admins = Vec::new();
for username in usernames {
match parse_active_local_user_id(username) {
Ok(user_id) => {
if services().users.is_admin(&user_id)? && !force {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"{username} is an admin and --force is not set, skipping over"
)))
.await;
admins.push(username);
continue;
}
// don't deactivate the server service account
if user_id == services().globals.server_user {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"{username} is the server service account, skipping over"
)))
.await;
continue;
}
user_ids.push(user_id);
},
Err(e) => {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"{username} is not a valid username, skipping over: {e}"
)))
.await;
continue;
},
}
}
let mut deactivation_count: usize = 0;
for user_id in user_ids {
match services().users.deactivate_account(&user_id) {
Ok(()) => {
deactivation_count = deactivation_count.saturating_add(1);
if !no_leave_rooms {
info!("Forcing user {user_id} to leave all rooms apart of deactivate-all");
let all_joined_rooms: Vec<OwnedRoomId> = services()
.rooms
.state_cache
.rooms_joined(&user_id)
.filter_map(Result::ok)
.collect();
update_displayname(user_id.clone(), None, all_joined_rooms.clone()).await?;
update_avatar_url(user_id.clone(), None, None, all_joined_rooms).await?;
leave_all_rooms(&user_id).await;
}
},
Err(e) => {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!("Failed deactivating user: {e}")))
.await;
},
}
}
if admins.is_empty() {
Ok(RoomMessageEventContent::text_plain(format!(
"Deactivated {deactivation_count} accounts."
)))
} else {
Ok(RoomMessageEventContent::text_plain(format!(
"Deactivated {deactivation_count} accounts.\nSkipped admin accounts: {}. Use --force to deactivate admin \
accounts",
admins.join(", ")
)))
}
}
pub(super) async fn list_joined_rooms(_body: Vec<&str>, user_id: String) -> Result<RoomMessageEventContent> {
// Validate user id
let user_id = parse_local_user_id(&user_id)?;
let mut rooms: Vec<(OwnedRoomId, u64, String)> = services()
.rooms
.state_cache
.rooms_joined(&user_id)
.filter_map(Result::ok)
.map(|room_id| get_room_info(&room_id))
.collect();
if rooms.is_empty() {
return Ok(RoomMessageEventContent::text_plain("User is not in any rooms."));
}
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let output_plain = format!(
"Rooms {user_id} Joined ({}):\n{}",
rooms.len(),
rooms
.iter()
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
.collect::<Vec<_>>()
.join("\n")
);
let output_html = format!(
"<table><caption>Rooms {user_id} Joined \
({})</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>",
escape_html(id.as_ref()),
members,
escape_html(name)
)
.unwrap();
output
})
);
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
}
pub(super) async fn put_room_tag(
_body: Vec<&str>, user_id: String, room_id: Box<RoomId>, tag: String,
) -> Result<RoomMessageEventContent> {
let user_id = parse_active_local_user_id(&user_id)?;
let event = services()
.account_data
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
let mut tags_event = event.map_or_else(
|| 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());
services().account_data.update(
Some(&room_id),
&user_id,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)?;
Ok(RoomMessageEventContent::text_plain(format!(
"Successfully updated room account data for {user_id} and room {room_id} with tag {tag}"
)))
}
pub(super) async fn delete_room_tag(
_body: Vec<&str>, user_id: String, room_id: Box<RoomId>, tag: String,
) -> Result<RoomMessageEventContent> {
let user_id = parse_active_local_user_id(&user_id)?;
let event = services()
.account_data
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
let mut tags_event = event.map_or_else(
|| 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());
services().account_data.update(
Some(&room_id),
&user_id,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)?;
Ok(RoomMessageEventContent::text_plain(format!(
"Successfully updated room account data for {user_id} and room {room_id}, deleting room tag {tag}"
)))
}
pub(super) async fn get_room_tags(
_body: Vec<&str>, user_id: String, room_id: Box<RoomId>,
) -> Result<RoomMessageEventContent> {
let user_id = parse_active_local_user_id(&user_id)?;
let event = services()
.account_data
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
let tags_event = event.map_or_else(
|| 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```",
tags_event.content.tags
)))
}
-131
View File
@@ -1,131 +0,0 @@
mod commands;
use clap::Subcommand;
use conduit::Result;
use ruma::{events::room::message::RoomMessageEventContent, RoomId};
use self::commands::*;
#[cfg_attr(test, derive(Debug))]
#[derive(Subcommand)]
pub(super) enum UserCommand {
/// - Create a new user
Create {
/// Username of the new user
username: String,
/// Password of the new user, if unspecified one is generated
password: Option<String>,
},
/// - Reset user password
ResetPassword {
/// Username of the user for whom the password should be reset
username: String,
},
/// - Deactivate a user
///
/// User will be removed from all rooms by default.
/// Use --no-leave-rooms to not leave all rooms by default.
Deactivate {
#[arg(short, long)]
no_leave_rooms: bool,
user_id: String,
},
/// - Deactivate a list of users
///
/// Recommended to use in conjunction with list-local-users.
///
/// Users will be removed from joined rooms by default.
///
/// Can be overridden with --no-leave-rooms.
///
/// Removing a mass amount of users from a room may cause a significant
/// amount of leave events. The time to leave rooms may depend significantly
/// on joined rooms and servers.
///
/// This command needs a newline separated list of users provided in a
/// Markdown code block below the command.
DeactivateAll {
#[arg(short, long)]
/// Remove users from their joined rooms
no_leave_rooms: bool,
#[arg(short, long)]
/// Also deactivate admin accounts and will assume leave all rooms too
force: bool,
},
/// - List local users in the database
List,
/// - Lists all the rooms (local and remote) that the specified user is
/// joined in
ListJoinedRooms {
user_id: String,
},
/// - Puts a room tag for the specified user and room ID.
///
/// This is primarily useful if you'd like to set your admin room
/// to the special "System Alerts" section in Element as a way to
/// permanently see your admin room without it being buried away in your
/// favourites or rooms. To do this, you would pass your user, your admin
/// room's internal ID, and the tag name `m.server_notice`.
PutRoomTag {
user_id: String,
room_id: Box<RoomId>,
tag: String,
},
/// - Deletes the room tag for the specified user and room ID
DeleteRoomTag {
user_id: String,
room_id: Box<RoomId>,
tag: String,
},
/// - Gets all the room tags for the specified user and room ID
GetRoomTags {
user_id: String,
room_id: Box<RoomId>,
},
}
pub(super) async fn process(command: UserCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
Ok(match command {
UserCommand::List => list(body).await?,
UserCommand::Create {
username,
password,
} => create(body, username, password).await?,
UserCommand::Deactivate {
no_leave_rooms,
user_id,
} => deactivate(body, no_leave_rooms, user_id).await?,
UserCommand::ResetPassword {
username,
} => reset_password(body, username).await?,
UserCommand::DeactivateAll {
no_leave_rooms,
force,
} => deactivate_all(body, no_leave_rooms, force).await?,
UserCommand::ListJoinedRooms {
user_id,
} => list_joined_rooms(body, user_id).await?,
UserCommand::PutRoomTag {
user_id,
room_id,
tag,
} => put_room_tag(body, user_id, room_id, tag).await?,
UserCommand::DeleteRoomTag {
user_id,
room_id,
tag,
} => delete_room_tag(body, user_id, room_id, tag).await?,
UserCommand::GetRoomTags {
user_id,
room_id,
} => get_room_tags(body, user_id, room_id).await?,
})
}
-63
View File
@@ -1,63 +0,0 @@
use conduit_core::Error;
use ruma::{OwnedRoomId, OwnedUserId, RoomId, UserId};
use service::user_is_local;
use crate::{services, Result};
pub(crate) fn escape_html(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}
pub(crate) fn get_room_info(id: &RoomId) -> (OwnedRoomId, u64, String) {
(
id.into(),
services()
.rooms
.state_cache
.room_joined_count(id)
.ok()
.flatten()
.unwrap_or(0),
services()
.rooms
.state_accessor
.get_name(id)
.ok()
.flatten()
.unwrap_or_else(|| id.to_string()),
)
}
/// Parses user ID
pub(crate) fn parse_user_id(user_id: &str) -> Result<OwnedUserId> {
UserId::parse_with_server_name(user_id.to_lowercase(), services().globals.server_name())
.map_err(|e| Error::Err(format!("The supplied username is not a valid username: {e}")))
}
/// Parses user ID as our local user
pub(crate) fn parse_local_user_id(user_id: &str) -> Result<OwnedUserId> {
let user_id = parse_user_id(user_id)?;
if !user_is_local(&user_id) {
return Err(Error::Err(String::from("User does not belong to our server.")));
}
Ok(user_id)
}
/// Parses user ID that is an active (not guest or deactivated) local user
pub(crate) fn parse_active_local_user_id(user_id: &str) -> Result<OwnedUserId> {
let user_id = parse_local_user_id(user_id)?;
if !services().users.exists(&user_id)? {
return Err(Error::Err(String::from("User does not exist on this server.")));
}
if services().users.is_deactivated(&user_id)? {
return Err(Error::Err(String::from("User is deactivated.")));
}
Ok(user_id)
}
+7
View File
@@ -0,0 +1,7 @@
//! Default allocator with no special features
/// Always returns the empty string
pub(crate) fn memory_stats() -> String { Default::default() }
/// Always returns the empty string
pub(crate) fn memory_usage() -> String { Default::default() }
+8
View File
@@ -0,0 +1,8 @@
#[global_allocator]
static HMALLOC: hardened_malloc_rs::HardenedMalloc = hardened_malloc_rs::HardenedMalloc;
pub(crate) fn memory_usage() -> String {
String::default() //TODO: get usage
}
pub(crate) fn memory_stats() -> String { "Extended statistics are not available from hardened_malloc.".to_owned() }
+3 -8
View File
@@ -7,8 +7,7 @@ use tikv_jemallocator as jemalloc;
#[global_allocator] #[global_allocator]
static JEMALLOC: jemalloc::Jemalloc = jemalloc::Jemalloc; static JEMALLOC: jemalloc::Jemalloc = jemalloc::Jemalloc;
#[must_use] pub(crate) fn memory_usage() -> String {
pub fn memory_usage() -> String {
use mallctl::stats; use mallctl::stats;
let allocated = stats::allocated::read().unwrap_or_default() as f64 / 1024.0 / 1024.0; let allocated = stats::allocated::read().unwrap_or_default() as f64 / 1024.0 / 1024.0;
let active = stats::active::read().unwrap_or_default() as f64 / 1024.0 / 1024.0; let active = stats::active::read().unwrap_or_default() as f64 / 1024.0 / 1024.0;
@@ -22,18 +21,14 @@ pub fn memory_usage() -> String {
) )
} }
#[must_use] pub(crate) fn memory_stats() -> String {
pub fn memory_stats() -> String {
const MAX_LENGTH: usize = 65536 - 4096; const MAX_LENGTH: usize = 65536 - 4096;
let opts_s = "d"; let opts_s = "d";
let mut str = String::new(); let mut str = String::new();
let opaque = std::ptr::from_mut(&mut str).cast::<c_void>(); let opaque = std::ptr::from_mut(&mut str).cast::<c_void>();
let opts_p: *const c_char = std::ffi::CString::new(opts_s) let opts_p: *const c_char = std::ffi::CString::new(opts_s).expect("cstring").into_raw() as *const c_char;
.expect("cstring")
.into_raw()
.cast_const();
// SAFETY: calls malloc_stats_print() with our string instance which must remain // SAFETY: calls malloc_stats_print() with our string instance which must remain
// in this frame. https://docs.rs/tikv-jemalloc-sys/latest/tikv_jemalloc_sys/fn.malloc_stats_print.html // in this frame. https://docs.rs/tikv-jemalloc-sys/latest/tikv_jemalloc_sys/fn.malloc_stats_print.html
+8 -10
View File
@@ -2,26 +2,24 @@
// jemalloc // jemalloc
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc", not(feature = "hardened_malloc")))] #[cfg(all(not(target_env = "msvc"), feature = "jemalloc", not(feature = "hardened_malloc")))]
pub mod je; mod je;
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc", not(feature = "hardened_malloc")))] #[cfg(all(not(target_env = "msvc"), feature = "jemalloc", not(feature = "hardened_malloc")))]
pub use je::{memory_stats, memory_usage}; pub(crate) use je::{memory_stats, memory_usage};
// hardened_malloc // hardened_malloc
//#[cfg(all(not(target_env = "msvc"), feature = "hardened_malloc", target_os = #[cfg(all(not(target_env = "msvc"), feature = "hardened_malloc", target_os = "linux", not(feature = "jemalloc")))]
//#[cfg(all(not(target_env "linux", not(feature = "jemalloc")))] mod hardened;
//pub mod hardened; #[cfg(all(not(target_env = "msvc"), feature = "hardened_malloc", target_os = "linux", not(feature = "jemalloc")))]
//#[cfg(all(not(target_env = "msvc"), feature = "hardened_malloc", target_os = pub(crate) use hardened::{memory_stats, memory_usage};
//#[cfg(all(not(target_env "linux", not(feature = "jemalloc")))]
//pub use hardened::{memory_stats, memory_usage};
// default, enabled when none or multiple of the above are enabled // default, enabled when none or multiple of the above are enabled
#[cfg(any( #[cfg(any(
not(any(feature = "jemalloc", feature = "hardened_malloc")), not(any(feature = "jemalloc", feature = "hardened_malloc")),
all(feature = "jemalloc", feature = "hardened_malloc"), all(feature = "jemalloc", feature = "hardened_malloc"),
))] ))]
pub mod default; mod default;
#[cfg(any( #[cfg(any(
not(any(feature = "jemalloc", feature = "hardened_malloc")), not(any(feature = "jemalloc", feature = "hardened_malloc")),
all(feature = "jemalloc", feature = "hardened_malloc"), all(feature = "jemalloc", feature = "hardened_malloc"),
))] ))]
pub use default::{memory_stats, memory_usage}; pub(crate) use default::{memory_stats, memory_usage};
-65
View File
@@ -1,65 +0,0 @@
[package]
name = "conduit_api"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
[lib]
path = "mod.rs"
crate-type = [
"rlib",
# "dylib",
]
[features]
element_hacks = []
dev_release_log_level = []
release_max_log_level = [
"tracing/max_level_trace",
"tracing/release_max_level_info",
"log/max_level_trace",
"log/release_max_level_info",
]
gzip_compression = [
"reqwest/gzip",
]
brotli_compression = [
"reqwest/brotli",
]
[dependencies]
axum-client-ip.workspace = true
axum-extra.workspace = true
axum.workspace = true
base64.workspace = true
bytes.workspace = true
conduit-core.workspace = true
conduit-database.workspace = true
conduit-service.workspace = true
futures-util.workspace = true
hmac.workspace = true
http.workspace = true
hyper.workspace = true
image.workspace = true
ipaddress.workspace = true
jsonwebtoken.workspace = true
log.workspace = true
rand.workspace = true
reqwest.workspace = true
ruma.workspace = true
serde_html_form.workspace = true
serde_json.workspace = true
serde.workspace = true
sha-1.workspace = true
thiserror.workspace = true
tokio.workspace = true
tracing.workspace = true
webpage.workspace = true
[lints]
workspace = true
-83
View File
@@ -1,83 +0,0 @@
pub(super) mod account;
pub(super) mod alias;
pub(super) mod backup;
pub(super) mod capabilities;
pub(super) mod config;
pub(super) mod context;
pub(super) mod device;
pub(super) mod directory;
pub(super) mod filter;
pub(super) mod keys;
pub(super) mod media;
pub(super) mod membership;
pub(super) mod message;
pub(super) mod presence;
pub(super) mod profile;
pub(super) mod push;
pub(super) mod read_marker;
pub(super) mod redact;
pub(super) mod relations;
pub(super) mod report;
pub(super) mod room;
pub(super) mod search;
pub(super) mod session;
pub(super) mod space;
pub(super) mod state;
pub(super) mod sync;
pub(super) mod tag;
pub(super) mod thirdparty;
pub(super) mod threads;
pub(super) mod to_device;
pub(super) mod typing;
pub(super) mod unstable;
pub(super) mod unversioned;
pub(super) mod user_directory;
pub(super) mod voip;
pub(super) use account::*;
pub use alias::get_alias_helper;
pub(super) use alias::*;
pub(super) use backup::*;
pub(super) use capabilities::*;
pub(super) use config::*;
pub(super) use context::*;
pub(super) use device::*;
pub(super) use directory::*;
pub(super) use filter::*;
pub(super) use keys::*;
pub(super) use media::*;
pub(super) use membership::*;
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room, validate_and_add_event_id};
pub(super) use message::*;
pub(super) use presence::*;
pub(super) use profile::*;
pub use profile::{update_all_rooms, update_avatar_url, update_displayname};
pub(super) use push::*;
pub(super) use read_marker::*;
pub(super) use redact::*;
pub(super) use relations::*;
pub(super) use report::*;
pub(super) use room::*;
pub(super) use search::*;
pub(super) use session::*;
pub(super) use space::*;
pub(super) use state::*;
pub(super) use sync::*;
pub(super) use tag::*;
pub(super) use thirdparty::*;
pub(super) use threads::*;
pub(super) use to_device::*;
pub(super) use typing::*;
pub(super) use unstable::*;
pub(super) use unversioned::*;
pub(super) use user_directory::*;
pub(super) use voip::*;
/// generated device ID length
const DEVICE_ID_LENGTH: usize = 10;
/// generated user access token length
const TOKEN_LENGTH: usize = 32;
/// generated user session ID length
const SESSION_ID_LENGTH: usize = service::uiaa::SESSION_ID_LENGTH;
@@ -1,12 +1,10 @@
use std::fmt::Write; use std::fmt::Write as _;
use axum_client_ip::InsecureClientIp;
use conduit::debug_info;
use register::RegistrationKind; use register::RegistrationKind;
use ruma::{ use ruma::{
api::client::{ api::client::{
account::{ account::{
change_password, check_registration_token_validity, deactivate, get_3pids, get_username_availability, change_password, deactivate, get_3pids, get_username_availability,
register::{self, LoginType}, register::{self, LoginType},
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, whoami, request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, whoami,
ThirdPartyIdRemovalStatus, ThirdPartyIdRemovalStatus,
@@ -15,15 +13,15 @@ use ruma::{
uiaa::{AuthFlow, AuthType, UiaaInfo}, uiaa::{AuthFlow, AuthType, UiaaInfo},
}, },
events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType}, events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType},
push, OwnedRoomId, UserId, push, UserId,
}; };
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use super::{join_room_by_id_helper, DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH}; use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
use crate::{ use crate::{
service::user_is_local, api::client_server::{self, join_room_by_id_helper},
services, service, services,
utils::{self}, utils::{self, user_id::user_is_local},
Error, Result, Ruma, Error, Result, Ruma,
}; };
@@ -40,9 +38,8 @@ const RANDOM_USER_ID_LENGTH: usize = 10;
/// ///
/// Note: This will not reserve the username, so the username might become /// Note: This will not reserve the username, so the username might become
/// invalid when trying to register /// invalid when trying to register
#[tracing::instrument(skip_all, fields(%client), name = "register_available")]
pub(crate) async fn get_register_available_route( pub(crate) async fn get_register_available_route(
InsecureClientIp(client): InsecureClientIp, body: Ruma<get_username_availability::v3::Request>, body: Ruma<get_username_availability::v3::Request>,
) -> Result<get_username_availability::v3::Response> { ) -> Result<get_username_availability::v3::Response> {
// Validate user id // 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.to_lowercase(), services().globals.server_name())
@@ -89,10 +86,7 @@ pub(crate) async fn get_register_available_route(
/// - If `inhibit_login` is false: Creates a device and returns device id and /// - If `inhibit_login` is false: Creates a device and returns device id and
/// access_token /// access_token
#[allow(clippy::doc_markdown)] #[allow(clippy::doc_markdown)]
#[tracing::instrument(skip_all, fields(%client), name = "register")] pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<register::v3::Response> {
pub(crate) async fn register_route(
InsecureClientIp(client): InsecureClientIp, body: Ruma<register::v3::Request>,
) -> Result<register::v3::Response> {
if !services().globals.allow_registration() && body.appservice_info.is_none() { if !services().globals.allow_registration() && body.appservice_info.is_none() {
info!( info!(
"Registration disabled and request not from known appservice, rejecting registration attempt for username \ "Registration disabled and request not from known appservice, rejecting registration attempt for username \
@@ -109,8 +103,8 @@ pub(crate) async fn register_route(
|| (services().globals.allow_registration() && services().globals.config.registration_token.is_some())) || (services().globals.allow_registration() && services().globals.config.registration_token.is_some()))
{ {
info!( info!(
"Guest registration disabled / registration enabled with token configured, rejecting guest registration \ "Guest registration disabled / registration enabled with token configured, rejecting guest registration, \
attempt, initial device name: {:?}", initial device name: {:?}",
body.initial_device_display_name body.initial_device_display_name
); );
return Err(Error::BadRequest( return Err(Error::BadRequest(
@@ -178,7 +172,8 @@ pub(crate) async fn register_route(
// UIAA // UIAA
let mut uiaainfo; let mut uiaainfo;
let skip_auth = if services().globals.config.registration_token.is_some() { let skip_auth;
if services().globals.config.registration_token.is_some() {
// Registration token required // Registration token required
uiaainfo = UiaaInfo { uiaainfo = UiaaInfo {
flows: vec![AuthFlow { flows: vec![AuthFlow {
@@ -189,7 +184,7 @@ pub(crate) async fn register_route(
session: None, session: None,
auth_error: None, auth_error: None,
}; };
body.appservice_info.is_some() skip_auth = body.appservice_info.is_some();
} else { } else {
// No registration token necessary, but clients must still go through the flow // No registration token necessary, but clients must still go through the flow
uiaainfo = UiaaInfo { uiaainfo = UiaaInfo {
@@ -201,8 +196,8 @@ pub(crate) async fn register_route(
session: None, session: None,
auth_error: None, auth_error: None,
}; };
body.appservice_info.is_some() || is_guest skip_auth = body.appservice_info.is_some() || is_guest;
}; }
if !skip_auth { if !skip_auth {
if let Some(auth) = &body.auth { if let Some(auth) = &body.auth {
@@ -245,8 +240,7 @@ pub(crate) async fn register_route(
// If `new_user_displayname_suffix` is set, registration will push whatever // If `new_user_displayname_suffix` is set, registration will push whatever
// content is set to the user's display name with a space before it // content is set to the user's display name with a space before it
if !services().globals.new_user_displayname_suffix().is_empty() { if !services().globals.new_user_displayname_suffix().is_empty() {
write!(displayname, " {}", services().globals.config.new_user_displayname_suffix) _ = write!(displayname, " {}", services().globals.config.new_user_displayname_suffix);
.expect("should be able to write to string buffer");
} }
services() services()
@@ -294,23 +288,20 @@ pub(crate) async fn register_route(
.users .users
.create_device(&user_id, &device_id, &token, body.initial_device_display_name.clone())?; .create_device(&user_id, &device_id, &token, body.initial_device_display_name.clone())?;
debug_info!(%user_id, %device_id, "User account was created"); info!("New user \"{}\" registered on this server.", user_id);
// log in conduit admin channel if a non-guest user registered // log in conduit admin channel if a non-guest user registered
if body.appservice_info.is_none() && !is_guest { if body.appservice_info.is_none() && !is_guest {
info!("New user \"{user_id}\" registered on this server.");
services() services()
.admin .admin
.send_message(RoomMessageEventContent::notice_plain(format!( .send_message(RoomMessageEventContent::notice_plain(format!(
"New user \"{user_id}\" registered on this server from IP {client}." "New user \"{user_id}\" registered on this server."
))) )))
.await; .await;
} }
// log in conduit admin channel if a guest registered // log in conduit admin channel if a guest registered
if body.appservice_info.is_none() && is_guest && services().globals.log_guest_registrations() { if body.appservice_info.is_none() && is_guest && services().globals.log_guest_registrations() {
info!("New guest user \"{user_id}\" registered on this server from IP.");
if let Some(device_display_name) = &body.initial_device_display_name { if let Some(device_display_name) = &body.initial_device_display_name {
if body if body
.initial_device_display_name .initial_device_display_name
@@ -321,15 +312,14 @@ pub(crate) async fn register_route(
.admin .admin
.send_message(RoomMessageEventContent::notice_plain(format!( .send_message(RoomMessageEventContent::notice_plain(format!(
"Guest user \"{user_id}\" with device display name `{device_display_name}` registered on this \ "Guest user \"{user_id}\" with device display name `{device_display_name}` registered on this \
server from IP {client}." server."
))) )))
.await; .await;
} else { } else {
services() services()
.admin .admin
.send_message(RoomMessageEventContent::notice_plain(format!( .send_message(RoomMessageEventContent::notice_plain(format!(
"Guest user \"{user_id}\" with no device display name registered on this server from IP \ "Guest user \"{user_id}\" with no device display name registered on this server.",
{client}.",
))) )))
.await; .await;
} }
@@ -337,7 +327,7 @@ pub(crate) async fn register_route(
services() services()
.admin .admin
.send_message(RoomMessageEventContent::notice_plain(format!( .send_message(RoomMessageEventContent::notice_plain(format!(
"Guest user \"{user_id}\" with no device display name registered on this server from IP {client}.", "Guest user \"{user_id}\" with no device display name registered on this server.",
))) )))
.await; .await;
} }
@@ -346,16 +336,19 @@ pub(crate) async fn register_route(
// If this is the first real user, grant them admin privileges except for guest // If this is the first real user, grant them admin privileges except for guest
// users Note: the server user, @conduit:servername, is generated first // users Note: the server user, @conduit:servername, is generated first
if !is_guest { if !is_guest {
if let Some(admin_room) = service::admin::Service::get_admin_room()? { if let Some(admin_room) = service::admin::Service::get_admin_room().await? {
if services() if services()
.rooms .rooms
.state_cache .state_cache
.room_joined_count(&admin_room)? .room_joined_count(&admin_room)?
== Some(1) == Some(1)
{ {
service::admin::make_user_admin(&user_id, displayname).await?; services()
.admin
.make_user_admin(&user_id, displayname)
.await?;
warn!("Granting {user_id} admin privileges as the first user"); warn!("Granting {} admin privileges as the first user", user_id);
} }
} }
} }
@@ -364,33 +357,7 @@ pub(crate) async fn register_route(
&& !services().globals.config.auto_join_rooms.is_empty() && !services().globals.config.auto_join_rooms.is_empty()
&& (services().globals.allow_guests_auto_join_rooms() || !is_guest) && (services().globals.allow_guests_auto_join_rooms() || !is_guest)
{ {
for room in &services().globals.config.auto_join_rooms { auto_join_rooms(&user_id).await?;
if !services()
.rooms
.state_cache
.server_in_room(services().globals.server_name(), room)?
{
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 Err(e) = join_room_by_id_helper(
Some(&user_id),
room,
Some("Automatically joining this room upon registration".to_owned()),
&[room_id_server_name.to_owned(), services().globals.server_name().to_owned()],
None,
)
.await
{
// don't return this error so we don't fail registrations
error!("Failed to automatically join room {room} for user {user_id}: {e}");
} else {
info!("Automatically joined room {room} for user {user_id}");
};
}
}
} }
Ok(register::v3::Response { Ok(register::v3::Response {
@@ -419,9 +386,8 @@ pub(crate) async fn register_route(
/// last seen ts) /// last seen ts)
/// - Forgets to-device events /// - Forgets to-device events
/// - Triggers device list updates /// - Triggers device list updates
#[tracing::instrument(skip_all, fields(%client), name = "change_password")]
pub(crate) async fn change_password_route( pub(crate) async fn change_password_route(
InsecureClientIp(client): InsecureClientIp, body: Ruma<change_password::v3::Request>, body: Ruma<change_password::v3::Request>,
) -> Result<change_password::v3::Response> { ) -> Result<change_password::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); 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_device = body.sender_device.as_ref().expect("user is authenticated");
@@ -470,7 +436,7 @@ pub(crate) async fn change_password_route(
} }
} }
info!("User {sender_user} changed their password."); info!("User {} changed their password.", sender_user);
services() services()
.admin .admin
.send_message(RoomMessageEventContent::notice_plain(format!( .send_message(RoomMessageEventContent::notice_plain(format!(
@@ -508,10 +474,7 @@ pub(crate) async fn whoami_route(body: Ruma<whoami::v3::Request>) -> Result<whoa
/// - Forgets all to-device events /// - Forgets all to-device events
/// - Triggers device list updates /// - Triggers device list updates
/// - Removes ability to log in again /// - Removes ability to log in again
#[tracing::instrument(skip_all, fields(%client), name = "deactivate")] pub(crate) async fn deactivate_route(body: Ruma<deactivate::v3::Request>) -> Result<deactivate::v3::Response> {
pub(crate) async fn deactivate_route(
InsecureClientIp(client): InsecureClientIp, body: Ruma<deactivate::v3::Request>,
) -> Result<deactivate::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); 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_device = body.sender_device.as_ref().expect("user is authenticated");
@@ -543,23 +506,13 @@ pub(crate) async fn deactivate_route(
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json.")); return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
} }
// Make the user leave all rooms before deactivation
client_server::leave_all_rooms(sender_user).await?;
// Remove devices and mark account as deactivated // Remove devices and mark account as deactivated
services().users.deactivate_account(sender_user)?; services().users.deactivate_account(sender_user)?;
// Remove profile pictures and display name info!("User {} deactivated their account.", sender_user);
let all_joined_rooms: Vec<OwnedRoomId> = services()
.rooms
.state_cache
.rooms_joined(sender_user)
.filter_map(Result::ok)
.collect();
super::update_displayname(sender_user.clone(), None, all_joined_rooms.clone()).await?;
super::update_avatar_url(sender_user.clone(), None, None, all_joined_rooms).await?;
// Make the user leave all rooms before deactivation
super::leave_all_rooms(sender_user).await;
info!("User {sender_user} deactivated their account.");
services() services()
.admin .admin
.send_message(RoomMessageEventContent::notice_plain(format!( .send_message(RoomMessageEventContent::notice_plain(format!(
@@ -615,23 +568,39 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
)) ))
} }
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity` /// Auto joins all the specified users to the rooms listed in the
/// `auto_join_rooms` config option
/// ///
/// Checks if the provided registration token is valid at the time of checking /// TODO: can this be moved somewhere else instead of this file where routes are
/// /// typically defined?
/// Currently does not have any ratelimiting, and this isn't very practical as pub(crate) async fn auto_join_rooms(user_id: &UserId) -> Result<()> {
/// there is only one registration token allowed. for room in &services().globals.config.auto_join_rooms {
pub(crate) async fn check_registration_token_validity( if !services()
body: Ruma<check_registration_token_validity::v1::Request>, .rooms
) -> Result<check_registration_token_validity::v1::Response> { .state_cache
let Some(reg_token) = services().globals.config.registration_token.clone() else { .server_in_room(&services().globals.config.server_name, room)?
return Err(Error::BadRequest( {
ErrorKind::forbidden(), warn!("Skipping room {room} to automatically join as we have never joined before.");
"Server does not allow token registration.", continue;
)); }
};
if let Some(room_id_server_name) = room.server_name() {
Ok(check_registration_token_validity::v1::Response { if let Err(e) = join_room_by_id_helper(
valid: reg_token == body.token, Some(user_id),
}) room,
Some("Automatically joining this room upon registration".to_owned()),
&[room_id_server_name.to_owned(), services().globals.config.server_name.clone()],
None,
)
.await
{
// don't return this error so we don't fail registrations
error!("Failed to automatically join room {room} for user {user_id}: {e}");
} else {
info!("Automatically joined room {room} for user {user_id}");
};
}
}
Ok(())
} }
@@ -8,22 +8,19 @@ use ruma::{
}, },
federation, federation,
}, },
OwnedRoomAliasId, OwnedServerName, RoomAliasId, RoomId, OwnedRoomAliasId, OwnedRoomId, OwnedServerName,
}; };
use tracing::debug; use tracing::debug;
use crate::{ use crate::{
debug_info, debug_warn, debug_info, debug_warn, service::appservice::RegistrationInfo, services, utils::server_name::server_is_ours, Error,
service::{appservice::RegistrationInfo, server_is_ours}, Result, Ruma,
services, Error, Result, Ruma,
}; };
/// # `PUT /_matrix/client/v3/directory/room/{roomAlias}` /// # `PUT /_matrix/client/v3/directory/room/{roomAlias}`
/// ///
/// Creates a new room alias on this server. /// Creates a new room alias on this server.
pub(crate) async fn create_alias_route(body: Ruma<create_alias::v3::Request>) -> Result<create_alias::v3::Response> { pub(crate) async fn create_alias_route(body: Ruma<create_alias::v3::Request>) -> Result<create_alias::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
alias_checks(&body.room_alias, &body.appservice_info).await?; alias_checks(&body.room_alias, &body.appservice_info).await?;
// this isn't apart of alias_checks or delete alias route because we should // this isn't apart of alias_checks or delete alias route because we should
@@ -45,10 +42,17 @@ pub(crate) async fn create_alias_route(body: Ruma<create_alias::v3::Request>) ->
return Err(Error::Conflict("Alias already exists.")); return Err(Error::Conflict("Alias already exists."));
} }
services() if services()
.rooms .rooms
.alias .alias
.set_alias(&body.room_alias, &body.room_id, sender_user)?; .set_alias(&body.room_alias, &body.room_id)
.is_err()
{
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid room alias. Alias must be in the form of '#localpart:server_name'",
));
};
Ok(create_alias::v3::Response::new()) Ok(create_alias::v3::Response::new())
} }
@@ -57,10 +61,9 @@ pub(crate) async fn create_alias_route(body: Ruma<create_alias::v3::Request>) ->
/// ///
/// Deletes a room alias from this server. /// Deletes a room alias from this server.
/// ///
/// - TODO: additional access control checks
/// - TODO: Update canonical alias event /// - TODO: Update canonical alias event
pub(crate) async fn delete_alias_route(body: Ruma<delete_alias::v3::Request>) -> Result<delete_alias::v3::Response> { pub(crate) async fn delete_alias_route(body: Ruma<delete_alias::v3::Request>) -> Result<delete_alias::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
alias_checks(&body.room_alias, &body.appservice_info).await?; alias_checks(&body.room_alias, &body.appservice_info).await?;
if services() if services()
@@ -72,11 +75,17 @@ pub(crate) async fn delete_alias_route(body: Ruma<delete_alias::v3::Request>) ->
return Err(Error::BadRequest(ErrorKind::NotFound, "Alias does not exist.")); return Err(Error::BadRequest(ErrorKind::NotFound, "Alias does not exist."));
} }
services() if services()
.rooms .rooms
.alias .alias
.remove_alias(&body.room_alias, sender_user) .remove_alias(&body.room_alias)
.await?; .is_err()
{
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid room alias. Alias must be in the form of '#localpart:server_name'",
));
};
// TODO: update alt_aliases? // TODO: update alt_aliases?
@@ -90,7 +99,7 @@ pub(crate) async fn get_alias_route(body: Ruma<get_alias::v3::Request>) -> Resul
get_alias_helper(body.body.room_alias, None).await get_alias_helper(body.body.room_alias, None).await
} }
pub async fn get_alias_helper( pub(crate) async fn get_alias_helper(
room_alias: OwnedRoomAliasId, servers: Option<Vec<OwnedServerName>>, room_alias: OwnedRoomAliasId, servers: Option<Vec<OwnedServerName>>,
) -> Result<get_alias::v3::Response> { ) -> Result<get_alias::v3::Response> {
debug!("get_alias_helper servers: {servers:?}"); debug!("get_alias_helper servers: {servers:?}");
@@ -110,7 +119,7 @@ pub async fn get_alias_helper(
) )
.await; .await;
debug!("room alias server_name get_alias_helper response: {response:?}"); debug_info!("room alias server_name get_alias_helper response: {response:?}");
if let Err(ref e) = response { if let Err(ref e) = response {
debug_info!( debug_info!(
@@ -131,7 +140,7 @@ pub async fn get_alias_helper(
}, },
) )
.await; .await;
debug!("Got response from server {server} for room aliases: {response:?}"); debug_info!("Got response from server {server} for room aliases: {response:?}");
if let Ok(ref response) = response { if let Ok(ref response) = response {
if !response.servers.is_empty() { if !response.servers.is_empty() {
@@ -153,7 +162,7 @@ pub async fn get_alias_helper(
pre_servers.push(room_alias.server_name().into()); pre_servers.push(room_alias.server_name().into());
let servers = room_available_servers(&room_id, &room_alias, &Some(pre_servers)); let servers = room_available_servers(&room_id, &room_alias, &Some(pre_servers));
debug!( debug_warn!(
"room alias servers from federation response for room ID {room_id} and room alias {room_alias}: \ "room alias servers from federation response for room ID {room_id} and room alias {room_alias}: \
{servers:?}" {servers:?}"
); );
@@ -204,13 +213,13 @@ pub async fn get_alias_helper(
let servers = room_available_servers(&room_id, &room_alias, &None); let servers = room_available_servers(&room_id, &room_alias, &None);
debug!("room alias servers for room ID {room_id} and room alias {room_alias}"); debug_warn!("room alias servers for room ID {room_id} and room alias {room_alias}");
Ok(get_alias::v3::Response::new(room_id, servers)) Ok(get_alias::v3::Response::new(room_id, servers))
} }
fn room_available_servers( fn room_available_servers(
room_id: &RoomId, room_alias: &RoomAliasId, pre_servers: &Option<Vec<OwnedServerName>>, room_id: &OwnedRoomId, room_alias: &OwnedRoomAliasId, pre_servers: &Option<Vec<OwnedServerName>>,
) -> Vec<OwnedServerName> { ) -> Vec<OwnedServerName> {
// find active servers in room state cache to suggest // find active servers in room state cache to suggest
let mut servers: Vec<OwnedServerName> = services() let mut servers: Vec<OwnedServerName> = services()
@@ -238,20 +247,20 @@ fn room_available_servers(
.iter() .iter()
.position(|server_name| server_is_ours(server_name)) .position(|server_name| server_is_ours(server_name))
{ {
servers.swap_remove(server_index); servers.remove(server_index);
servers.insert(0, services().globals.server_name().to_owned()); servers.insert(0, services().globals.server_name().to_owned());
} else if let Some(alias_server_index) = servers } else if let Some(alias_server_index) = servers
.iter() .iter()
.position(|server| server == room_alias.server_name()) .position(|server| server == room_alias.server_name())
{ {
servers.swap_remove(alias_server_index); servers.remove(alias_server_index);
servers.insert(0, room_alias.server_name().into()); servers.insert(0, room_alias.server_name().into());
} }
servers servers
} }
async fn alias_checks(room_alias: &RoomAliasId, appservice_info: &Option<RegistrationInfo>) -> Result<()> { async fn alias_checks(room_alias: &OwnedRoomAliasId, appservice_info: &Option<RegistrationInfo>) -> Result<()> {
if !server_is_ours(room_alias.server_name()) { if !server_is_ours(room_alias.server_name()) {
return Err(Error::BadRequest(ErrorKind::InvalidParam, "Alias is from another server.")); return Err(Error::BadRequest(ErrorKind::InvalidParam, "Alias is from another server."));
} }
@@ -13,7 +13,8 @@ use crate::{services, Error, Result, Ruma};
/// Allows loading room history around an event. /// Allows loading room history around an event.
/// ///
/// - Only works if the user is joined (TODO: always allow, but only show events /// - Only works if the user is joined (TODO: always allow, but only show events
/// if the user was joined, depending on history_visibility) /// if the user was
/// joined, depending on history_visibility)
pub(crate) async fn get_context_route(body: Ruma<get_context::v3::Request>) -> Result<get_context::v3::Response> { pub(crate) async fn get_context_route(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_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated"); let sender_device = body.sender_device.as_ref().expect("user is authenticated");
@@ -162,7 +163,7 @@ pub(crate) async fn get_context_route(body: Ruma<get_context::v3::Request>) -> R
.map(|(_, pdu)| pdu.to_room_event()) .map(|(_, pdu)| pdu.to_room_event())
.collect(); .collect();
let mut state = Vec::with_capacity(state_ids.len()); let mut state = Vec::new();
for (shortstatekey, id) in state_ids { for (shortstatekey, id) in state_ids {
let (event_type, state_key) = services() let (event_type, state_key) = services()
@@ -1,4 +1,3 @@
use axum_client_ip::InsecureClientIp;
use ruma::{ use ruma::{
api::{ api::{
client::{ client::{
@@ -12,8 +11,12 @@ use ruma::{
events::{ events::{
room::{ room::{
avatar::RoomAvatarEventContent, avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent, create::RoomCreateEventContent,
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent}, join_rules::{JoinRule, RoomJoinRulesEventContent},
topic::RoomTopicEventContent,
}, },
StateEventType, StateEventType,
}, },
@@ -21,16 +24,15 @@ use ruma::{
}; };
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use crate::{service::server_is_ours, services, Error, Result, Ruma}; use crate::{services, utils::server_name::server_is_ours, Error, Result, Ruma};
/// # `POST /_matrix/client/v3/publicRooms` /// # `POST /_matrix/client/v3/publicRooms`
/// ///
/// Lists the public rooms on this server. /// Lists the public rooms on this server.
/// ///
/// - Rooms are ordered by the number of joined members /// - Rooms are ordered by the number of joined members
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms")]
pub(crate) async fn get_public_rooms_filtered_route( pub(crate) async fn get_public_rooms_filtered_route(
InsecureClientIp(client): InsecureClientIp, body: Ruma<get_public_rooms_filtered::v3::Request>, body: Ruma<get_public_rooms_filtered::v3::Request>,
) -> Result<get_public_rooms_filtered::v3::Response> { ) -> Result<get_public_rooms_filtered::v3::Response> {
if let Some(server) = &body.server { if let Some(server) = &body.server {
if services() if services()
@@ -54,8 +56,8 @@ pub(crate) async fn get_public_rooms_filtered_route(
) )
.await .await
.map_err(|e| { .map_err(|e| {
warn!(?body.server, "Failed to return /publicRooms: {e}"); warn!("Failed to return our /publicRooms: {e}");
Error::BadRequest(ErrorKind::Unknown, "Failed to return the requested server's public room list.") Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.")
})?; })?;
Ok(response) Ok(response)
@@ -66,9 +68,8 @@ pub(crate) async fn get_public_rooms_filtered_route(
/// Lists the public rooms on this server. /// Lists the public rooms on this server.
/// ///
/// - Rooms are ordered by the number of joined members /// - Rooms are ordered by the number of joined members
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms")]
pub(crate) async fn get_public_rooms_route( pub(crate) async fn get_public_rooms_route(
InsecureClientIp(client): InsecureClientIp, body: Ruma<get_public_rooms::v3::Request>, body: Ruma<get_public_rooms::v3::Request>,
) -> Result<get_public_rooms::v3::Response> { ) -> Result<get_public_rooms::v3::Response> {
if let Some(server) = &body.server { if let Some(server) = &body.server {
if services() if services()
@@ -92,8 +93,8 @@ pub(crate) async fn get_public_rooms_route(
) )
.await .await
.map_err(|e| { .map_err(|e| {
warn!(?body.server, "Failed to return /publicRooms: {e}"); warn!("Failed to return our /publicRooms: {e}");
Error::BadRequest(ErrorKind::Unknown, "Failed to return the requested server's public room list.") Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.")
})?; })?;
Ok(get_public_rooms::v3::Response { Ok(get_public_rooms::v3::Response {
@@ -109,9 +110,8 @@ pub(crate) async fn get_public_rooms_route(
/// Sets the visibility of a given room in the room directory. /// Sets the visibility of a given room in the room directory.
/// ///
/// - TODO: Access control checks /// - TODO: Access control checks
#[tracing::instrument(skip_all, fields(%client), name = "room_directory")]
pub(crate) async fn set_room_visibility_route( pub(crate) async fn set_room_visibility_route(
InsecureClientIp(client): InsecureClientIp, body: Ruma<set_room_visibility::v3::Request>, body: Ruma<set_room_visibility::v3::Request>,
) -> Result<set_room_visibility::v3::Response> { ) -> Result<set_room_visibility::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
@@ -231,7 +231,12 @@ pub(crate) async fn get_public_rooms_filtered_helper(
canonical_alias: services() canonical_alias: services()
.rooms .rooms
.state_accessor .state_accessor
.get_canonical_alias(&room_id)?, .room_state_get(&room_id, &StateEventType::RoomCanonicalAlias, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomCanonicalAliasEventContent| c.alias)
.map_err(|_| Error::bad_database("Invalid canonical alias event in database."))
})?,
name: services().rooms.state_accessor.get_name(&room_id)?, name: services().rooms.state_accessor.get_name(&room_id)?,
num_joined_members: services() num_joined_members: services()
.rooms .rooms
@@ -246,13 +251,40 @@ pub(crate) async fn get_public_rooms_filtered_helper(
topic: services() topic: services()
.rooms .rooms
.state_accessor .state_accessor
.get_room_topic(&room_id) .room_state_get(&room_id, &StateEventType::RoomTopic, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomTopicEventContent| Some(c.topic))
.map_err(|e| {
error!("Invalid room topic event in database for room {room_id}: {e}");
Error::bad_database("Invalid room topic event in database.")
})
})
.unwrap_or(None), .unwrap_or(None),
world_readable: services().rooms.state_accessor.is_world_readable(&room_id)?, world_readable: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomHistoryVisibility, "")?
.map_or(Ok(false), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomHistoryVisibilityEventContent| {
c.history_visibility == HistoryVisibility::WorldReadable
})
.map_err(|e| {
error!(
"Invalid room history visibility event in database for room {room_id}, assuming is \"shared\": {e}",
);
Error::bad_database("Invalid room history visibility event in database.")
})}).unwrap_or(false),
guest_can_join: services() guest_can_join: services()
.rooms .rooms
.state_accessor .state_accessor
.guest_can_join(&room_id)?, .room_state_get(&room_id, &StateEventType::RoomGuestAccess, "")?
.map_or(Ok(false), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomGuestAccessEventContent| c.guest_access == GuestAccess::CanJoin)
.map_err(|_| Error::bad_database("Invalid room guest access event in database."))
})?,
avatar_url: services() avatar_url: services()
.rooms .rooms
.state_accessor .state_accessor
@@ -22,9 +22,8 @@ use tracing::debug;
use super::SESSION_ID_LENGTH; use super::SESSION_ID_LENGTH;
use crate::{ use crate::{
service::user_is_local,
services, services,
utils::{self}, utils::{self, user_id::user_is_local},
Error, Result, Ruma, Error, Result, Ruma,
}; };
@@ -248,7 +247,7 @@ pub(crate) async fn get_key_changes_route(
}) })
} }
pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool + Send>( pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
sender_user: Option<&UserId>, device_keys_input: &BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>, allowed_signatures: F, sender_user: Option<&UserId>, device_keys_input: &BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>, allowed_signatures: F,
include_display_names: bool, include_display_names: bool,
) -> Result<get_keys::v3::Response> { ) -> Result<get_keys::v3::Response> {
@@ -15,15 +15,9 @@ use webpage::HTML;
use crate::{ use crate::{
debug_warn, debug_warn,
service::{ service::media::{FileMeta, UrlPreviewData},
media::{FileMeta, UrlPreviewData},
server_is_ours,
},
services, services,
utils::{ utils::{self, server_name::server_is_ours},
self,
content_disposition::{content_disposition_type, make_content_disposition, sanitise_filename},
},
Error, Result, Ruma, RumaResponse, Error, Result, Ruma, RumaResponse,
}; };
@@ -136,21 +130,17 @@ pub(crate) async fn create_content_route(
mxc.clone(), mxc.clone(),
body.filename body.filename
.as_ref() .as_ref()
.map(|filename| { .map(|filename| format!("attachment; filename={filename}"))
format!(
"{}; filename={}",
content_disposition_type(&body.content_type),
sanitise_filename(filename.to_owned())
)
})
.as_deref(), .as_deref(),
body.content_type.as_deref(), body.content_type.as_deref(),
&body.file, &body.file,
) )
.await?; .await?;
let content_uri = mxc.into();
Ok(create_content::v3::Response { Ok(create_content::v3::Response {
content_uri: mxc.into(), content_uri,
blurhash: None, blurhash: None,
}) })
} }
@@ -185,20 +175,19 @@ pub(crate) async fn get_content_route(body: Ruma<get_content::v3::Request>) -> R
if let Some(FileMeta { if let Some(FileMeta {
content_type, content_type,
file, file,
content_disposition, ..
}) = services().media.get(mxc.clone()).await? }) = services().media.get(mxc.clone()).await?
{ {
let content_disposition = Some(make_content_disposition(&content_type, content_disposition, None)); // TODO: safely sanitise filename to be included in the content-disposition
Ok(get_content::v3::Response { Ok(get_content::v3::Response {
file, file,
content_type, content_type,
content_disposition, content_disposition: Some("attachment".to_owned()),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()), cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
}) })
} else if !server_is_ours(&body.server_name) && body.allow_remote { } else if !server_is_ours(&body.server_name) && body.allow_remote {
let response = get_remote_content( get_remote_content(
&mxc, &mxc,
&body.server_name, &body.server_name,
body.media_id.clone(), body.media_id.clone(),
@@ -209,20 +198,6 @@ pub(crate) async fn get_content_route(body: Ruma<get_content::v3::Request>) -> R
.map_err(|e| { .map_err(|e| {
debug_warn!("Fetching media `{}` failed: {:?}", mxc, e); debug_warn!("Fetching media `{}` failed: {:?}", mxc, e);
Error::BadRequest(ErrorKind::NotFound, "Remote media error.") Error::BadRequest(ErrorKind::NotFound, "Remote media error.")
})?;
let content_disposition = Some(make_content_disposition(
&response.content_type,
response.content_disposition,
None,
));
Ok(get_content::v3::Response {
file: response.file,
content_type: response.content_type,
content_disposition,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
}) })
} else { } else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found.")) Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
@@ -263,19 +238,13 @@ pub(crate) async fn get_content_as_filename_route(
if let Some(FileMeta { if let Some(FileMeta {
content_type, content_type,
file, file,
content_disposition, ..
}) = services().media.get(mxc.clone()).await? }) = services().media.get(mxc.clone()).await?
{ {
let content_disposition = Some(make_content_disposition(
&content_type,
content_disposition,
Some(body.filename.clone()),
));
Ok(get_content_as_filename::v3::Response { Ok(get_content_as_filename::v3::Response {
file, file,
content_type, content_type,
content_disposition, content_disposition: Some("attachment".to_owned()),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()), cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
}) })
@@ -289,21 +258,13 @@ pub(crate) async fn get_content_as_filename_route(
) )
.await .await
{ {
Ok(remote_content_response) => { Ok(remote_content_response) => Ok(get_content_as_filename::v3::Response {
let content_disposition = Some(make_content_disposition( content_disposition: Some("attachment".to_owned()),
&remote_content_response.content_type,
remote_content_response.content_disposition,
None,
));
Ok(get_content_as_filename::v3::Response {
content_disposition,
content_type: remote_content_response.content_type, content_type: remote_content_response.content_type,
file: remote_content_response.file, file: remote_content_response.file,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()), cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
}) }),
},
Err(e) => { Err(e) => {
debug_warn!("Fetching media `{}` failed: {:?}", mxc, e); debug_warn!("Fetching media `{}` failed: {:?}", mxc, e);
Err(Error::BadRequest(ErrorKind::NotFound, "Remote media error.")) Err(Error::BadRequest(ErrorKind::NotFound, "Remote media error."))
@@ -348,7 +309,7 @@ pub(crate) async fn get_content_thumbnail_route(
if let Some(FileMeta { if let Some(FileMeta {
content_type, content_type,
file, file,
content_disposition, ..
}) = services() }) = services()
.media .media
.get_thumbnail( .get_thumbnail(
@@ -362,20 +323,18 @@ pub(crate) async fn get_content_thumbnail_route(
) )
.await? .await?
{ {
let content_disposition = Some(make_content_disposition(&content_type, content_disposition, None));
Ok(get_content_thumbnail::v3::Response { Ok(get_content_thumbnail::v3::Response {
file, file,
content_type, content_type,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()), cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition, content_disposition: Some("attachment".to_owned()),
}) })
} else if !server_is_ours(&body.server_name) && body.allow_remote { } else if !server_is_ours(&body.server_name) && body.allow_remote {
if services() if services()
.globals .globals
.prevent_media_downloads_from() .prevent_media_downloads_from()
.contains(&body.server_name) .contains(&body.server_name.clone())
{ {
// we'll lie to the client and say the blocked server's media was not found and // we'll lie to the client and say the blocked server's media was not found and
// log. the client has no way of telling anyways so this is a security bonus. // log. the client has no way of telling anyways so this is a security bonus.
@@ -414,18 +373,12 @@ pub(crate) async fn get_content_thumbnail_route(
) )
.await?; .await?;
let content_disposition = Some(make_content_disposition(
&get_thumbnail_response.content_type,
get_thumbnail_response.content_disposition,
None,
));
Ok(get_content_thumbnail::v3::Response { Ok(get_content_thumbnail::v3::Response {
file: get_thumbnail_response.file, file: get_thumbnail_response.file,
content_type: get_thumbnail_response.content_type, content_type: get_thumbnail_response.content_type,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()), cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
content_disposition, content_disposition: Some("attachment".to_owned()),
}) })
}, },
Err(e) => { Err(e) => {
@@ -484,18 +437,12 @@ async fn get_remote_content(
) )
.await?; .await?;
let content_disposition = Some(make_content_disposition(
&content_response.content_type,
content_response.content_disposition,
None,
));
services() services()
.media .media
.create( .create(
None, None,
mxc.to_owned(), mxc.to_owned(),
content_disposition.as_deref(), Some("attachment"),
content_response.content_type.as_deref(), content_response.content_type.as_deref(),
&content_response.file, &content_response.file,
) )
@@ -504,7 +451,7 @@ async fn get_remote_content(
Ok(get_content::v3::Response { Ok(get_content::v3::Response {
file: content_response.file, file: content_response.file,
content_type: content_response.content_type, content_type: content_response.content_type,
content_disposition, content_disposition: Some("attachment".to_owned()),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()), cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()), cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
}) })
@@ -1,13 +1,10 @@
use std::{ use std::{
cmp, cmp,
collections::{hash_map::Entry, BTreeMap, HashMap, HashSet}, collections::{hash_map::Entry, BTreeMap, HashMap, HashSet},
net::IpAddr,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use axum_client_ip::InsecureClientIp;
use conduit::utils::mutex_map;
use ruma::{ use ruma::{
api::{ api::{
client::{ client::{
@@ -24,13 +21,12 @@ use ruma::{
room::{ room::{
join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent}, join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent}, member::{MembershipState, RoomMemberEventContent},
message::RoomMessageEventContent,
}, },
StateEventType, TimelineEventType, StateEventType, TimelineEventType,
}, },
serde::Base64, serde::Base64,
state_res, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, OwnedServerName, state_res, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, OwnedServerName,
OwnedUserId, RoomId, RoomVersionId, ServerName, UserId, OwnedUserId, RoomId, RoomVersionId, UserId,
}; };
use serde_json::value::{to_raw_value, RawValue as RawJsonValue}; use serde_json::value::{to_raw_value, RawValue as RawJsonValue};
use tokio::sync::RwLock; use tokio::sync::RwLock;
@@ -38,125 +34,12 @@ use tracing::{debug, error, info, trace, warn};
use super::get_alias_helper; use super::get_alias_helper;
use crate::{ use crate::{
client::{update_avatar_url, update_displayname}, service::pdu::{gen_event_id_canonical_json, PduBuilder},
service::{ services,
pdu::{gen_event_id_canonical_json, PduBuilder}, utils::{self, server_name::server_is_ours, user_id::user_is_local},
server_is_ours, user_is_local, Error, PduEvent, Result, Ruma,
},
services, utils, Error, PduEvent, Result, Ruma,
}; };
/// Checks if the room is banned in any way possible and the sender user is not
/// an admin.
///
/// Performs automatic deactivation if `auto_deactivate_banned_room_attempts` is
/// enabled
#[tracing::instrument]
async fn banned_room_check(
user_id: &UserId, room_id: Option<&RoomId>, server_name: Option<&ServerName>, client_ip: IpAddr,
) -> Result<()> {
if !services().users.is_admin(user_id)? {
if let Some(room_id) = room_id {
if services().rooms.metadata.is_banned(room_id)?
|| services()
.globals
.config
.forbidden_remote_server_names
.contains(&room_id.server_name().unwrap().to_owned())
{
warn!(
"User {user_id} who is not an admin attempted to send an invite for or attempted to join a banned \
room or banned room server name: {room_id}"
);
if services()
.globals
.config
.auto_deactivate_banned_room_attempts
{
warn!("Automatically deactivating user {user_id} due to attempted banned room join");
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Automatically deactivating user {user_id} due to attempted banned room join from IP \
{client_ip}"
)))
.await;
if let Err(e) = services().users.deactivate_account(user_id) {
warn!(%user_id, %e, "Failed to deactivate account");
}
let all_joined_rooms: Vec<OwnedRoomId> = services()
.rooms
.state_cache
.rooms_joined(user_id)
.filter_map(Result::ok)
.collect();
update_displayname(user_id.into(), None, all_joined_rooms.clone()).await?;
update_avatar_url(user_id.into(), None, None, all_joined_rooms).await?;
leave_all_rooms(user_id).await;
}
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This room is banned on this homeserver.",
));
}
} else if let Some(server_name) = server_name {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&server_name.to_owned())
{
warn!(
"User {user_id} who is not an admin tried joining a room which has the server name {server_name} \
that is globally forbidden. Rejecting.",
);
if services()
.globals
.config
.auto_deactivate_banned_room_attempts
{
warn!("Automatically deactivating user {user_id} due to attempted banned room join");
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Automatically deactivating user {user_id} due to attempted banned room join from IP \
{client_ip}"
)))
.await;
if let Err(e) = services().users.deactivate_account(user_id) {
warn!(%user_id, %e, "Failed to deactivate account");
}
let all_joined_rooms: Vec<OwnedRoomId> = services()
.rooms
.state_cache
.rooms_joined(user_id)
.filter_map(Result::ok)
.collect();
update_displayname(user_id.into(), None, all_joined_rooms.clone()).await?;
update_avatar_url(user_id.into(), None, None, all_joined_rooms).await?;
leave_all_rooms(user_id).await;
}
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
}
}
Ok(())
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/join` /// # `POST /_matrix/client/r0/rooms/{roomId}/join`
/// ///
/// Tries to join the sender user into a room. /// Tries to join the sender user into a room.
@@ -165,23 +48,44 @@ async fn banned_room_check(
/// rules locally /// rules locally
/// - If the server does not know about the room: asks other servers over /// - If the server does not know about the room: asks other servers over
/// federation /// federation
#[tracing::instrument(skip_all, fields(%client_ip), name = "join")]
pub(crate) async fn join_room_by_id_route( pub(crate) async fn join_room_by_id_route(
InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<join_room_by_id::v3::Request>, body: Ruma<join_room_by_id::v3::Request>,
) -> Result<join_room_by_id::v3::Response> { ) -> Result<join_room_by_id::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
banned_room_check(sender_user, Some(&body.room_id), body.room_id.server_name(), client_ip).await?; if services().rooms.metadata.is_banned(&body.room_id)? && !services().users.is_admin(sender_user)? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This room is banned on this homeserver.",
));
}
if let Some(server) = body.room_id.server_name() {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&server.to_owned())
&& !services().users.is_admin(sender_user)?
{
warn!(
"User {sender_user} tried joining room ID {} which has a server name that is globally forbidden. \
Rejecting.",
body.room_id
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
}
// There is no body.server_name for /roomId/join // There is no body.server_name for /roomId/join
let mut servers = services() let mut servers = services()
.rooms .rooms
.state_cache .state_cache
.servers_invite_via(&body.room_id) .servers_invite_via(&body.room_id)?
.filter_map(Result::ok) .unwrap_or(
.collect::<Vec<_>>();
servers.extend(
services() services()
.rooms .rooms
.state_cache .state_cache
@@ -192,7 +96,8 @@ pub(crate) async fn join_room_by_id_route(
.filter_map(|event: serde_json::Value| event.get("sender").cloned()) .filter_map(|event: serde_json::Value| event.get("sender").cloned())
.filter_map(|sender| sender.as_str().map(ToOwned::to_owned)) .filter_map(|sender| sender.as_str().map(ToOwned::to_owned))
.filter_map(|sender| UserId::parse(sender).ok()) .filter_map(|sender| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()), .map(|user| user.server_name().to_owned())
.collect::<Vec<_>>(),
); );
if let Some(server) = body.room_id.server_name() { if let Some(server) = body.room_id.server_name() {
@@ -218,27 +123,47 @@ pub(crate) async fn join_room_by_id_route(
/// - If the server does not know about the room: use the server name query /// - If the server does not know about the room: use the server name query
/// param if specified. if not specified, asks other servers over federation /// param if specified. if not specified, asks other servers over federation
/// via room alias server name and room ID server name /// via room alias server name and room ID server name
#[tracing::instrument(skip_all, fields(%client), name = "join")]
pub(crate) async fn join_room_by_id_or_alias_route( pub(crate) async fn join_room_by_id_or_alias_route(
InsecureClientIp(client): InsecureClientIp, body: Ruma<join_room_by_id_or_alias::v3::Request>, body: Ruma<join_room_by_id_or_alias::v3::Request>,
) -> Result<join_room_by_id_or_alias::v3::Response> { ) -> Result<join_room_by_id_or_alias::v3::Response> {
let sender_user = body.sender_user.as_deref().expect("user is authenticated"); let sender_user = body.sender_user.as_deref().expect("user is authenticated");
let body = body.body; let body = body.body;
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) { let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) {
Ok(room_id) => { Ok(room_id) => {
banned_room_check(sender_user, Some(&room_id), room_id.server_name(), client).await?; if services().rooms.metadata.is_banned(&room_id)? && !services().users.is_admin(sender_user)? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This room is banned on this homeserver.",
));
}
if let Some(server) = room_id.server_name() {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&server.to_owned())
&& !services().users.is_admin(sender_user)?
{
warn!(
"User {sender_user} tried joining room ID {room_id} which has a server name that is globally \
forbidden. Rejecting.",
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
}
let mut servers = body.server_name.clone(); let mut servers = body.server_name.clone();
servers.extend( servers.extend(
services() services()
.rooms .rooms
.state_cache .state_cache
.servers_invite_via(&room_id) .servers_invite_via(&room_id)?
.filter_map(Result::ok), .unwrap_or(
);
servers.extend(
services() services()
.rooms .rooms
.state_cache .state_cache
@@ -249,7 +174,9 @@ pub(crate) async fn join_room_by_id_or_alias_route(
.filter_map(|event: serde_json::Value| event.get("sender").cloned()) .filter_map(|event: serde_json::Value| event.get("sender").cloned())
.filter_map(|sender| sender.as_str().map(ToOwned::to_owned)) .filter_map(|sender| sender.as_str().map(ToOwned::to_owned))
.filter_map(|sender| UserId::parse(sender).ok()) .filter_map(|sender| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()), .map(|user| user.server_name().to_owned())
.collect(),
),
); );
if let Some(server) = room_id.server_name() { if let Some(server) = room_id.server_name() {
@@ -259,9 +186,69 @@ pub(crate) async fn join_room_by_id_or_alias_route(
(servers, room_id) (servers, room_id)
}, },
Err(room_alias) => { Err(room_alias) => {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&room_alias.server_name().to_owned())
&& !services().users.is_admin(sender_user)?
{
warn!(
"User {sender_user} tried joining room alias {room_alias} which has a server name that is \
globally forbidden. Rejecting.",
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
let response = get_alias_helper(room_alias.clone(), Some(body.server_name.clone())).await?; let response = get_alias_helper(room_alias.clone(), Some(body.server_name.clone())).await?;
banned_room_check(sender_user, Some(&response.room_id), Some(room_alias.server_name()), client).await?; if services().rooms.metadata.is_banned(&response.room_id)? && !services().users.is_admin(sender_user)? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This room is banned on this homeserver.",
));
}
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&room_alias.server_name().to_owned())
&& !services().users.is_admin(sender_user)?
{
warn!(
"User {sender_user} tried joining room alias {room_alias} with room ID {}, which the alias has a \
server name that is globally forbidden. Rejecting.",
&response.room_id
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
if let Some(server) = response.room_id.server_name() {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&server.to_owned())
&& !services().users.is_admin(sender_user)?
{
warn!(
"User {sender_user} tried joining room alias {room_alias} with room ID {}, which has a server \
name that is globally forbidden. Rejecting.",
&response.room_id
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
}
let mut servers = body.server_name; let mut servers = body.server_name;
servers.extend(response.servers); servers.extend(response.servers);
@@ -269,11 +256,8 @@ pub(crate) async fn join_room_by_id_or_alias_route(
services() services()
.rooms .rooms
.state_cache .state_cache
.servers_invite_via(&response.room_id) .servers_invite_via(&response.room_id)?
.filter_map(Result::ok), .unwrap_or(
);
servers.extend(
services() services()
.rooms .rooms
.state_cache .state_cache
@@ -284,7 +268,9 @@ pub(crate) async fn join_room_by_id_or_alias_route(
.filter_map(|event: serde_json::Value| event.get("sender").cloned()) .filter_map(|event: serde_json::Value| event.get("sender").cloned())
.filter_map(|sender| sender.as_str().map(ToOwned::to_owned)) .filter_map(|sender| sender.as_str().map(ToOwned::to_owned))
.filter_map(|sender| UserId::parse(sender).ok()) .filter_map(|sender| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()), .map(|user| user.server_name().to_owned())
.collect(),
),
); );
(servers, response.room_id) (servers, response.room_id)
@@ -321,10 +307,7 @@ pub(crate) async fn leave_room_route(body: Ruma<leave_room::v3::Request>) -> Res
/// # `POST /_matrix/client/r0/rooms/{roomId}/invite` /// # `POST /_matrix/client/r0/rooms/{roomId}/invite`
/// ///
/// Tries to send an invite event into the room. /// Tries to send an invite event into the room.
#[tracing::instrument(skip_all, fields(%client), name = "invite")] pub(crate) async fn invite_user_route(body: Ruma<invite_user::v3::Request>) -> Result<invite_user::v3::Response> {
pub(crate) async fn invite_user_route(
InsecureClientIp(client): InsecureClientIp, body: Ruma<invite_user::v3::Request>,
) -> Result<invite_user::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services().users.is_admin(sender_user)? && services().globals.block_non_admin_invites() { if !services().users.is_admin(sender_user)? && services().globals.block_non_admin_invites() {
@@ -338,7 +321,30 @@ pub(crate) async fn invite_user_route(
)); ));
} }
banned_room_check(sender_user, Some(&body.room_id), body.room_id.server_name(), client).await?; if services().rooms.metadata.is_banned(&body.room_id)? && !services().users.is_admin(sender_user)? {
info!(
"Local user {} who is not an admin attempted to send an invite for banned room {}.",
&sender_user, &body.room_id
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This room is banned on this homeserver.",
));
}
if let Some(server) = body.room_id.server_name() {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&server.to_owned())
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Server is banned on this homeserver.",
));
}
}
if let invite_user::v3::InvitationRecipient::UserId { if let invite_user::v3::InvitationRecipient::UserId {
user_id, user_id,
@@ -357,6 +363,15 @@ pub(crate) async fn invite_user_route(
pub(crate) async fn kick_user_route(body: Ruma<kick_user::v3::Request>) -> Result<kick_user::v3::Response> { pub(crate) async fn kick_user_route(body: Ruma<kick_user::v3::Request>) -> Result<kick_user::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if let Ok(true) = services()
.rooms
.state_cache
.is_left(sender_user, &body.room_id)
{
info!("{} is not in room {}", &body.user_id, &body.room_id);
return Ok(kick_user::v3::Response {});
}
let mut event: RoomMemberEventContent = serde_json::from_str( let mut event: RoomMemberEventContent = serde_json::from_str(
services() services()
.rooms .rooms
@@ -374,11 +389,16 @@ pub(crate) async fn kick_user_route(body: Ruma<kick_user::v3::Request>) -> Resul
event.membership = MembershipState::Leave; event.membership = MembershipState::Leave;
event.reason.clone_from(&body.reason); event.reason.clone_from(&body.reason);
let state_lock = services() let mutex_state = Arc::clone(
services()
.globals .globals
.roomid_mutex_state .roomid_mutex_state
.lock(&body.room_id) .write()
.await; .await
.entry(body.room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
services() services()
.rooms .rooms
@@ -408,6 +428,17 @@ pub(crate) async fn kick_user_route(body: Ruma<kick_user::v3::Request>) -> Resul
pub(crate) async fn ban_user_route(body: Ruma<ban_user::v3::Request>) -> Result<ban_user::v3::Response> { pub(crate) async fn ban_user_route(body: Ruma<ban_user::v3::Request>) -> Result<ban_user::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if let Ok(Some(membership_event)) = services()
.rooms
.state_accessor
.get_member(&body.room_id, sender_user)
{
if membership_event.membership == MembershipState::Ban {
info!("{} is already banned in {}", &body.user_id, &body.room_id);
return Ok(ban_user::v3::Response {});
}
}
let event = services() let event = services()
.rooms .rooms
.state_accessor .state_accessor
@@ -415,11 +446,11 @@ pub(crate) async fn ban_user_route(body: Ruma<ban_user::v3::Request>) -> Result<
.map_or( .map_or(
Ok(RoomMemberEventContent { Ok(RoomMemberEventContent {
membership: MembershipState::Ban, membership: MembershipState::Ban,
displayname: None, displayname: services().users.displayname(&body.user_id)?,
avatar_url: None, avatar_url: services().users.avatar_url(&body.user_id)?,
is_direct: None, is_direct: None,
third_party_invite: None, third_party_invite: None,
blurhash: services().users.blurhash(&body.user_id).unwrap_or_default(), blurhash: services().users.blurhash(&body.user_id)?,
reason: body.reason.clone(), reason: body.reason.clone(),
join_authorized_via_users_server: None, join_authorized_via_users_server: None,
}), }),
@@ -427,8 +458,14 @@ pub(crate) async fn ban_user_route(body: Ruma<ban_user::v3::Request>) -> Result<
serde_json::from_str(event.content.get()) serde_json::from_str(event.content.get())
.map(|event: RoomMemberEventContent| RoomMemberEventContent { .map(|event: RoomMemberEventContent| RoomMemberEventContent {
membership: MembershipState::Ban, membership: MembershipState::Ban,
displayname: None, displayname: services()
avatar_url: None, .users
.displayname(&body.user_id)
.unwrap_or_default(),
avatar_url: services()
.users
.avatar_url(&body.user_id)
.unwrap_or_default(),
blurhash: services().users.blurhash(&body.user_id).unwrap_or_default(), blurhash: services().users.blurhash(&body.user_id).unwrap_or_default(),
reason: body.reason.clone(), reason: body.reason.clone(),
join_authorized_via_users_server: None, join_authorized_via_users_server: None,
@@ -438,11 +475,16 @@ pub(crate) async fn ban_user_route(body: Ruma<ban_user::v3::Request>) -> Result<
}, },
)?; )?;
let state_lock = services() let mutex_state = Arc::clone(
services()
.globals .globals
.roomid_mutex_state .roomid_mutex_state
.lock(&body.room_id) .write()
.await; .await
.entry(body.room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
services() services()
.rooms .rooms
@@ -472,6 +514,17 @@ pub(crate) async fn ban_user_route(body: Ruma<ban_user::v3::Request>) -> Result<
pub(crate) async fn unban_user_route(body: Ruma<unban_user::v3::Request>) -> Result<unban_user::v3::Response> { pub(crate) async fn unban_user_route(body: Ruma<unban_user::v3::Request>) -> Result<unban_user::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if let Ok(Some(membership_event)) = services()
.rooms
.state_accessor
.get_member(&body.room_id, sender_user)
{
if membership_event.membership != MembershipState::Ban {
info!("{} is already unbanned in {}", &body.user_id, &body.room_id);
return Ok(unban_user::v3::Response {});
}
}
let mut event: RoomMemberEventContent = serde_json::from_str( let mut event: RoomMemberEventContent = serde_json::from_str(
services() services()
.rooms .rooms
@@ -487,11 +540,16 @@ pub(crate) async fn unban_user_route(body: Ruma<unban_user::v3::Request>) -> Res
event.reason.clone_from(&body.reason); event.reason.clone_from(&body.reason);
event.join_authorized_via_users_server = None; event.join_authorized_via_users_server = None;
let state_lock = services() let mutex_state = Arc::clone(
services()
.globals .globals
.roomid_mutex_state .roomid_mutex_state
.lock(&body.room_id) .write()
.await; .await
.entry(body.room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
services() services()
.rooms .rooms
@@ -527,17 +585,6 @@ pub(crate) async fn unban_user_route(body: Ruma<unban_user::v3::Request>) -> Res
pub(crate) async fn forget_room_route(body: Ruma<forget_room::v3::Request>) -> Result<forget_room::v3::Response> { pub(crate) async fn forget_room_route(body: Ruma<forget_room::v3::Request>) -> Result<forget_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services()
.rooms
.state_cache
.is_joined(sender_user, &body.room_id)?
{
return Err(Error::BadRequest(
ErrorKind::Unknown,
"You must leave the room before forgetting it",
));
}
services() services()
.rooms .rooms
.state_cache .state_cache
@@ -643,20 +690,29 @@ pub(crate) async fn joined_members_route(
}) })
} }
pub async fn join_room_by_id_helper( pub(crate) async fn join_room_by_id_helper(
sender_user: Option<&UserId>, room_id: &RoomId, reason: Option<String>, servers: &[OwnedServerName], sender_user: Option<&UserId>, room_id: &RoomId, reason: Option<String>, servers: &[OwnedServerName],
third_party_signed: Option<&ThirdPartySigned>, _third_party_signed: Option<&ThirdPartySigned>,
) -> Result<join_room_by_id::v3::Response> { ) -> Result<join_room_by_id::v3::Response> {
let sender_user = sender_user.expect("user is authenticated"); let sender_user = sender_user.expect("user is authenticated");
if matches!(services().rooms.state_cache.is_joined(sender_user, room_id), Ok(true)) { if let Ok(true) = services().rooms.state_cache.is_joined(sender_user, room_id) {
info!("{sender_user} is already joined in {room_id}"); info!("{sender_user} is already joined in {room_id}");
return Ok(join_room_by_id::v3::Response { return Ok(join_room_by_id::v3::Response {
room_id: room_id.into(), room_id: room_id.into(),
}); });
} }
let state_lock = services().globals.roomid_mutex_state.lock(room_id).await; let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(room_id.to_owned())
.or_default(),
);
let state_lock = mutex_state.lock().await;
// Ask a remote server if we are not participating in this room // Ask a remote server if we are not participating in this room
if !services() if !services()
@@ -664,16 +720,6 @@ pub async fn join_room_by_id_helper(
.state_cache .state_cache
.server_in_room(services().globals.server_name(), room_id)? .server_in_room(services().globals.server_name(), room_id)?
{ {
join_room_by_id_helper_remote(sender_user, room_id, reason, servers, third_party_signed, state_lock).await
} else {
join_room_by_id_helper_local(sender_user, room_id, reason, servers, third_party_signed, state_lock).await
}
}
async fn join_room_by_id_helper_remote(
sender_user: &UserId, room_id: &RoomId, reason: Option<String>, servers: &[OwnedServerName],
_third_party_signed: Option<&ThirdPartySigned>, state_lock: mutex_map::Guard<()>,
) -> Result<join_room_by_id::v3::Response> {
info!("Joining {room_id} over federation."); info!("Joining {room_id} over federation.");
let (make_join_response, remote_server) = make_join_request(sender_user, room_id, servers).await?; let (make_join_response, remote_server) = make_join_request(sender_user, room_id, servers).await?;
@@ -800,10 +846,11 @@ async fn join_room_by_id_helper_remote(
RoomVersionId::V8 | RoomVersionId::V9 | RoomVersionId::V10 | RoomVersionId::V11 => { RoomVersionId::V8 | RoomVersionId::V9 | RoomVersionId::V10 | RoomVersionId::V11 => {
if let Some(signed_raw) = &send_join_response.room_state.event { if let Some(signed_raw) = &send_join_response.room_state.event {
info!( info!(
"There is a signed event. This room is probably using restricted joins. Adding signature to \ "There is a signed event. This room is probably using restricted joins. Adding signature \
our event" to our event"
); );
let Ok((signed_event_id, signed_value)) = gen_event_id_canonical_json(signed_raw, &room_version_id) let Ok((signed_event_id, signed_value)) =
gen_event_id_canonical_json(signed_raw, &room_version_id)
else { else {
// Event could not be converted to canonical json // Event could not be converted to canonical json
return Err(Error::BadRequest( return Err(Error::BadRequest(
@@ -826,8 +873,10 @@ async fn join_room_by_id_helper_remote(
"Server sent invalid signatures type", "Server sent invalid signatures type",
)) ))
.and_then(|e| { .and_then(|e| {
e.get(remote_server.as_str()) e.get(remote_server.as_str()).ok_or(Error::BadRequest(
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Server did not send its signature")) ErrorKind::InvalidParam,
"Server did not send its signature",
))
}) { }) {
Ok(signature) => { Ok(signature) => {
join_event join_event
@@ -1001,14 +1050,7 @@ async fn join_room_by_id_helper_remote(
.rooms .rooms
.state .state
.set_room_state(room_id, statehash_after_join, &state_lock)?; .set_room_state(room_id, statehash_after_join, &state_lock)?;
} else {
Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
}
async fn join_room_by_id_helper_local(
sender_user: &UserId, room_id: &RoomId, reason: Option<String>, servers: &[OwnedServerName],
_third_party_signed: Option<&ThirdPartySigned>, state_lock: mutex_map::Guard<()>,
) -> Result<join_room_by_id::v3::Response> {
info!("We can join locally"); info!("We can join locally");
let join_rules_event = let join_rules_event =
@@ -1029,7 +1071,10 @@ async fn join_room_by_id_helper_local(
let restriction_rooms = match join_rules_event_content { let restriction_rooms = match join_rules_event_content {
Some(RoomJoinRulesEventContent { Some(RoomJoinRulesEventContent {
join_rule: JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted), join_rule: JoinRule::Restricted(restricted),
})
| Some(RoomJoinRulesEventContent {
join_rule: JoinRule::KnockRestricted(restricted),
}) => restricted }) => restricted
.allow .allow
.into_iter() .into_iter()
@@ -1110,7 +1155,10 @@ async fn join_room_by_id_helper_local(
.iter() .iter()
.any(|server_name| !server_is_ours(server_name)) .any(|server_name| !server_is_ours(server_name))
{ {
info!("We couldn't do the join locally, maybe federation can help to satisfy the restricted join requirements"); info!(
"We couldn't do the join locally, maybe federation can help to satisfy the restricted join \
requirements"
);
let (make_join_response, remote_server) = make_join_request(sender_user, room_id, servers).await?; let (make_join_response, remote_server) = make_join_request(sender_user, room_id, servers).await?;
let room_version_id = match make_join_response.room_version { let room_version_id = match make_join_response.room_version {
@@ -1187,7 +1235,8 @@ async fn join_room_by_id_helper_local(
ruma::signatures::reference_hash(&join_event_stub, &room_version_id) ruma::signatures::reference_hash(&join_event_stub, &room_version_id)
.expect("ruma can calculate reference hashes") .expect("ruma can calculate reference hashes")
); );
let event_id = <&EventId>::try_from(event_id.as_str()).expect("ruma's reference hashes are valid event ids"); let event_id =
<&EventId>::try_from(event_id.as_str()).expect("ruma's reference hashes are valid event ids");
// Add event_id back // Add event_id back
join_event_stub.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.as_str().to_owned())); join_event_stub.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.as_str().to_owned()));
@@ -1209,7 +1258,8 @@ async fn join_room_by_id_helper_local(
.await?; .await?;
if let Some(signed_raw) = send_join_response.room_state.event { if let Some(signed_raw) = send_join_response.room_state.event {
let Ok((signed_event_id, signed_value)) = gen_event_id_canonical_json(&signed_raw, &room_version_id) else { let Ok((signed_event_id, signed_value)) = gen_event_id_canonical_json(&signed_raw, &room_version_id)
else {
// Event could not be converted to canonical json // Event could not be converted to canonical json
return Err(Error::BadRequest( return Err(Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
@@ -1242,6 +1292,7 @@ async fn join_room_by_id_helper_local(
} else { } else {
return Err(error); return Err(error);
} }
}
Ok(join_room_by_id::v3::Response::new(room_id.to_owned())) Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
} }
@@ -1319,7 +1370,7 @@ async fn make_join_request(
make_join_response_and_server make_join_response_and_server
} }
pub async fn validate_and_add_event_id( async fn validate_and_add_event_id(
pdu: &RawJsonValue, room_version: &RoomVersionId, pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>, pdu: &RawJsonValue, room_version: &RoomVersionId, pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>,
) -> Result<(OwnedEventId, CanonicalJsonObject)> { ) -> Result<(OwnedEventId, CanonicalJsonObject)> {
let mut value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| { let mut value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
@@ -1390,7 +1441,17 @@ pub(crate) async fn invite_helper(
if !user_is_local(user_id) { if !user_is_local(user_id) {
let (pdu, pdu_json, invite_room_state) = { let (pdu, pdu_json, invite_room_state) = {
let state_lock = services().globals.roomid_mutex_state.lock(room_id).await; let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(room_id.to_owned())
.or_default(),
);
let state_lock = mutex_state.lock().await;
let content = to_raw_value(&RoomMemberEventContent { let content = to_raw_value(&RoomMemberEventContent {
avatar_url: services().users.avatar_url(user_id)?, avatar_url: services().users.avatar_url(user_id)?,
displayname: None, displayname: None,
@@ -1502,7 +1563,16 @@ pub(crate) async fn invite_helper(
)); ));
} }
let state_lock = services().globals.roomid_mutex_state.lock(room_id).await; let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(room_id.to_owned())
.or_default(),
);
let state_lock = mutex_state.lock().await;
services() services()
.rooms .rooms
@@ -1536,9 +1606,8 @@ pub(crate) async fn invite_helper(
Ok(()) Ok(())
} }
// Make a user leave all their joined rooms, forgets all rooms, and ignores // Make a user leave all their joined rooms
// errors pub(crate) async fn leave_all_rooms(user_id: &UserId) -> Result<()> {
pub async fn leave_all_rooms(user_id: &UserId) {
let all_rooms = services() let all_rooms = services()
.rooms .rooms
.state_cache .state_cache
@@ -1558,16 +1627,13 @@ pub async fn leave_all_rooms(user_id: &UserId) {
}; };
// ignore errors // ignore errors
if let Err(e) = leave_room(user_id, &room_id, None).await { _ = leave_room(user_id, &room_id, None).await;
warn!(%room_id, %user_id, %e, "Failed to leave room");
}
if let Err(e) = services().rooms.state_cache.forget(&room_id, user_id) {
warn!(%room_id, %user_id, %e, "Failed to forget room");
}
}
} }
pub async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option<String>) -> Result<()> { Ok(())
}
pub(crate) async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option<String>) -> Result<()> {
// Ask a remote server if we don't have this room // Ask a remote server if we don't have this room
if !services() if !services()
.rooms .rooms
@@ -1596,7 +1662,16 @@ pub async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option<Strin
true, true,
)?; )?;
} else { } else {
let state_lock = services().globals.roomid_mutex_state.lock(room_id).await; let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(room_id.to_owned())
.or_default(),
);
let state_lock = mutex_state.lock().await;
let member_event = let member_event =
services() services()
@@ -1661,14 +1736,11 @@ async fn remote_leave_room(user_id: &UserId, room_id: &RoomId) -> Result<()> {
.invite_state(user_id, room_id)? .invite_state(user_id, room_id)?
.ok_or(Error::BadRequest(ErrorKind::BadState, "User is not invited."))?; .ok_or(Error::BadRequest(ErrorKind::BadState, "User is not invited."))?;
let mut servers: HashSet<OwnedServerName> = services() let servers: HashSet<OwnedServerName> = services()
.rooms .rooms
.state_cache .state_cache
.servers_invite_via(room_id) .servers_invite_via(room_id)?
.filter_map(Result::ok) .map_or(
.collect();
servers.extend(
invite_state invite_state
.iter() .iter()
.filter_map(|event| serde_json::from_str(event.json().get()).ok()) .filter_map(|event| serde_json::from_str(event.json().get()).ok())
@@ -1677,6 +1749,7 @@ async fn remote_leave_room(user_id: &UserId, room_id: &RoomId) -> Result<()> {
.filter_map(|sender| UserId::parse(sender).ok()) .filter_map(|sender| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()) .map(|user| user.server_name().to_owned())
.collect::<HashSet<OwnedServerName>>(), .collect::<HashSet<OwnedServerName>>(),
HashSet::from_iter,
); );
debug!("servers in remote_leave_room: {servers:?}"); debug!("servers in remote_leave_room: {servers:?}");
@@ -1,6 +1,8 @@
use std::collections::{BTreeMap, HashSet}; use std::{
collections::{BTreeMap, HashSet},
sync::Arc,
};
use conduit::PduCount;
use ruma::{ use ruma::{
api::client::{ api::client::{
error::ErrorKind, error::ErrorKind,
@@ -12,7 +14,10 @@ use ruma::{
}; };
use serde_json::{from_str, Value}; use serde_json::{from_str, Value};
use crate::{service::pdu::PduBuilder, services, utils, Error, PduEvent, Result, Ruma}; use crate::{
service::{pdu::PduBuilder, rooms::timeline::PduCount},
services, utils, Error, PduEvent, Result, Ruma,
};
/// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}` /// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`
/// ///
@@ -29,11 +34,16 @@ pub(crate) async fn send_message_event_route(
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_deref(); let sender_device = body.sender_device.as_deref();
let state_lock = services() let mutex_state = Arc::clone(
services()
.globals .globals
.roomid_mutex_state .roomid_mutex_state
.lock(&body.room_id) .write()
.await; .await
.entry(body.room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
// Forbid m.room.encrypted if encryption is disabled // Forbid m.room.encrypted if encryption is disabled
if MessageLikeEventType::RoomEncrypted == body.event_type && !services().globals.allow_encryption() { if MessageLikeEventType::RoomEncrypted == body.event_type && !services().globals.allow_encryption() {
@@ -107,7 +117,8 @@ pub(crate) async fn send_message_event_route(
/// Allows paginating through room history. /// Allows paginating through room history.
/// ///
/// - Only works if the user is joined (TODO: always allow, but only show events /// - Only works if the user is joined (TODO: always allow, but only show events
/// where the user was joined, depending on `history_visibility`) /// where the user was
/// joined, depending on `history_visibility`)
pub(crate) async fn get_message_events_route( pub(crate) async fn get_message_events_route(
body: Ruma<get_message_events::v3::Request>, body: Ruma<get_message_events::v3::Request>,
) -> Result<get_message_events::v3::Response> { ) -> Result<get_message_events::v3::Response> {
+83
View File
@@ -0,0 +1,83 @@
mod account;
mod alias;
mod backup;
mod capabilities;
mod config;
mod context;
mod device;
mod directory;
mod filter;
mod keys;
mod media;
mod membership;
mod message;
mod presence;
mod profile;
mod push;
mod read_marker;
mod redact;
mod relations;
mod report;
mod room;
mod search;
mod session;
mod space;
mod state;
mod sync;
mod tag;
mod thirdparty;
mod threads;
mod to_device;
mod typing;
mod unstable;
mod unversioned;
mod user_directory;
mod voip;
pub(crate) use account::*;
pub(crate) use alias::*;
pub(crate) use backup::*;
pub(crate) use capabilities::*;
pub(crate) use config::*;
pub(crate) use context::*;
pub(crate) use device::*;
pub(crate) use directory::*;
pub(crate) use filter::*;
pub(crate) use keys::*;
pub(crate) use media::*;
pub(crate) use membership::*;
pub(crate) use message::*;
pub(crate) use presence::*;
pub(crate) use profile::*;
pub(crate) use push::*;
pub(crate) use read_marker::*;
pub(crate) use redact::*;
pub(crate) use relations::*;
pub(crate) use report::*;
pub(crate) use room::*;
pub(crate) use search::*;
pub(crate) use session::*;
pub(crate) use space::*;
pub(crate) use state::*;
pub(crate) use sync::*;
pub(crate) use tag::*;
pub(crate) use thirdparty::*;
pub(crate) use threads::*;
pub(crate) use to_device::*;
pub(crate) use typing::*;
pub(crate) use unstable::*;
pub(crate) use unversioned::*;
pub(crate) use user_directory::*;
pub(crate) use voip::*;
/// generated device ID length
const DEVICE_ID_LENGTH: usize = 10;
/// generated user access token length
const TOKEN_LENGTH: usize = 32;
/// generated user session ID length
pub(crate) const SESSION_ID_LENGTH: usize = 32;
/// auto-generated password length
pub(crate) const AUTO_GEN_PASSWORD_LENGTH: usize = 25;
@@ -1,3 +1,5 @@
use std::sync::Arc;
use ruma::{ use ruma::{
api::{ api::{
client::{ client::{
@@ -8,15 +10,10 @@ use ruma::{
}, },
events::{room::member::RoomMemberEventContent, StateEventType, TimelineEventType}, events::{room::member::RoomMemberEventContent, StateEventType, TimelineEventType},
presence::PresenceState, presence::PresenceState,
OwnedMxcUri, OwnedRoomId, OwnedUserId,
}; };
use serde_json::value::to_raw_value; use serde_json::value::to_raw_value;
use tracing::warn;
use crate::{ use crate::{service::pdu::PduBuilder, services, utils::user_id::user_is_local, Error, Result, Ruma};
service::{pdu::PduBuilder, user_is_local},
services, Error, Result, Ruma,
};
/// # `PUT /_matrix/client/r0/profile/{userId}/displayname` /// # `PUT /_matrix/client/r0/profile/{userId}/displayname`
/// ///
@@ -27,14 +24,67 @@ pub(crate) async fn set_displayname_route(
body: Ruma<set_display_name::v3::Request>, body: Ruma<set_display_name::v3::Request>,
) -> Result<set_display_name::v3::Response> { ) -> Result<set_display_name::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let all_joined_rooms: Vec<OwnedRoomId> = services()
services()
.users
.set_displayname(sender_user, body.displayname.clone())
.await?;
// Send a new membership event and presence update into all joined rooms
let all_rooms_joined: Vec<_> = services()
.rooms .rooms
.state_cache .state_cache
.rooms_joined(sender_user) .rooms_joined(sender_user)
.filter_map(Result::ok) .filter_map(Result::ok)
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
displayname: body.displayname.clone(),
join_authorized_via_users_server: None,
..serde_json::from_str(
services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomMember, sender_user.as_str())?
.ok_or_else(|| {
Error::bad_database("Tried to send displayname 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(sender_user.to_string()),
redacts: None,
},
room_id,
))
})
.filter_map(Result::ok)
.collect(); .collect();
update_displayname(sender_user.clone(), body.displayname.clone(), all_joined_rooms).await?; for (pdu_builder, room_id) in all_rooms_joined {
let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
_ = services()
.rooms
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
.await;
}
if services().globals.allow_local_presence() { if services().globals.allow_local_presence() {
// Presence update // Presence update
@@ -50,8 +100,8 @@ pub(crate) async fn set_displayname_route(
/// ///
/// Returns the displayname of the user. /// Returns the displayname of the user.
/// ///
/// - If user is on another server and we do not have a local copy already fetch /// - If user is on another server and we do not have a local copy already
/// displayname over federation /// fetch displayname over federation
pub(crate) async fn get_displayname_route( pub(crate) async fn get_displayname_route(
body: Ruma<get_display_name::v3::Request>, body: Ruma<get_display_name::v3::Request>,
) -> Result<get_display_name::v3::Response> { ) -> Result<get_display_name::v3::Response> {
@@ -111,20 +161,72 @@ pub(crate) async fn set_avatar_url_route(
body: Ruma<set_avatar_url::v3::Request>, body: Ruma<set_avatar_url::v3::Request>,
) -> Result<set_avatar_url::v3::Response> { ) -> Result<set_avatar_url::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let all_joined_rooms: Vec<OwnedRoomId> = services()
services()
.users
.set_avatar_url(sender_user, body.avatar_url.clone())
.await?;
services()
.users
.set_blurhash(sender_user, body.blurhash.clone())
.await?;
// Send a new membership event and presence update into all joined rooms
let all_joined_rooms: Vec<_> = services()
.rooms .rooms
.state_cache .state_cache
.rooms_joined(sender_user) .rooms_joined(sender_user)
.filter_map(Result::ok) .filter_map(Result::ok)
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
avatar_url: body.avatar_url.clone(),
join_authorized_via_users_server: None,
..serde_json::from_str(
services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomMember, sender_user.as_str())?
.ok_or_else(|| {
Error::bad_database("Tried to send displayname 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(sender_user.to_string()),
redacts: None,
},
room_id,
))
})
.filter_map(Result::ok)
.collect(); .collect();
update_avatar_url( for (pdu_builder, room_id) in all_joined_rooms {
sender_user.clone(), let mutex_state = Arc::clone(
body.avatar_url.clone(), services()
body.blurhash.clone(), .globals
all_joined_rooms, .roomid_mutex_state
) .write()
.await?; .await
.entry(room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
_ = services()
.rooms
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
.await;
}
if services().globals.allow_local_presence() { if services().globals.allow_local_presence() {
// Presence update // Presence update
@@ -140,8 +242,8 @@ pub(crate) async fn set_avatar_url_route(
/// ///
/// Returns the `avatar_url` and `blurhash` of the user. /// Returns the `avatar_url` and `blurhash` of the user.
/// ///
/// - If user is on another server and we do not have a local copy already fetch /// - If user is on another server and we do not have a local copy already
/// `avatar_url` and blurhash over federation /// fetch `avatar_url` and blurhash over federation
pub(crate) async fn get_avatar_url_route( pub(crate) async fn get_avatar_url_route(
body: Ruma<get_avatar_url::v3::Request>, body: Ruma<get_avatar_url::v3::Request>,
) -> Result<get_avatar_url::v3::Response> { ) -> Result<get_avatar_url::v3::Response> {
@@ -251,116 +353,3 @@ pub(crate) async fn get_profile_route(body: Ruma<get_profile::v3::Request>) -> R
displayname: services().users.displayname(&body.user_id)?, displayname: services().users.displayname(&body.user_id)?,
}) })
} }
pub async fn update_displayname(
user_id: OwnedUserId, displayname: Option<String>, all_joined_rooms: Vec<OwnedRoomId>,
) -> Result<()> {
services()
.users
.set_displayname(&user_id, displayname.clone())
.await?;
// 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,
},
room_id,
))
})
.filter_map(Result::ok)
.collect();
update_all_rooms(all_joined_rooms, user_id).await;
Ok(())
}
pub async fn update_avatar_url(
user_id: OwnedUserId, avatar_url: Option<OwnedMxcUri>, blurhash: Option<String>, all_joined_rooms: Vec<OwnedRoomId>,
) -> Result<()> {
services()
.users
.set_avatar_url(&user_id, avatar_url.clone())
.await?;
services()
.users
.set_blurhash(&user_id, blurhash.clone())
.await?;
// 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 {
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,
},
room_id,
))
})
.filter_map(Result::ok)
.collect();
update_all_rooms(all_joined_rooms, user_id).await;
Ok(())
}
pub async fn update_all_rooms(all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>, user_id: OwnedUserId) {
for (pdu_builder, room_id) in all_joined_rooms {
let state_lock = services().globals.roomid_mutex_state.lock(room_id).await;
if let Err(e) = services()
.rooms
.timeline
.build_and_append_pdu(pdu_builder, &user_id, room_id, &state_lock)
.await
{
warn!(%user_id, %room_id, %e, "Failed to update/send new profile join membership update in room");
}
}
}
@@ -1,6 +1,5 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use conduit::PduCount;
use ruma::{ use ruma::{
api::client::{error::ErrorKind, read_marker::set_read_marker, receipt::create_receipt}, api::client::{error::ErrorKind, read_marker::set_read_marker, receipt::create_receipt},
events::{ events::{
@@ -10,7 +9,7 @@ use ruma::{
MilliSecondsSinceUnixEpoch, MilliSecondsSinceUnixEpoch,
}; };
use crate::{services, Error, Result, Ruma}; use crate::{service::rooms::timeline::PduCount, services, Error, Result, Ruma};
/// # `POST /_matrix/client/r0/rooms/{roomId}/read_markers` /// # `POST /_matrix/client/r0/rooms/{roomId}/read_markers`
/// ///
@@ -1,3 +1,5 @@
use std::sync::Arc;
use ruma::{ use ruma::{
api::client::redact::redact_event, api::client::redact::redact_event,
events::{room::redaction::RoomRedactionEventContent, TimelineEventType}, events::{room::redaction::RoomRedactionEventContent, TimelineEventType},
@@ -15,11 +17,16 @@ pub(crate) async fn redact_event_route(body: Ruma<redact_event::v3::Request>) ->
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let body = body.body; let body = body.body;
let state_lock = services() let mutex_state = Arc::clone(
services()
.globals .globals
.roomid_mutex_state .roomid_mutex_state
.lock(&body.room_id) .write()
.await; .await
.entry(body.room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
let event_id = services() let event_id = services()
.rooms .rooms
@@ -91,12 +91,12 @@ fn is_report_valid(
)); ));
} }
if !services() if services()
.rooms .rooms
.state_cache .state_cache
.room_members(&pdu.room_id) .room_members(&pdu.room_id)
.filter_map(Result::ok) .filter_map(Result::ok)
.any(|user_id| user_id == *sender_user) .any(|user_id| user_id != *sender_user)
{ {
return Err(Error::BadRequest( return Err(Error::BadRequest(
ErrorKind::NotFound, ErrorKind::NotFound,
@@ -104,14 +104,14 @@ fn is_report_valid(
)); ));
} }
if score.map(|s| s > int!(0) || s < int!(-100)) == Some(true) { if let Some(true) = score.map(|s| s > int!(0) || s < int!(-100)) {
return Err(Error::BadRequest( return Err(Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Invalid score, must be within 0 to -100", "Invalid score, must be within 0 to -100",
)); ));
}; };
if reason.clone().map(|s| s.len() >= 750) == Some(true) { if let Some(true) = reason.clone().map(|s| s.len() >= 750) {
return Err(Error::BadRequest( return Err(Error::BadRequest(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Reason too long, should be 750 characters or fewer", "Reason too long, should be 750 characters or fewer",
@@ -1,6 +1,5 @@
use std::{cmp::max, collections::BTreeMap}; use std::{cmp::max, collections::BTreeMap, sync::Arc};
use conduit::{debug_info, debug_warn};
use ruma::{ use ruma::{
api::client::{ api::client::{
error::ErrorKind, error::ErrorKind,
@@ -28,8 +27,9 @@ use ruma::{
use serde_json::{json, value::to_raw_value}; use serde_json::{json, value::to_raw_value};
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use super::invite_helper;
use crate::{ use crate::{
api::client_server::invite_helper,
debug_info, debug_warn,
service::{appservice::RegistrationInfo, pdu::PduBuilder}, service::{appservice::RegistrationInfo, pdu::PduBuilder},
services, Error, Result, Ruma, services, Error, Result, Ruma,
}; };
@@ -89,8 +89,18 @@ pub(crate) async fn create_room_route(body: Ruma<create_room::v3::Request>) -> R
)); ));
} }
let _short_id = services().rooms.short.get_or_create_shortroomid(&room_id)?; services().rooms.short.get_or_create_shortroomid(&room_id)?;
let state_lock = services().globals.roomid_mutex_state.lock(&room_id).await;
let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
let alias: Option<OwnedRoomAliasId> = if let Some(alias) = &body.room_alias_name { let alias: Option<OwnedRoomAliasId> = if let Some(alias) = &body.room_alias_name {
Some(room_alias_check(alias, &body.appservice_info).await?) Some(room_alias_check(alias, &body.appservice_info).await?)
@@ -378,7 +388,7 @@ pub(crate) async fn create_room_route(body: Ruma<create_room::v3::Request>) -> R
Error::BadRequest(ErrorKind::InvalidParam, "Invalid initial state event.") Error::BadRequest(ErrorKind::InvalidParam, "Invalid initial state event.")
})?; })?;
debug_info!("Room creation initial state event: {event:?}"); debug_warn!("initial state event: {event:?}");
// client/appservice workaround: if a user sends an initial_state event with a // client/appservice workaround: if a user sends an initial_state event with a
// state event in there with the content of literally `{}` (not null or empty // state event in there with the content of literally `{}` (not null or empty
@@ -450,17 +460,12 @@ pub(crate) async fn create_room_route(body: Ruma<create_room::v3::Request>) -> R
// 8. Events implied by invite (and TODO: invite_3pid) // 8. Events implied by invite (and TODO: invite_3pid)
drop(state_lock); drop(state_lock);
for user_id in &body.invite { for user_id in &body.invite {
if let Err(e) = invite_helper(sender_user, user_id, &room_id, None, body.is_direct).await { _ = invite_helper(sender_user, user_id, &room_id, None, body.is_direct).await;
warn!(%e, "Failed to send invite");
}
} }
// Homeserver specific stuff // Homeserver specific stuff
if let Some(alias) = alias { if let Some(alias) = alias {
services() services().rooms.alias.set_alias(&alias, &room_id)?;
.rooms
.alias
.set_alias(&alias, &room_id, sender_user)?;
} }
if body.visibility == room::Visibility::Public { if body.visibility == room::Visibility::Public {
@@ -567,17 +572,21 @@ pub(crate) async fn upgrade_room_route(body: Ruma<upgrade_room::v3::Request>) ->
// Create a replacement room // Create a replacement room
let replacement_room = RoomId::new(services().globals.server_name()); let replacement_room = RoomId::new(services().globals.server_name());
services()
let _short_id = services()
.rooms .rooms
.short .short
.get_or_create_shortroomid(&replacement_room)?; .get_or_create_shortroomid(&replacement_room)?;
let state_lock = services() let mutex_state = Arc::clone(
services()
.globals .globals
.roomid_mutex_state .roomid_mutex_state
.lock(&body.room_id) .write()
.await; .await
.entry(body.room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
// Send a m.room.tombstone event to the old room to indicate that it is not // 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 // intended to be used any further Fail if the sender does not have the required
@@ -605,11 +614,16 @@ pub(crate) async fn upgrade_room_route(body: Ruma<upgrade_room::v3::Request>) ->
// Change lock to replacement room // Change lock to replacement room
drop(state_lock); drop(state_lock);
let state_lock = services() let mutex_state = Arc::clone(
services()
.globals .globals
.roomid_mutex_state .roomid_mutex_state
.lock(&replacement_room) .write()
.await; .await
.entry(replacement_room.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
// Get the old room creation event // Get the old room creation event
let mut create_event_content = serde_json::from_str::<CanonicalJsonObject>( let mut create_event_content = serde_json::from_str::<CanonicalJsonObject>(
@@ -771,7 +785,7 @@ pub(crate) async fn upgrade_room_route(body: Ruma<upgrade_room::v3::Request>) ->
services() services()
.rooms .rooms
.alias .alias
.set_alias(&alias, &replacement_room, sender_user)?; .set_alias(&alias, &replacement_room)?;
} }
// Get the old room power levels // Get the old room power levels
@@ -801,7 +815,7 @@ pub(crate) async fn upgrade_room_route(body: Ruma<upgrade_room::v3::Request>) ->
// Modify the power levels in the old room to prevent sending of events and // Modify the power levels in the old room to prevent sending of events and
// inviting new users // inviting new users
services() _ = services()
.rooms .rooms
.timeline .timeline
.build_and_append_pdu( .build_and_append_pdu(
@@ -870,7 +884,7 @@ fn default_power_levels_content(
/// if a room is being created with a room alias, run our checks /// if a room is being created with a room alias, run our checks
async fn room_alias_check( async fn room_alias_check(
room_alias_name: &str, appservice_info: &Option<RegistrationInfo>, room_alias_name: &String, appservice_info: &Option<RegistrationInfo>,
) -> Result<OwnedRoomAliasId> { ) -> Result<OwnedRoomAliasId> {
// Basic checks on the room alias validity // Basic checks on the room alias validity
if room_alias_name.contains(':') { if room_alias_name.contains(':') {
@@ -884,6 +898,20 @@ async fn room_alias_check(
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Room alias contained spaces which is not a valid room alias.", "Room alias contained spaces which is not a valid room alias.",
)); ));
} else if room_alias_name.len() > 255 {
// there is nothing spec-wise saying to check the limit of this,
// however absurdly long room aliases are guaranteed to be unreadable or done
// maliciously. there is no reason a room alias should even exceed 100
// characters as is. generally in spec, 255 is matrix's fav number
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Room alias is excessively long, clients may not be able to handle this. Please shorten it.",
));
} else if room_alias_name.contains('"') {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Room alias contained `\"` which is not allowed.",
));
} }
// check if room alias is forbidden // check if room alias is forbidden
@@ -928,7 +956,7 @@ async fn room_alias_check(
} }
/// if a room is being created with a custom room ID, run our checks against it /// if a room is being created with a custom room ID, run our checks against it
fn custom_room_id_check(custom_room_id: &str) -> Result<OwnedRoomId> { fn custom_room_id_check(custom_room_id: &String) -> Result<OwnedRoomId> {
// apply forbidden room alias checks to custom room IDs too // apply forbidden room alias checks to custom room IDs too
if services() if services()
.globals .globals
@@ -949,6 +977,8 @@ fn custom_room_id_check(custom_room_id: &str) -> Result<OwnedRoomId> {
ErrorKind::InvalidParam, ErrorKind::InvalidParam,
"Custom room ID contained spaces which is not valid.", "Custom room ID contained spaces which is not valid.",
)); ));
} else if custom_room_id.len() > 255 {
return Err(Error::BadRequest(ErrorKind::InvalidParam, "Custom room ID is too long."));
} }
let full_room_id = format!("!{}:{}", custom_room_id, services().globals.config.server_name); let full_room_id = format!("!{}:{}", custom_room_id, services().globals.config.server_name);
@@ -133,7 +133,6 @@ pub(crate) async fn search_events_route(body: Ruma<search_events::v3::Request>)
let results: Vec<_> = results let results: Vec<_> = results
.iter() .iter()
.skip(skip)
.filter_map(|result| { .filter_map(|result| {
services() services()
.rooms .rooms
@@ -141,8 +140,7 @@ pub(crate) async fn search_events_route(body: Ruma<search_events::v3::Request>)
.get_pdu_from_id(result) .get_pdu_from_id(result)
.ok()? .ok()?
.filter(|pdu| { .filter(|pdu| {
!pdu.is_redacted() services()
&& services()
.rooms .rooms
.state_accessor .state_accessor
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id) .user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id)
@@ -164,11 +162,15 @@ pub(crate) async fn search_events_route(body: Ruma<search_events::v3::Request>)
}) })
}) })
.filter_map(Result::ok) .filter_map(Result::ok)
.skip(skip)
.take(limit) .take(limit)
.collect(); .collect();
let more_unloaded_results = searches.iter_mut().any(|s| s.peek().is_some()); let next_batch = if results.len() < limit {
let next_batch = more_unloaded_results.then(|| next_batch.to_string()); None
} else {
Some(next_batch.to_string())
};
Ok(search_events::v3::Response::new(ResultCategories { Ok(search_events::v3::Response::new(ResultCategories {
room_events: ResultRoomEvents { room_events: ResultRoomEvents {
@@ -1,3 +1,4 @@
use argon2::{PasswordHash, PasswordVerifier};
use ruma::{ use ruma::{
api::client::{ api::client::{
error::ErrorKind, error::ErrorKind,
@@ -17,10 +18,10 @@ use ruma::{
UserId, UserId,
}; };
use serde::Deserialize; use serde::Deserialize;
use tracing::{debug, info, warn}; use tracing::{debug, error, info, warn};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH}; use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::{services, utils, utils::hash, Error, Result, Ruma}; use crate::{services, utils, Error, Result, Ruma};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct Claims { struct Claims {
@@ -75,7 +76,14 @@ pub(crate) async fn login_route(body: Ruma<login::v3::Request>) -> Result<login:
warn!("Bad login type: {:?}", &body.login_info); warn!("Bad login type: {:?}", &body.login_info);
return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type.")); return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
} }
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?; .map_err(|e| {
warn!("Failed to parse username from user logging in: {e}");
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
})?;
if services().appservice.is_exclusive_user_id(&user_id).await {
return Err(Error::BadRequest(ErrorKind::Exclusive, "User ID reserved by appservice."));
}
let hash = services() let hash = services()
.users .users
@@ -86,7 +94,18 @@ pub(crate) async fn login_route(body: Ruma<login::v3::Request>) -> Result<login:
return Err(Error::BadRequest(ErrorKind::UserDeactivated, "The user has been deactivated")); return Err(Error::BadRequest(ErrorKind::UserDeactivated, "The user has been deactivated"));
} }
if hash::verify_password(password, &hash).is_err() { let Ok(parsed_hash) = PasswordHash::new(&hash) else {
error!("error while hashing user {}", user_id);
return Err(Error::BadServerResponse("could not hash"));
};
let hash_matches = services()
.globals
.argon
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok();
if !hash_matches {
return Err(Error::BadRequest(ErrorKind::forbidden(), "Wrong username or password.")); return Err(Error::BadRequest(ErrorKind::forbidden(), "Wrong username or password."));
} }
@@ -106,10 +125,17 @@ pub(crate) async fn login_route(body: Ruma<login::v3::Request>) -> Result<login:
let username = token.claims.sub.to_lowercase(); let username = token.claims.sub.to_lowercase();
let user_id =
UserId::parse_with_server_name(username, services().globals.server_name()).map_err(|e| { UserId::parse_with_server_name(username, services().globals.server_name()).map_err(|e| {
warn!("Failed to parse username from user logging in: {e}"); warn!("Failed to parse username from user logging in: {e}");
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.") Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
})? })?;
if services().appservice.is_exclusive_user_id(&user_id).await {
return Err(Error::BadRequest(ErrorKind::Exclusive, "User ID reserved by appservice."));
}
user_id
} else { } else {
return Err(Error::BadRequest( return Err(Error::BadRequest(
ErrorKind::Unknown, ErrorKind::Unknown,
@@ -19,8 +19,10 @@ use ruma::{
use tracing::{error, log::warn}; use tracing::{error, log::warn};
use crate::{ use crate::{
service::{pdu::PduBuilder, server_is_ours}, service::{self, pdu::PduBuilder},
services, Error, Result, Ruma, RumaResponse, services,
utils::server_name::server_is_ours,
Error, Result, Ruma, RumaResponse,
}; };
/// # `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}/{stateKey}` /// # `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}/{stateKey}`
@@ -172,7 +174,18 @@ async fn send_state_event_for_key_helper(
sender: &UserId, room_id: &RoomId, event_type: &StateEventType, json: &Raw<AnyStateEventContent>, state_key: String, sender: &UserId, room_id: &RoomId, event_type: &StateEventType, json: &Raw<AnyStateEventContent>, state_key: String,
) -> Result<Arc<EventId>> { ) -> Result<Arc<EventId>> {
allowed_to_send_state_event(room_id, event_type, json).await?; allowed_to_send_state_event(room_id, event_type, json).await?;
let state_lock = services().globals.roomid_mutex_state.lock(room_id).await;
let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(room_id.to_owned())
.or_default(),
);
let state_lock = mutex_state.lock().await;
let event_id = services() let event_id = services()
.rooms .rooms
.timeline .timeline
@@ -205,7 +218,7 @@ async fn allowed_to_send_state_event(
}, },
// admin room is a sensitive room, it should not ever be made public // admin room is a sensitive room, it should not ever be made public
StateEventType::RoomJoinRules => { StateEventType::RoomJoinRules => {
if let Some(admin_room_id) = service::admin::Service::get_admin_room()? { if let Some(admin_room_id) = service::admin::Service::get_admin_room().await? {
if admin_room_id == room_id { if admin_room_id == room_id {
if let Ok(join_rule) = serde_json::from_str::<RoomJoinRulesEventContent>(json.json().get()) { if let Ok(join_rule) = serde_json::from_str::<RoomJoinRulesEventContent>(json.json().get()) {
if join_rule.join_rule == JoinRule::Public { if join_rule.join_rule == JoinRule::Public {
@@ -220,7 +233,7 @@ async fn allowed_to_send_state_event(
}, },
// admin room is a sensitive room, it should not ever be made world readable // admin room is a sensitive room, it should not ever be made world readable
StateEventType::RoomHistoryVisibility => { StateEventType::RoomHistoryVisibility => {
if let Some(admin_room_id) = service::admin::Service::get_admin_room()? { if let Some(admin_room_id) = service::admin::Service::get_admin_room().await? {
if admin_room_id == room_id { if admin_room_id == room_id {
if let Ok(visibility_content) = if let Ok(visibility_content) =
serde_json::from_str::<RoomHistoryVisibilityEventContent>(json.json().get()) serde_json::from_str::<RoomHistoryVisibilityEventContent>(json.json().get())
@@ -1,10 +1,10 @@
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap, HashSet}, collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap, HashSet},
sync::Arc,
time::Duration, time::Duration,
}; };
use conduit::PduCount;
use ruma::{ use ruma::{
api::client::{ api::client::{
filter::{FilterDefinition, LazyLoadOptions}, filter::{FilterDefinition, LazyLoadOptions},
@@ -25,18 +25,23 @@ use ruma::{
StateEventType, TimelineEventType, StateEventType, TimelineEventType,
}, },
serde::Raw, serde::Raw,
uint, DeviceId, EventId, OwnedUserId, RoomId, UInt, UserId, uint, DeviceId, EventId, OwnedDeviceId, OwnedUserId, RoomId, UInt, UserId,
}; };
use tracing::{error, Instrument as _, Span}; use tokio::sync::watch::Sender;
use tracing::{debug, error, Instrument as _, Span};
use crate::{service::pdu::EventHash, services, utils, Error, PduEvent, Result, Ruma, RumaResponse}; use crate::{
service::{pdu::EventHash, rooms::timeline::PduCount},
services, utils, Error, PduEvent, Result, Ruma, RumaResponse,
};
/// # `GET /_matrix/client/r0/sync` /// # `GET /_matrix/client/r0/sync`
/// ///
/// Synchronize the client's state with the latest state on the server. /// Synchronize the client's state with the latest state on the server.
/// ///
/// - This endpoint takes a `since` parameter which should be the `next_batch` /// - This endpoint takes a `since` parameter which should be the `next_batch`
/// value from a previous request for incremental syncs. /// value from a
/// previous request for incremental syncs.
/// ///
/// Calling this endpoint without a `since` parameter returns: /// Calling this endpoint without a `since` parameter returns:
/// - Some of the most recent events of each timeline /// - Some of the most recent events of each timeline
@@ -48,9 +53,11 @@ use crate::{service::pdu::EventHash, services, utils, Error, PduEvent, Result, R
/// returns: For joined rooms: /// returns: For joined rooms:
/// - Some of the most recent events of each timeline that happened after since /// - Some of the most recent events of each timeline that happened after since
/// - If user joined the room after since: All state events (unless lazy loading /// - If user joined the room after since: All state events (unless lazy loading
/// is activated) and all device list updates in that room /// is activated) and
/// all device list updates in that room
/// - If the user was already in the room: A list of all events that are in the /// - If the user was already in the room: A list of all events that are in the
/// state now, but were not in the state at `since` /// state now, but were
/// not in the state at `since`
/// - If the state we send contains a member event: Joined and invited member /// - If the state we send contains a member event: Joined and invited member
/// counts, heroes /// counts, heroes
/// - Device list updates that happened after `since` /// - Device list updates that happened after `since`
@@ -66,6 +73,10 @@ use crate::{service::pdu::EventHash, services, utils, Error, PduEvent, Result, R
/// For left rooms: /// For left rooms:
/// - If the user left after `since`: `prev_batch` token, empty state (TODO: /// - If the user left after `since`: `prev_batch` token, empty state (TODO:
/// subset of the state at the point of the leave) /// subset of the state at the point of the leave)
///
/// - Sync is handled in an async task, multiple requests from the same device
/// with the same
/// `since` will be cached
pub(crate) async fn sync_events_route( pub(crate) async fn sync_events_route(
body: Ruma<sync_events::v3::Request>, body: Ruma<sync_events::v3::Request>,
) -> Result<sync_events::v3::Response, RumaResponse<UiaaResponse>> { ) -> Result<sync_events::v3::Response, RumaResponse<UiaaResponse>> {
@@ -73,6 +84,95 @@ pub(crate) async fn sync_events_route(
let sender_device = body.sender_device.expect("user is authenticated"); let sender_device = body.sender_device.expect("user is authenticated");
let body = body.body; let body = body.body;
let mut rx = match services()
.globals
.sync_receivers
.write()
.await
.entry((sender_user.clone(), sender_device.clone()))
{
Entry::Vacant(v) => {
let (tx, rx) = tokio::sync::watch::channel(None);
v.insert((body.since.clone(), rx.clone()));
tokio::spawn(sync_helper_wrapper(sender_user.clone(), sender_device.clone(), body, tx));
rx
},
Entry::Occupied(mut o) => {
if o.get().0 != body.since {
let (tx, rx) = tokio::sync::watch::channel(None);
o.insert((body.since.clone(), rx.clone()));
debug!("Sync started for {sender_user}");
tokio::spawn(sync_helper_wrapper(sender_user.clone(), sender_device.clone(), body, tx));
rx
} else {
o.get().1.clone()
}
},
};
let we_have_to_wait = rx.borrow().is_none();
if we_have_to_wait {
if let Err(e) = rx.changed().await {
error!("Error waiting for sync: {}", e);
}
}
let result = match rx
.borrow()
.as_ref()
.expect("When sync channel changes it's always set to some")
{
Ok(response) => Ok(response.clone()),
Err(error) => Err(error.to_response()),
};
result
}
async fn sync_helper_wrapper(
sender_user: OwnedUserId, sender_device: OwnedDeviceId, body: sync_events::v3::Request,
tx: Sender<Option<Result<sync_events::v3::Response>>>,
) {
let since = body.since.clone();
let r = sync_helper(sender_user.clone(), sender_device.clone(), body).await;
if let Ok((_, caching_allowed)) = r {
if !caching_allowed {
match services()
.globals
.sync_receivers
.write()
.await
.entry((sender_user, sender_device))
{
Entry::Occupied(o) => {
// Only remove if the device didn't start a different /sync already
if o.get().0 == since {
o.remove();
}
},
Entry::Vacant(_) => {},
}
}
}
_ = tx.send(Some(r.map(|(r, _)| r)));
}
async fn sync_helper(
sender_user: OwnedUserId,
sender_device: OwnedDeviceId,
body: sync_events::v3::Request,
// bool = caching allowed
) -> Result<(sync_events::v3::Response, bool), Error> {
// Presence update // Presence update
if services().globals.allow_local_presence() { if services().globals.allow_local_presence() {
services() services()
@@ -138,7 +238,7 @@ pub(crate) async fn sync_events_route(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Coalesce database writes for the remainder of this scope. // Coalesce database writes for the remainder of this scope.
let _cork = services().globals.db.cork_and_flush(); let _cork = services().globals.db.cork_and_flush()?;
for room_id in all_joined_rooms { for room_id in all_joined_rooms {
let room_id = room_id?; let room_id = room_id?;
@@ -193,9 +293,20 @@ pub(crate) async fn sync_events_route(
for result in all_invited_rooms { for result in all_invited_rooms {
let (room_id, invite_state_events) = result?; let (room_id, invite_state_events) = result?;
{
// Get and drop the lock to wait for remaining operations to finish // Get and drop the lock to wait for remaining operations to finish
let insert_lock = services().globals.roomid_mutex_insert.lock(&room_id).await; let mutex_insert = Arc::clone(
services()
.globals
.roomid_mutex_insert
.write()
.await
.entry(room_id.clone())
.or_default(),
);
let insert_lock = mutex_insert.lock().await;
drop(insert_lock); drop(insert_lock);
};
let invite_count = services() let invite_count = services()
.rooms .rooms
@@ -302,24 +413,32 @@ pub(crate) async fn sync_events_route(
if duration.as_secs() > 30 { if duration.as_secs() > 30 {
duration = Duration::from_secs(30); duration = Duration::from_secs(30);
} }
#[allow(clippy::let_underscore_must_use)]
{
_ = tokio::time::timeout(duration, watcher).await; _ = tokio::time::timeout(duration, watcher).await;
Ok((response, false))
} else {
Ok((response, since != next_batch)) // Only cache if we made progress
} }
} }
Ok(response) #[tracing::instrument(skip_all, fields(user_id = %sender_user, room_id = %room_id))]
}
#[tracing::instrument(skip_all, fields(user_id = %sender_user, room_id = %room_id), name = "left_room")]
async fn handle_left_room( async fn handle_left_room(
since: u64, room_id: &RoomId, sender_user: &UserId, left_rooms: &mut BTreeMap<ruma::OwnedRoomId, LeftRoom>, since: u64, room_id: &RoomId, sender_user: &UserId, left_rooms: &mut BTreeMap<ruma::OwnedRoomId, LeftRoom>,
next_batch_string: &str, full_state: bool, lazy_load_enabled: bool, next_batch_string: &str, full_state: bool, lazy_load_enabled: bool,
) -> Result<()> { ) -> Result<()> {
{
// Get and drop the lock to wait for remaining operations to finish // Get and drop the lock to wait for remaining operations to finish
let insert_lock = services().globals.roomid_mutex_insert.lock(room_id).await; let mutex_insert = Arc::clone(
services()
.globals
.roomid_mutex_insert
.write()
.await
.entry(room_id.to_owned())
.or_default(),
);
let insert_lock = mutex_insert.lock().await;
drop(insert_lock); drop(insert_lock);
};
let left_count = services() let left_count = services()
.rooms .rooms
@@ -466,10 +585,8 @@ async fn handle_left_room(
} }
async fn process_presence_updates( async fn process_presence_updates(
presence_updates: &mut HashMap<OwnedUserId, PresenceEvent>, since: u64, syncing_user: &UserId, presence_updates: &mut HashMap<OwnedUserId, PresenceEvent>, since: u64, syncing_user: &OwnedUserId,
) -> Result<()> { ) -> Result<()> {
use crate::service::presence::Presence;
// Take presence updates // Take presence updates
for (user_id, _, presence_bytes) in services().presence.presence_since(since) { for (user_id, _, presence_bytes) in services().presence.presence_since(since) {
if !services() if !services()
@@ -480,6 +597,7 @@ async fn process_presence_updates(
continue; continue;
} }
use crate::service::presence::Presence;
let presence_event = Presence::from_json_bytes_to_event(&presence_bytes, &user_id)?; let presence_event = Presence::from_json_bytes_to_event(&presence_bytes, &user_id)?;
match presence_updates.entry(user_id) { match presence_updates.entry(user_id) {
Entry::Vacant(slot) => { Entry::Vacant(slot) => {
@@ -518,10 +636,21 @@ async fn load_joined_room(
next_batch: u64, next_batchcount: PduCount, lazy_load_enabled: bool, lazy_load_send_redundant: bool, next_batch: u64, next_batchcount: PduCount, lazy_load_enabled: bool, lazy_load_send_redundant: bool,
full_state: bool, device_list_updates: &mut HashSet<OwnedUserId>, left_encrypted_users: &mut HashSet<OwnedUserId>, full_state: bool, device_list_updates: &mut HashSet<OwnedUserId>, left_encrypted_users: &mut HashSet<OwnedUserId>,
) -> Result<JoinedRoom> { ) -> Result<JoinedRoom> {
{
// Get and drop the lock to wait for remaining operations to finish // Get and drop the lock to wait for remaining operations to finish
// This will make sure the we have all events until next_batch // This will make sure the we have all events until next_batch
let insert_lock = services().globals.roomid_mutex_insert.lock(room_id).await; let mutex_insert = Arc::clone(
services()
.globals
.roomid_mutex_insert
.write()
.await
.entry(room_id.to_owned())
.or_default(),
);
let insert_lock = mutex_insert.lock().await;
drop(insert_lock); drop(insert_lock);
};
let (timeline_pdus, limited) = load_timeline(sender_user, room_id, sincecount, 10)?; let (timeline_pdus, limited) = load_timeline(sender_user, room_id, sincecount, 10)?;
@@ -574,7 +703,7 @@ async fn load_joined_room(
.unwrap_or(0); .unwrap_or(0);
// Recalculate heroes (first 5 members) // Recalculate heroes (first 5 members)
let mut heroes: Vec<OwnedUserId> = Vec::with_capacity(5); let mut heroes = Vec::new();
if joined_member_count.saturating_add(invited_member_count) <= 5 { if joined_member_count.saturating_add(invited_member_count) <= 5 {
// Go through all PDUs and for each member event, check if the user is still // Go through all PDUs and for each member event, check if the user is still
@@ -599,7 +728,7 @@ async fn load_joined_room(
&& (services().rooms.state_cache.is_joined(&user_id, room_id)? && (services().rooms.state_cache.is_joined(&user_id, room_id)?
|| services().rooms.state_cache.is_invited(&user_id, room_id)?) || services().rooms.state_cache.is_invited(&user_id, room_id)?)
{ {
Ok::<_, Error>(Some(user_id)) Ok::<_, Error>(Some(state_key.clone()))
} else { } else {
Ok(None) Ok(None)
} }
@@ -607,11 +736,12 @@ async fn load_joined_room(
Ok(None) Ok(None)
} }
}) })
// Filter out buggy users
.filter_map(Result::ok) .filter_map(Result::ok)
// Filter for possible heroes // Filter for possible heroes
.flatten() .flatten()
{ {
if heroes.contains(&hero) || hero == sender_user { if heroes.contains(&hero) || hero == sender_user.as_str() {
continue; continue;
} }
@@ -715,7 +845,8 @@ async fn load_joined_room(
// Incremental /sync // Incremental /sync
let since_shortstatehash = since_shortstatehash.unwrap(); let since_shortstatehash = since_shortstatehash.unwrap();
let mut delta_state_events = Vec::new(); let mut state_events = Vec::new();
let mut lazy_loaded = HashSet::new();
if since_shortstatehash != current_shortstatehash { if since_shortstatehash != current_shortstatehash {
let current_state_ids = services() let current_state_ids = services()
@@ -736,12 +867,55 @@ async fn load_joined_room(
continue; continue;
}; };
delta_state_events.push(pdu); if pdu.kind == TimelineEventType::RoomMember {
match UserId::parse(
pdu.state_key
.as_ref()
.expect("State event has state key")
.clone(),
) {
Ok(state_key_userid) => {
lazy_loaded.insert(state_key_userid);
},
Err(e) => error!("Invalid state key for member event: {}", e),
}
}
state_events.push(pdu);
tokio::task::yield_now().await; tokio::task::yield_now().await;
} }
} }
} }
for (_, event) in &timeline_pdus {
if lazy_loaded.contains(&event.sender) {
continue;
}
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
sender_user,
sender_device,
room_id,
&event.sender,
)? || lazy_load_send_redundant
{
if let Some(member_event) = services().rooms.state_accessor.room_state_get(
room_id,
&StateEventType::RoomMember,
event.sender.as_str(),
)? {
lazy_loaded.insert(event.sender.clone());
state_events.push(member_event);
}
}
}
services()
.rooms
.lazy_loading
.lazy_load_mark_sent(sender_user, sender_device, room_id, lazy_loaded, next_batchcount)
.await;
let encrypted_room = services() let encrypted_room = services()
.rooms .rooms
.state_accessor .state_accessor
@@ -757,12 +931,12 @@ async fn load_joined_room(
// Calculations: // Calculations:
let new_encrypted_room = encrypted_room && since_encryption.is_none(); let new_encrypted_room = encrypted_room && since_encryption.is_none();
let send_member_count = delta_state_events let send_member_count = state_events
.iter() .iter()
.any(|event| event.kind == TimelineEventType::RoomMember); .any(|event| event.kind == TimelineEventType::RoomMember);
if encrypted_room { if encrypted_room {
for state_event in &delta_state_events { for state_event in &state_events {
if state_event.kind != TimelineEventType::RoomMember { if state_event.kind != TimelineEventType::RoomMember {
continue; continue;
} }
@@ -823,57 +997,6 @@ async fn load_joined_room(
(None, None, Vec::new()) (None, None, Vec::new())
}; };
let mut state_events = delta_state_events;
let mut lazy_loaded = HashSet::new();
// Mark all member events we're returning as lazy-loaded
for pdu in &state_events {
if pdu.kind == TimelineEventType::RoomMember {
match UserId::parse(
pdu.state_key
.as_ref()
.expect("State event has state key")
.clone(),
) {
Ok(state_key_userid) => {
lazy_loaded.insert(state_key_userid);
},
Err(e) => error!("Invalid state key for member event: {}", e),
}
}
}
// Fetch contextual member state events for events from the timeline, and
// mark them as lazy-loaded as well.
for (_, event) in &timeline_pdus {
if lazy_loaded.contains(&event.sender) {
continue;
}
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
sender_user,
sender_device,
room_id,
&event.sender,
)? || lazy_load_send_redundant
{
if let Some(member_event) = services().rooms.state_accessor.room_state_get(
room_id,
&StateEventType::RoomMember,
event.sender.as_str(),
)? {
lazy_loaded.insert(event.sender.clone());
state_events.push(member_event);
}
}
}
services()
.rooms
.lazy_loading
.lazy_load_mark_sent(sender_user, sender_device, room_id, lazy_loaded, next_batchcount)
.await;
( (
heroes, heroes,
joined_member_count, joined_member_count,
@@ -1004,7 +1127,8 @@ fn load_timeline(
sender_user: &UserId, room_id: &RoomId, roomsincecount: PduCount, limit: u64, sender_user: &UserId, room_id: &RoomId, roomsincecount: PduCount, limit: u64,
) -> Result<(Vec<(PduCount, PduEvent)>, bool), Error> { ) -> Result<(Vec<(PduCount, PduEvent)>, bool), Error> {
let timeline_pdus; let timeline_pdus;
let limited = if services() let limited;
if services()
.rooms .rooms
.timeline .timeline
.last_timeline_count(sender_user, room_id)? .last_timeline_count(sender_user, room_id)?
@@ -1034,11 +1158,11 @@ fn load_timeline(
// They /sync response doesn't always return all messages, so we say the output // They /sync response doesn't always return all messages, so we say the output
// is limited unless there are events in non_timeline_pdus // is limited unless there are events in non_timeline_pdus
non_timeline_pdus.next().is_some() limited = non_timeline_pdus.next().is_some();
} else { } else {
timeline_pdus = Vec::new(); timeline_pdus = Vec::new();
false limited = false;
}; }
Ok((timeline_pdus, limited)) Ok((timeline_pdus, limited))
} }
@@ -1546,7 +1670,6 @@ pub(crate) async fn sync_events_v4_route(
), ),
num_live: None, // Count events in timeline greater than global sync counter num_live: None, // Count events in timeline greater than global sync counter
timestamp: None, timestamp: None,
heroes: None,
}, },
); );
} }
@@ -1561,11 +1684,8 @@ pub(crate) async fn sync_events_v4_route(
if duration.as_secs() > 30 { if duration.as_secs() > 30 {
duration = Duration::from_secs(30); duration = Duration::from_secs(30);
} }
#[allow(clippy::let_underscore_must_use)]
{
_ = tokio::time::timeout(duration, watcher).await; _ = tokio::time::timeout(duration, watcher).await;
} }
}
Ok(sync_events::v4::Response { Ok(sync_events::v4::Response {
initial: globalsince == 0, initial: globalsince == 0,

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