mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-06 04:31:37 +00:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 99b44bbf09 | |||
| 95aeff8cdc | |||
| 9e62e66ae4 | |||
| 76b93e252d | |||
| 66d479e2eb | |||
| 241371463e | |||
| d970df5fd2 | |||
| 4e644961f3 | |||
| 35cf9af5c8 | |||
| 04e796176a | |||
| 9783940105 | |||
| 1e430f9470 | |||
| 5cce024841 | |||
| e87c461b8d | |||
| b934898f51 | |||
| 83e3de55a4 | |||
| 609e239436 | |||
| 34417c96ae | |||
| f33f281edb | |||
| ddbca59193 | |||
| b5a2e49ae4 | |||
| 37248a4f68 | |||
| dd22325ea2 | |||
| 30a56d5cb9 | |||
| 3183210459 | |||
| 57d7743037 | |||
| cb09bfa4e7 | |||
| 0ed691edef | |||
| c58b9f05ed | |||
| fb7e739b72 | |||
| c7adbae03f | |||
| 8b35de6a43 | |||
| d191494f18 | |||
| 6d1f12b22d | |||
| ca3ee9224b | |||
| 427b973b67 | |||
| aacaf5a2a0 | |||
| 256bed992e | |||
| ecb87ccd1c | |||
| 14a4b24fc5 | |||
| 731761f0fc | |||
| 4524a00fc6 | |||
| 9db750e97c | |||
| b14a4d470b | |||
| 5d1f141882 | |||
| b447cfff56 | |||
| 283888e788 | |||
| f54e59a068 | |||
| 2a183cc5a4 | |||
| 54acd07555 | |||
| 583cb924f1 | |||
| 9286838d23 | |||
| d1ebcfaf0b | |||
| e820551f62 | |||
| bd3db65cb2 | |||
| e4a43b1a5b | |||
| 5775e0ad9d | |||
| 238cc627e3 | |||
| b1516209c4 | |||
| 0589884109 | |||
| 4a83df5b57 | |||
| aa08edc55f | |||
| 00c7e220bb | |||
| 87be4d1a52 | |||
| 205506f206 | |||
| 66181c61af | |||
| b7a0442298 |
@@ -26,3 +26,7 @@ max_line_length = 98
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[*.json]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
dotenv_if_exists
|
||||
|
||||
# use flake ".#${DIRENV_DEVSHELL:-default}"
|
||||
if [ -f /etc/os-release ] && grep -q '^ID=nixos' /etc/os-release; then
|
||||
use flake ".#${DIRENV_DEVSHELL:-default}"
|
||||
fi
|
||||
|
||||
PATH_add bin
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
name: detect-runner-os
|
||||
description: |
|
||||
Detect the actual OS name and version of the runner.
|
||||
Provides separate outputs for name, version, and a combined slug.
|
||||
|
||||
outputs:
|
||||
name:
|
||||
description: 'OS name (e.g. Ubuntu, Debian)'
|
||||
value: ${{ steps.detect.outputs.name }}
|
||||
version:
|
||||
description: 'OS version (e.g. 22.04, 11)'
|
||||
value: ${{ steps.detect.outputs.version }}
|
||||
slug:
|
||||
description: 'Combined OS slug (e.g. Ubuntu-22.04)'
|
||||
value: ${{ steps.detect.outputs.slug }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Detect runner OS
|
||||
id: detect
|
||||
shell: bash
|
||||
run: |
|
||||
# Detect OS version (try lsb_release first, fall back to /etc/os-release)
|
||||
OS_VERSION=$(lsb_release -rs 2>/dev/null || grep VERSION_ID /etc/os-release | cut -d'"' -f2)
|
||||
|
||||
# Detect OS name and capitalise (try lsb_release first, fall back to /etc/os-release)
|
||||
OS_NAME=$(lsb_release -is 2>/dev/null || grep "^ID=" /etc/os-release | cut -d'=' -f2 | tr -d '"' | sed 's/\b\(.\)/\u\1/g')
|
||||
|
||||
# Create combined slug
|
||||
OS_SLUG="${OS_NAME}-${OS_VERSION}"
|
||||
|
||||
# Set outputs
|
||||
echo "name=${OS_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "version=${OS_VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "slug=${OS_SLUG}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Log detection results
|
||||
echo "🔍 Detected Runner OS: ${OS_NAME} ${OS_VERSION}"
|
||||
@@ -1,27 +0,0 @@
|
||||
name: prefligit
|
||||
description: |
|
||||
Runs prefligit, pre-commit reimplemented in Rust.
|
||||
inputs:
|
||||
extra_args:
|
||||
description: options to pass to pre-commit run
|
||||
required: false
|
||||
default: '--all-files'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install uv
|
||||
uses: https://github.com/astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
ignore-nothing-to-cache: true
|
||||
- name: Install Prefligit
|
||||
shell: bash
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/j178/prefligit/releases/download/v0.0.10/prefligit-installer.sh | sh
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/prefligit
|
||||
key: prefligit-0|${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- run: prefligit run --show-diff-on-failure --color=always -v ${{ inputs.extra_args }}
|
||||
shell: bash
|
||||
@@ -0,0 +1,167 @@
|
||||
name: setup-llvm-with-apt
|
||||
description: |
|
||||
Set up LLVM toolchain with APT package management and smart caching.
|
||||
Supports cross-compilation architectures and additional package installation.
|
||||
|
||||
Creates symlinks in /usr/bin: clang, clang++, lld, llvm-ar, llvm-ranlib
|
||||
|
||||
inputs:
|
||||
dpkg-arch:
|
||||
description: 'Debian architecture for cross-compilation (e.g. arm64)'
|
||||
required: false
|
||||
default: ''
|
||||
extra-packages:
|
||||
description: 'Additional APT packages to install (space-separated)'
|
||||
required: false
|
||||
default: ''
|
||||
llvm-version:
|
||||
description: 'LLVM version to install'
|
||||
required: false
|
||||
default: '20'
|
||||
|
||||
outputs:
|
||||
llvm-version:
|
||||
description: 'Installed LLVM version'
|
||||
value: ${{ steps.configure.outputs.version }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Detect runner OS
|
||||
id: runner-os
|
||||
uses: ./.forgejo/actions/detect-runner-os
|
||||
|
||||
- name: Configure cross-compilation architecture
|
||||
if: inputs.dpkg-arch != ''
|
||||
shell: bash
|
||||
run: |
|
||||
echo "🏗️ Adding ${{ inputs.dpkg-arch }} architecture"
|
||||
sudo dpkg --add-architecture ${{ inputs.dpkg-arch }}
|
||||
|
||||
# Restrict default sources to amd64
|
||||
sudo sed -i 's/^deb http/deb [arch=amd64] http/g' /etc/apt/sources.list
|
||||
sudo sed -i 's/^deb https/deb [arch=amd64] https/g' /etc/apt/sources.list
|
||||
|
||||
# Add ports sources for foreign architecture
|
||||
sudo tee /etc/apt/sources.list.d/${{ inputs.dpkg-arch }}.list > /dev/null <<EOF
|
||||
deb [arch=${{ inputs.dpkg-arch }}] http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted universe multiverse
|
||||
deb [arch=${{ inputs.dpkg-arch }}] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted universe multiverse
|
||||
deb [arch=${{ inputs.dpkg-arch }}] http://ports.ubuntu.com/ubuntu-ports/ jammy-security main restricted universe multiverse
|
||||
EOF
|
||||
|
||||
echo "✅ Architecture ${{ inputs.dpkg-arch }} configured"
|
||||
|
||||
- name: Start LLVM cache group
|
||||
shell: bash
|
||||
run: echo "::group::📦 Restoring LLVM cache"
|
||||
|
||||
- name: Check for LLVM cache
|
||||
id: cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/usr/bin/clang-*
|
||||
/usr/bin/clang++-*
|
||||
/usr/bin/lld-*
|
||||
/usr/bin/llvm-*
|
||||
/usr/lib/llvm-*/
|
||||
/usr/lib/x86_64-linux-gnu/libLLVM*.so*
|
||||
/usr/lib/x86_64-linux-gnu/libclang*.so*
|
||||
/etc/apt/sources.list.d/archive_uri-*
|
||||
/etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
||||
key: llvm-${{ steps.runner-os.outputs.slug }}-v${{ inputs.llvm-version }}-v3-${{ hashFiles('**/Cargo.lock', 'rust-toolchain.toml') }}
|
||||
|
||||
- name: End LLVM cache group
|
||||
shell: bash
|
||||
run: echo "::endgroup::"
|
||||
|
||||
- name: Check and install LLVM if needed
|
||||
id: llvm-setup
|
||||
shell: bash
|
||||
run: |
|
||||
echo "🔍 Checking for LLVM ${{ inputs.llvm-version }}..."
|
||||
|
||||
# Check both binaries and libraries exist
|
||||
if [ -f "/usr/bin/clang-${{ inputs.llvm-version }}" ] && \
|
||||
[ -f "/usr/bin/clang++-${{ inputs.llvm-version }}" ] && \
|
||||
[ -f "/usr/bin/lld-${{ inputs.llvm-version }}" ] && \
|
||||
([ -f "/usr/lib/x86_64-linux-gnu/libLLVM.so.${{ inputs.llvm-version }}.1" ] || \
|
||||
[ -f "/usr/lib/x86_64-linux-gnu/libLLVM-${{ inputs.llvm-version }}.so.1" ] || \
|
||||
[ -f "/usr/lib/llvm-${{ inputs.llvm-version }}/lib/libLLVM.so" ]); then
|
||||
echo "✅ LLVM ${{ inputs.llvm-version }} found and verified"
|
||||
echo "needs-install=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "📦 LLVM ${{ inputs.llvm-version }} not found or incomplete - installing..."
|
||||
|
||||
echo "::group::🔧 Installing LLVM ${{ inputs.llvm-version }}"
|
||||
wget -O - https://apt.llvm.org/llvm.sh | bash -s -- ${{ inputs.llvm-version }}
|
||||
echo "::endgroup::"
|
||||
|
||||
if [ ! -f "/usr/bin/clang-${{ inputs.llvm-version }}" ]; then
|
||||
echo "❌ Failed to install LLVM ${{ inputs.llvm-version }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Installed LLVM ${{ inputs.llvm-version }}"
|
||||
echo "needs-install=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Prepare for additional packages
|
||||
if: inputs.extra-packages != ''
|
||||
shell: bash
|
||||
run: |
|
||||
# Update APT if LLVM was cached (installer script already does apt-get update)
|
||||
if [[ "${{ steps.llvm-setup.outputs.needs-install }}" != "true" ]]; then
|
||||
echo "::group::📦 Running apt-get update (LLVM cached, extra packages needed)"
|
||||
sudo apt-get update
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
echo "::group::📦 Installing additional packages"
|
||||
|
||||
- name: Install additional packages
|
||||
if: inputs.extra-packages != ''
|
||||
uses: https://github.com/awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: ${{ inputs.extra-packages }}
|
||||
version: 1.0
|
||||
|
||||
- name: End package installation group
|
||||
if: inputs.extra-packages != ''
|
||||
shell: bash
|
||||
run: echo "::endgroup::"
|
||||
|
||||
- name: Configure LLVM environment
|
||||
id: configure
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::group::🔧 Configuring LLVM ${{ inputs.llvm-version }} environment"
|
||||
|
||||
# Create symlinks
|
||||
sudo ln -sf "/usr/bin/clang-${{ inputs.llvm-version }}" /usr/bin/clang
|
||||
sudo ln -sf "/usr/bin/clang++-${{ inputs.llvm-version }}" /usr/bin/clang++
|
||||
sudo ln -sf "/usr/bin/lld-${{ inputs.llvm-version }}" /usr/bin/lld
|
||||
sudo ln -sf "/usr/bin/llvm-ar-${{ inputs.llvm-version }}" /usr/bin/llvm-ar
|
||||
sudo ln -sf "/usr/bin/llvm-ranlib-${{ inputs.llvm-version }}" /usr/bin/llvm-ranlib
|
||||
echo " ✓ Created symlinks"
|
||||
|
||||
# Setup library paths
|
||||
LLVM_LIB_PATH="/usr/lib/llvm-${{ inputs.llvm-version }}/lib"
|
||||
if [ -d "$LLVM_LIB_PATH" ]; then
|
||||
echo "LD_LIBRARY_PATH=${LLVM_LIB_PATH}:${LD_LIBRARY_PATH:-}" >> $GITHUB_ENV
|
||||
echo "LIBCLANG_PATH=${LLVM_LIB_PATH}" >> $GITHUB_ENV
|
||||
|
||||
echo "$LLVM_LIB_PATH" | sudo tee "/etc/ld.so.conf.d/llvm-${{ inputs.llvm-version }}.conf" > /dev/null
|
||||
sudo ldconfig
|
||||
echo " ✓ Configured library paths"
|
||||
else
|
||||
# Fallback to standard library location
|
||||
if [ -d "/usr/lib/x86_64-linux-gnu" ]; then
|
||||
echo "LIBCLANG_PATH=/usr/lib/x86_64-linux-gnu" >> $GITHUB_ENV
|
||||
echo " ✓ Using fallback library path"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set output
|
||||
echo "version=${{ inputs.llvm-version }}" >> $GITHUB_OUTPUT
|
||||
echo "::endgroup::"
|
||||
echo "✅ LLVM ready: $(clang --version | head -1)"
|
||||
@@ -0,0 +1,236 @@
|
||||
name: setup-rust
|
||||
description: |
|
||||
Set up Rust toolchain with sccache for compilation caching.
|
||||
Respects rust-toolchain.toml by default or accepts explicit version override.
|
||||
|
||||
inputs:
|
||||
cache-key-suffix:
|
||||
description: 'Optional suffix for cache keys (e.g. platform identifier)'
|
||||
required: false
|
||||
default: ''
|
||||
rust-components:
|
||||
description: 'Additional Rust components to install (space-separated)'
|
||||
required: false
|
||||
default: ''
|
||||
rust-target:
|
||||
description: 'Rust target triple (e.g. x86_64-unknown-linux-gnu)'
|
||||
required: false
|
||||
default: ''
|
||||
rust-version:
|
||||
description: 'Rust version to install (e.g. nightly). Defaults to 1.87.0'
|
||||
required: false
|
||||
default: '1.87.0'
|
||||
sccache-cache-limit:
|
||||
description: 'Maximum size limit for sccache local cache (e.g. 2G, 500M)'
|
||||
required: false
|
||||
default: '2G'
|
||||
github-token:
|
||||
description: 'GitHub token for downloading sccache from GitHub releases'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
outputs:
|
||||
rust-version:
|
||||
description: 'Installed Rust version'
|
||||
value: ${{ steps.rust-setup.outputs.version }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Detect runner OS
|
||||
id: runner-os
|
||||
uses: ./.forgejo/actions/detect-runner-os
|
||||
|
||||
- name: Configure Cargo environment
|
||||
shell: bash
|
||||
run: |
|
||||
# Use workspace-relative paths for better control and consistency
|
||||
echo "CARGO_HOME=${{ github.workspace }}/.cargo" >> $GITHUB_ENV
|
||||
echo "CARGO_TARGET_DIR=${{ github.workspace }}/target" >> $GITHUB_ENV
|
||||
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> $GITHUB_ENV
|
||||
echo "RUSTUP_HOME=${{ github.workspace }}/.rustup" >> $GITHUB_ENV
|
||||
|
||||
# Limit binstall resolution timeout to avoid GitHub rate limit delays
|
||||
echo "BINSTALL_MAXIMUM_RESOLUTION_TIMEOUT=10" >> $GITHUB_ENV
|
||||
|
||||
# Ensure directories exist for first run
|
||||
mkdir -p "${{ github.workspace }}/.cargo"
|
||||
mkdir -p "${{ github.workspace }}/.sccache"
|
||||
mkdir -p "${{ github.workspace }}/target"
|
||||
mkdir -p "${{ github.workspace }}/.rustup"
|
||||
|
||||
- name: Start cache restore group
|
||||
shell: bash
|
||||
run: echo "::group::📦 Restoring caches (registry, toolchain, build artifacts)"
|
||||
|
||||
- name: Cache Cargo registry and git
|
||||
id: registry-cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cargo/registry/index
|
||||
.cargo/registry/cache
|
||||
.cargo/git/db
|
||||
# Registry cache saved per workflow, restored from any workflow's cache
|
||||
# Each workflow maintains its own registry that accumulates its needed crates
|
||||
key: cargo-registry-${{ steps.runner-os.outputs.slug }}-${{ github.workflow }}
|
||||
restore-keys: |
|
||||
cargo-registry-${{ steps.runner-os.outputs.slug }}-
|
||||
|
||||
- name: Cache toolchain binaries
|
||||
id: toolchain-cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cargo/bin
|
||||
.rustup/toolchains
|
||||
.rustup/update-hashes
|
||||
# Shared toolchain cache across all Rust versions
|
||||
key: toolchain-${{ steps.runner-os.outputs.slug }}
|
||||
|
||||
- name: Debug GitHub token availability
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -z "${{ inputs.github-token }}" ]; then
|
||||
echo "⚠️ No GitHub token provided - sccache will use fallback download method"
|
||||
else
|
||||
echo "✅ GitHub token provided for sccache"
|
||||
fi
|
||||
|
||||
- name: Setup sccache
|
||||
uses: https://github.com/mozilla-actions/sccache-action@v0.0.9
|
||||
with:
|
||||
token: ${{ inputs.github-token }}
|
||||
|
||||
- name: Cache build artifacts
|
||||
id: build-cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
target/**/deps
|
||||
!target/**/deps/*.rlib
|
||||
target/**/build
|
||||
target/**/.fingerprint
|
||||
target/**/incremental
|
||||
target/**/*.d
|
||||
/timelord/
|
||||
# Build artifacts - cache per code change, restore from deps when code changes
|
||||
key: >-
|
||||
build-${{ steps.runner-os.outputs.slug }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}-${{ hashFiles('**/*.rs', '**/Cargo.toml') }}
|
||||
restore-keys: |
|
||||
build-${{ steps.runner-os.outputs.slug }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}-
|
||||
|
||||
- name: End cache restore group
|
||||
shell: bash
|
||||
run: echo "::endgroup::"
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
shell: bash
|
||||
run: |
|
||||
# Install rustup if not already cached
|
||||
if ! command -v rustup &> /dev/null; then
|
||||
echo "::group::📦 Installing rustup"
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain none
|
||||
source "$CARGO_HOME/env"
|
||||
echo "::endgroup::"
|
||||
else
|
||||
echo "✅ rustup already available"
|
||||
fi
|
||||
|
||||
# Setup the appropriate Rust version
|
||||
if [[ -n "${{ inputs.rust-version }}" ]]; then
|
||||
echo "::group::📦 Setting up Rust ${{ inputs.rust-version }}"
|
||||
# Set override first to prevent rust-toolchain.toml from auto-installing
|
||||
rustup override set ${{ inputs.rust-version }} 2>/dev/null || true
|
||||
|
||||
# Check if we need to install/update the toolchain
|
||||
if rustup toolchain list | grep -q "^${{ inputs.rust-version }}-"; then
|
||||
rustup update ${{ inputs.rust-version }}
|
||||
else
|
||||
rustup toolchain install ${{ inputs.rust-version }} --profile minimal -c cargo,clippy,rustfmt
|
||||
fi
|
||||
else
|
||||
echo "::group::📦 Setting up Rust from rust-toolchain.toml"
|
||||
rustup show
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Configure PATH and install tools
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ inputs.github-token }}
|
||||
run: |
|
||||
# Add .cargo/bin to PATH permanently for all subsequent steps
|
||||
echo "${{ github.workspace }}/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
# For this step only, we need to add it to PATH since GITHUB_PATH takes effect in the next step
|
||||
export PATH="${{ github.workspace }}/.cargo/bin:$PATH"
|
||||
|
||||
# Install cargo-binstall for fast binary installations
|
||||
if command -v cargo-binstall &> /dev/null; then
|
||||
echo "✅ cargo-binstall already available"
|
||||
else
|
||||
echo "::group::📦 Installing cargo-binstall"
|
||||
curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
|
||||
if command -v prek &> /dev/null; then
|
||||
echo "✅ prek already available"
|
||||
else
|
||||
echo "::group::📦 Installing prek"
|
||||
# prek isn't regularly published to crates.io, so we use git source
|
||||
cargo-binstall -y --no-symlinks --git https://github.com/j178/prek prek
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
|
||||
if command -v timelord &> /dev/null; then
|
||||
echo "✅ timelord already available"
|
||||
else
|
||||
echo "::group::📦 Installing timelord"
|
||||
cargo-binstall -y --no-symlinks timelord-cli
|
||||
echo "::endgroup::"
|
||||
fi
|
||||
|
||||
- name: Configure sccache environment
|
||||
shell: bash
|
||||
run: |
|
||||
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
|
||||
echo "CMAKE_C_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV
|
||||
echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV
|
||||
echo "CMAKE_CUDA_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV
|
||||
echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV
|
||||
|
||||
# Configure incremental compilation GC
|
||||
# If we restored from old cache (partial hit), clean up aggressively
|
||||
if [[ "${{ steps.build-cache.outputs.cache-hit }}" != "true" ]]; then
|
||||
echo "♻️ Partial cache hit - enabling cache cleanup"
|
||||
echo "CARGO_INCREMENTAL_GC_THRESHOLD=5" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Install Rust components
|
||||
if: inputs.rust-components != ''
|
||||
shell: bash
|
||||
run: |
|
||||
echo "📦 Installing components: ${{ inputs.rust-components }}"
|
||||
rustup component add ${{ inputs.rust-components }}
|
||||
|
||||
- name: Install Rust target
|
||||
if: inputs.rust-target != ''
|
||||
shell: bash
|
||||
run: |
|
||||
echo "📦 Installing target: ${{ inputs.rust-target }}"
|
||||
rustup target add ${{ inputs.rust-target }}
|
||||
|
||||
- name: Output version and summary
|
||||
id: rust-setup
|
||||
shell: bash
|
||||
run: |
|
||||
RUST_VERSION=$(rustc --version | cut -d' ' -f2)
|
||||
echo "version=$RUST_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "📋 Setup complete:"
|
||||
echo " Rust: $(rustc --version)"
|
||||
echo " Cargo: $(cargo --version)"
|
||||
echo " prek: $(prek --version 2>/dev/null || echo 'installed')"
|
||||
echo " timelord: $(timelord --version 2>/dev/null || echo 'installed')"
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Checks / Prefligit
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prefligit:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
FROM_REF: ${{ github.event.pull_request.base.sha || (!github.event.forced && ( github.event.before != '0000000000000000000000000000000000000000' && github.event.before || github.sha )) || format('{0}~', github.sha) }}
|
||||
TO_REF: ${{ github.sha }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: ./.forgejo/actions/prefligit
|
||||
with:
|
||||
extra_args: --all-files --hook-stage manual
|
||||
@@ -0,0 +1,79 @@
|
||||
name: Checks / Prek
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
fast-checks:
|
||||
name: Pre-commit & Formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Rust nightly
|
||||
uses: ./.forgejo/actions/setup-rust
|
||||
with:
|
||||
rust-version: nightly
|
||||
github-token: ${{ secrets.GH_PUBLIC_RO }}
|
||||
|
||||
- name: Run prek
|
||||
run: |
|
||||
prek run \
|
||||
--all-files \
|
||||
--hook-stage manual \
|
||||
--show-diff-on-failure \
|
||||
--color=always \
|
||||
-v
|
||||
|
||||
- name: Check Rust formatting
|
||||
run: |
|
||||
cargo +nightly fmt --all -- --check && \
|
||||
echo "✅ Formatting check passed" || \
|
||||
exit 1
|
||||
|
||||
clippy-and-tests:
|
||||
name: Clippy and Cargo Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup LLVM
|
||||
uses: ./.forgejo/actions/setup-llvm-with-apt
|
||||
with:
|
||||
extra-packages: liburing-dev liburing2
|
||||
|
||||
- name: Setup Rust with caching
|
||||
uses: ./.forgejo/actions/setup-rust
|
||||
with:
|
||||
github-token: ${{ secrets.GH_PUBLIC_RO }}
|
||||
|
||||
- name: Run Clippy lints
|
||||
run: |
|
||||
cargo clippy \
|
||||
--workspace \
|
||||
--features full \
|
||||
--locked \
|
||||
--no-deps \
|
||||
--profile test \
|
||||
-- \
|
||||
-D warnings
|
||||
|
||||
- name: Run Cargo tests
|
||||
run: |
|
||||
cargo test \
|
||||
--workspace \
|
||||
--features full \
|
||||
--locked \
|
||||
--profile test \
|
||||
--all-targets \
|
||||
--no-fail-fast
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
var-lib-apt-${{ matrix.slug }}
|
||||
key: var-lib-apt-${{ matrix.slug }}
|
||||
- name: inject cache into docker
|
||||
uses: https://github.com/reproducible-containers/buildkit-cache-dance@v3.1.0
|
||||
uses: https://github.com/reproducible-containers/buildkit-cache-dance@v3.3.0
|
||||
with:
|
||||
cache-map: |
|
||||
{
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Maintenance / Renovate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 5am UTC daily to avoid late-night dev
|
||||
- cron: '0 5 * * *'
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dryRun:
|
||||
description: 'Dry run mode'
|
||||
required: false
|
||||
default: null
|
||||
type: choice
|
||||
options:
|
||||
- null
|
||||
- 'extract'
|
||||
- 'lookup'
|
||||
- 'full'
|
||||
logLevel:
|
||||
description: 'Log level'
|
||||
required: false
|
||||
default: 'info'
|
||||
type: choice
|
||||
options:
|
||||
- 'info'
|
||||
- 'warning'
|
||||
- 'critical'
|
||||
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
# Re-run when config changes
|
||||
- '.forgejo/workflows/renovate.yml'
|
||||
- 'renovate.json'
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
name: Renovate
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/renovatebot/renovate:41
|
||||
options: --tmpfs /tmp:exec
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: print node heap
|
||||
run: /usr/local/renovate/node -e 'console.log(`node heap limit = ${require("v8").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'
|
||||
|
||||
- name: Restore renovate repo cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/tmp/renovate/cache/renovate/repository
|
||||
key: repo-cache-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
repo-cache-
|
||||
|
||||
- name: Restore renovate package cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/tmp/renovate/cache/renovate/renovate-cache-sqlite
|
||||
key: package-cache-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
package-cache-
|
||||
|
||||
- name: Self-hosted Renovate
|
||||
uses: https://github.com/renovatebot/github-action@v43.0.9
|
||||
env:
|
||||
LOG_LEVEL: ${{ inputs.logLevel || 'info' }}
|
||||
RENOVATE_DRY_RUN: ${{ inputs.dryRun || 'false' }}
|
||||
|
||||
RENOVATE_PLATFORM: forgejo
|
||||
RENOVATE_ENDPOINT: ${{ github.server_url }}
|
||||
RENOVATE_AUTODISCOVER: 'false'
|
||||
RENOVATE_REPOSITORIES: '["${{ github.repository }}"]'
|
||||
|
||||
RENOVATE_GIT_TIMEOUT: 60000
|
||||
|
||||
RENOVATE_REQUIRE_CONFIG: 'required'
|
||||
RENOVATE_ONBOARDING: 'false'
|
||||
|
||||
RENOVATE_PR_COMMITS_PER_RUN_LIMIT: 3
|
||||
|
||||
RENOVATE_GITHUB_TOKEN_WARN: 'false'
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
GITHUB_COM_TOKEN: ${{ secrets.GH_PUBLIC_RO }}
|
||||
|
||||
RENOVATE_REPOSITORY_CACHE: 'enabled'
|
||||
RENOVATE_X_SQLITE_PACKAGE_CACHE: true
|
||||
|
||||
- name: Save renovate repo cache
|
||||
if: always() && env.RENOVATE_DRY_RUN != 'full'
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/tmp/renovate/cache/renovate/repository
|
||||
key: repo-cache-${{ github.run_id }}
|
||||
|
||||
- name: Save renovate package cache
|
||||
if: always() && env.RENOVATE_DRY_RUN != 'full'
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/tmp/renovate/cache/renovate/renovate-cache-sqlite
|
||||
key: package-cache-${{ github.run_id }}
|
||||
@@ -1,144 +0,0 @@
|
||||
name: Checks / Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
format:
|
||||
name: Format
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install rust
|
||||
uses: ./.forgejo/actions/rust-toolchain
|
||||
with:
|
||||
toolchain: "nightly"
|
||||
components: "rustfmt"
|
||||
|
||||
- name: Check formatting
|
||||
run: |
|
||||
cargo +nightly fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install rust
|
||||
uses: ./.forgejo/actions/rust-toolchain
|
||||
|
||||
- uses: https://github.com/actions/create-github-app-token@v2
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
github-api-url: https://api.github.com
|
||||
owner: ${{ vars.GH_APP_OWNER }}
|
||||
repositories: ""
|
||||
- name: Install sccache
|
||||
uses: ./.forgejo/actions/sccache
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
- run: sudo apt-get update
|
||||
- name: Install system dependencies
|
||||
uses: https://github.com/awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: clang liburing-dev
|
||||
version: 1
|
||||
- name: Cache Rust registry
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/git
|
||||
!~/.cargo/git/checkouts
|
||||
~/.cargo/registry
|
||||
!~/.cargo/registry/src
|
||||
key: rust-registry-${{hashFiles('**/Cargo.lock') }}
|
||||
- name: Timelord
|
||||
uses: ./.forgejo/actions/timelord
|
||||
with:
|
||||
key: sccache-v0
|
||||
path: .
|
||||
- name: Clippy
|
||||
run: |
|
||||
cargo clippy \
|
||||
--workspace \
|
||||
--all-features \
|
||||
--locked \
|
||||
--no-deps \
|
||||
--profile test \
|
||||
-- \
|
||||
-D warnings
|
||||
|
||||
- name: Show sccache stats
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
|
||||
cargo-test:
|
||||
name: Cargo Test
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install rust
|
||||
uses: ./.forgejo/actions/rust-toolchain
|
||||
|
||||
- uses: https://github.com/actions/create-github-app-token@v2
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
github-api-url: https://api.github.com
|
||||
owner: ${{ vars.GH_APP_OWNER }}
|
||||
repositories: ""
|
||||
- name: Install sccache
|
||||
uses: ./.forgejo/actions/sccache
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
- run: sudo apt-get update
|
||||
- name: Install system dependencies
|
||||
uses: https://github.com/awalsh128/cache-apt-pkgs-action@v1
|
||||
with:
|
||||
packages: clang liburing-dev
|
||||
version: 1
|
||||
- name: Cache Rust registry
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/git
|
||||
!~/.cargo/git/checkouts
|
||||
~/.cargo/registry
|
||||
!~/.cargo/registry/src
|
||||
key: rust-registry-${{hashFiles('**/Cargo.lock') }}
|
||||
- name: Timelord
|
||||
uses: ./.forgejo/actions/timelord
|
||||
with:
|
||||
key: sccache-v0
|
||||
path: .
|
||||
- name: Cargo Test
|
||||
run: |
|
||||
cargo test \
|
||||
--workspace \
|
||||
--all-features \
|
||||
--locked \
|
||||
--profile test \
|
||||
--all-targets \
|
||||
--no-fail-fast
|
||||
|
||||
- name: Show sccache stats
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
+2
-3
@@ -1,5 +1,4 @@
|
||||
github: [JadedBlueEyes]
|
||||
# Doesn't support an array, so we can only list nex
|
||||
ko_fi: nexy7574
|
||||
github: [JadedBlueEyes, nexy7574]
|
||||
custom:
|
||||
- https://ko-fi.com/nexy7574
|
||||
- https://ko-fi.com/JadedBlueEyes
|
||||
|
||||
@@ -9,7 +9,7 @@ repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-byte-order-marker
|
||||
- id: fix-byte-order-marker
|
||||
- id: check-case-conflict
|
||||
- id: check-symlinks
|
||||
- id: destroyed-symlinks
|
||||
|
||||
+3
-3
@@ -65,11 +65,11 @@ # Run tests
|
||||
cargo test
|
||||
|
||||
# Check compilation
|
||||
cargo check --workspace --all-features
|
||||
cargo check --workspace --features full
|
||||
|
||||
# Run lints
|
||||
cargo clippy --workspace --all-features
|
||||
# Auto-fix: cargo clippy --workspace --all-features --fix --allow-staged;
|
||||
cargo clippy --workspace --features full
|
||||
# Auto-fix: cargo clippy --workspace --features full --fix --allow-staged;
|
||||
|
||||
# Format code (must use nightly)
|
||||
cargo +nightly fmt
|
||||
|
||||
Generated
+488
-207
File diff suppressed because it is too large
Load Diff
+12
-4
@@ -21,7 +21,7 @@ license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||
rust-version = "1.86.0"
|
||||
version = "0.5.0-rc.6"
|
||||
version = "0.5.0-rc.7"
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "conduwuit"
|
||||
@@ -352,7 +352,7 @@ version = "0.1.2"
|
||||
[workspace.dependencies.ruma]
|
||||
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
||||
#branch = "conduwuit-changes"
|
||||
rev = "b753738047d1f443aca870896ef27ecaacf027da"
|
||||
rev = "8fb268fa2771dfc3a1c8075ef1246e7c9a0a53fd"
|
||||
features = [
|
||||
"compat",
|
||||
"rand",
|
||||
@@ -391,7 +391,7 @@ features = [
|
||||
|
||||
[workspace.dependencies.rust-rocksdb]
|
||||
git = "https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1"
|
||||
rev = "fc9a99ac54a54208f90fdcba33ae6ee8bc3531dd"
|
||||
rev = "99b0319416b64830dd6f8943e1f65e15aeef18bc"
|
||||
default-features = false
|
||||
features = [
|
||||
"multi-threaded-cf",
|
||||
@@ -546,6 +546,11 @@ features = ["std"]
|
||||
[workspace.dependencies.maplit]
|
||||
version = "1.0.2"
|
||||
|
||||
[workspace.dependencies.ldap3]
|
||||
version = "0.11.5"
|
||||
default-features = false
|
||||
features = ["sync", "tls-rustls"]
|
||||
|
||||
#
|
||||
# Patches
|
||||
#
|
||||
@@ -867,7 +872,7 @@ unused-qualifications = "warn"
|
||||
#unused-results = "warn" # TODO
|
||||
|
||||
## some sadness
|
||||
elided_named_lifetimes = "allow" # TODO!
|
||||
mismatched_lifetime_syntaxes = "allow" # TODO!
|
||||
let_underscore_drop = "allow"
|
||||
missing_docs = "allow"
|
||||
# cfgs cannot be limited to expected cfgs or their de facto non-transitive/opt-in use-case e.g.
|
||||
@@ -1006,3 +1011,6 @@ literal_string_with_formatting_args = { level = "allow", priority = 1 }
|
||||
|
||||
|
||||
needless_raw_string_hashes = "allow"
|
||||
|
||||
# TODO: Enable this lint & fix all instances
|
||||
collapsible_if = "allow"
|
||||
|
||||
@@ -57,7 +57,7 @@ ### What are the project's goals?
|
||||
|
||||
### Can I try it out?
|
||||
|
||||
Check out the [documentation](introduction) for installation instructions.
|
||||
Check out the [documentation](https://continuwuity.org) for installation instructions.
|
||||
|
||||
There are currently no open registration Continuwuity instances available.
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ StandardError=journal+console
|
||||
|
||||
Environment="CONTINUWUITY_LOG_TO_JOURNALD=true"
|
||||
Environment="CONTINUWUITY_JOURNALD_IDENTIFIER=%N"
|
||||
Environment="CONTINUWUITY_DATABASE_PATH=/var/lib/conduwuit"
|
||||
|
||||
TTYReset=yes
|
||||
# uncomment to allow buffer to be cleared every restart
|
||||
@@ -64,7 +65,7 @@ StateDirectory=conduwuit
|
||||
RuntimeDirectory=conduwuit
|
||||
RuntimeDirectoryMode=0750
|
||||
|
||||
Environment=CONTINUWUITY_CONFIG=${CREDENTIALS_DIRECTORY}/config.toml
|
||||
Environment=CONTINUWUITY_CONFIG=%d/config.toml
|
||||
LoadCredential=config.toml:/etc/conduwuit/conduwuit.toml
|
||||
BindPaths=/var/lib/private/conduwuit:/var/lib/matrix-conduit
|
||||
BindPaths=/var/lib/private/conduwuit:/var/lib/private/matrix-conduit
|
||||
|
||||
+96
-2
@@ -79,9 +79,11 @@
|
||||
# This is the only directory where continuwuity will save its data,
|
||||
# including media. Note: this was previously "/var/lib/matrix-conduit".
|
||||
#
|
||||
# YOU NEED TO EDIT THIS.
|
||||
# YOU NEED TO EDIT THIS, UNLESS you are running continuwuity as a `systemd` service.
|
||||
# The service file sets it to `/var/lib/conduwuit` using an environment variable
|
||||
# and also grants write access.
|
||||
#
|
||||
# example: "/var/lib/continuwuity"
|
||||
# example: "/var/lib/conduwuit"
|
||||
#
|
||||
#database_path =
|
||||
|
||||
@@ -1696,6 +1698,10 @@
|
||||
#
|
||||
#config_reload_signal = true
|
||||
|
||||
# This item is undocumented. Please contribute documentation for it.
|
||||
#
|
||||
#ldap = false
|
||||
|
||||
[global.tls]
|
||||
|
||||
# Path to a valid TLS certificate file.
|
||||
@@ -1774,3 +1780,91 @@
|
||||
# is 33.55MB. Setting it to 0 disables blurhashing.
|
||||
#
|
||||
#blurhash_max_raw_size = 33554432
|
||||
|
||||
[global.ldap]
|
||||
|
||||
# Whether to enable LDAP login.
|
||||
#
|
||||
# example: "true"
|
||||
#
|
||||
#enable = false
|
||||
|
||||
# Whether to force LDAP authentication or authorize classical password
|
||||
# login.
|
||||
#
|
||||
# example: "true"
|
||||
#
|
||||
#ldap_only = false
|
||||
|
||||
# URI of the LDAP server.
|
||||
#
|
||||
# example: "ldap://ldap.example.com:389"
|
||||
#
|
||||
#uri = ""
|
||||
|
||||
# Root of the searches.
|
||||
#
|
||||
# example: "ou=users,dc=example,dc=org"
|
||||
#
|
||||
#base_dn = ""
|
||||
|
||||
# Bind DN if anonymous search is not enabled.
|
||||
#
|
||||
# You can use the variable `{username}` that will be replaced by the
|
||||
# entered username. In such case, the password used to bind will be the
|
||||
# one provided for the login and not the one given by
|
||||
# `bind_password_file`. Beware: automatically granting admin rights will
|
||||
# not work if you use this direct bind instead of a LDAP search.
|
||||
#
|
||||
# example: "cn=ldap-reader,dc=example,dc=org" or
|
||||
# "cn={username},ou=users,dc=example,dc=org"
|
||||
#
|
||||
#bind_dn = ""
|
||||
|
||||
# Path to a file on the system that contains the password for the
|
||||
# `bind_dn`.
|
||||
#
|
||||
# The server must be able to access the file, and it must not be empty.
|
||||
#
|
||||
#bind_password_file = ""
|
||||
|
||||
# Search filter to limit user searches.
|
||||
#
|
||||
# You can use the variable `{username}` that will be replaced by the
|
||||
# entered username for more complex filters.
|
||||
#
|
||||
# example: "(&(objectClass=person)(memberOf=matrix))"
|
||||
#
|
||||
#filter = "(objectClass=*)"
|
||||
|
||||
# Attribute to use to uniquely identify the user.
|
||||
#
|
||||
# example: "uid" or "cn"
|
||||
#
|
||||
#uid_attribute = "uid"
|
||||
|
||||
# Attribute containing the display name of the user.
|
||||
#
|
||||
# example: "givenName" or "sn"
|
||||
#
|
||||
#name_attribute = "givenName"
|
||||
|
||||
# Root of the searches for admin users.
|
||||
#
|
||||
# Defaults to `base_dn` if empty.
|
||||
#
|
||||
# example: "ou=admins,dc=example,dc=org"
|
||||
#
|
||||
#admin_base_dn = ""
|
||||
|
||||
# The LDAP search filter to find administrative users for continuwuity.
|
||||
#
|
||||
# If left blank, administrative state must be configured manually for each
|
||||
# user.
|
||||
#
|
||||
# You can use the variable `{username}` that will be replaced by the
|
||||
# entered username for more complex filters.
|
||||
#
|
||||
# example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
|
||||
#
|
||||
#admin_filter = ""
|
||||
|
||||
Vendored
+1
@@ -16,6 +16,7 @@ Environment="CONTINUWUITY_CONFIG=/etc/conduwuit/conduwuit.toml"
|
||||
|
||||
Environment="CONTINUWUITY_LOG_TO_JOURNALD=true"
|
||||
Environment="CONTINUWUITY_JOURNALD_IDENTIFIER=%N"
|
||||
Environment="CONTINUWUITY_DATABASE_PATH=/var/lib/conduwuit"
|
||||
|
||||
ExecStart=/usr/sbin/conduwuit
|
||||
|
||||
|
||||
+22
-19
@@ -78,7 +78,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
WORKDIR /app
|
||||
COPY ./rust-toolchain.toml .
|
||||
RUN rustc --version \
|
||||
&& rustup target add $(xx-cargo --print-target-triple)
|
||||
&& xx-cargo --setup-target-triple
|
||||
|
||||
# Build binary
|
||||
# We disable incremental compilation to save disk space, as it only produces a minimal speedup for this case.
|
||||
@@ -87,8 +87,10 @@ RUN echo "CARGO_INCREMENTAL=0" >> /etc/environment
|
||||
# Configure pkg-config
|
||||
RUN <<EOF
|
||||
set -o xtrace
|
||||
echo "PKG_CONFIG_LIBDIR=/usr/lib/$(xx-info)/pkgconfig" >> /etc/environment
|
||||
echo "PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /etc/environment
|
||||
if command -v "$(xx-info)-pkg-config" >/dev/null 2>/dev/null; then
|
||||
echo "PKG_CONFIG_LIBDIR=/usr/lib/$(xx-info)/pkgconfig" >> /etc/environment
|
||||
echo "PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /etc/environment
|
||||
fi
|
||||
echo "PKG_CONFIG_ALLOW_CROSS=true" >> /etc/environment
|
||||
EOF
|
||||
|
||||
@@ -109,16 +111,17 @@ RUN <<EOF
|
||||
EOF
|
||||
|
||||
# Apply CPU-specific optimizations if TARGET_CPU is provided
|
||||
ARG TARGET_CPU=
|
||||
ARG TARGET_CPU
|
||||
|
||||
RUN <<EOF
|
||||
set -o allexport
|
||||
set -o xtrace
|
||||
. /etc/environment
|
||||
if [ -n "${TARGET_CPU}" ]; then
|
||||
echo "CFLAGS='${CFLAGS} -march=${TARGET_CPU}'" >> /etc/environment
|
||||
echo "CXXFLAGS='${CXXFLAGS} -march=${TARGET_CPU}'" >> /etc/environment
|
||||
echo "RUSTFLAGS='${RUSTFLAGS} -C target-cpu=${TARGET_CPU}'" >> /etc/environment
|
||||
fi
|
||||
set -o allexport
|
||||
set -o xtrace
|
||||
. /etc/environment
|
||||
if [ -n "${TARGET_CPU}" ]; then
|
||||
echo "CFLAGS='${CFLAGS} -march=${TARGET_CPU}'" >> /etc/environment
|
||||
echo "CXXFLAGS='${CXXFLAGS} -march=${TARGET_CPU}'" >> /etc/environment
|
||||
echo "RUSTFLAGS='${RUSTFLAGS} -C target-cpu=${TARGET_CPU}'" >> /etc/environment
|
||||
fi
|
||||
EOF
|
||||
|
||||
# Prepare output directories
|
||||
@@ -136,12 +139,12 @@ ARG TARGETPLATFORM
|
||||
RUN xx-cargo --print-target-triple
|
||||
|
||||
# Conduwuit version info
|
||||
ARG GIT_COMMIT_HASH=
|
||||
ARG GIT_COMMIT_HASH_SHORT=
|
||||
ARG GIT_REMOTE_URL=
|
||||
ARG GIT_REMOTE_COMMIT_URL=
|
||||
ARG CONDUWUIT_VERSION_EXTRA=
|
||||
ARG CONTINUWUITY_VERSION_EXTRA=
|
||||
ARG GIT_COMMIT_HASH
|
||||
ARG GIT_COMMIT_HASH_SHORT
|
||||
ARG GIT_REMOTE_URL
|
||||
ARG GIT_REMOTE_COMMIT_URL
|
||||
ARG CONDUWUIT_VERSION_EXTRA
|
||||
ARG CONTINUWUITY_VERSION_EXTRA
|
||||
ENV GIT_COMMIT_HASH=$GIT_COMMIT_HASH
|
||||
ENV GIT_COMMIT_HASH_SHORT=$GIT_COMMIT_HASH_SHORT
|
||||
ENV GIT_REMOTE_URL=$GIT_REMOTE_URL
|
||||
@@ -169,7 +172,7 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name"))
|
||||
for BINARY in "${BINARIES[@]}"; do
|
||||
echo $BINARY
|
||||
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY
|
||||
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY
|
||||
cp $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY /out/sbin/$BINARY
|
||||
done
|
||||
EOF
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
# Why does this exist?
|
||||
# Debian doesn't provide prebuilt musl packages
|
||||
# rocksdb requires a prebuilt liburing, and linking fails if a gnu one is provided
|
||||
|
||||
ARG RUST_VERSION=1
|
||||
ARG ALPINE_VERSION=3.22
|
||||
|
||||
FROM --platform=$BUILDPLATFORM docker.io/tonistiigi/xx AS xx
|
||||
FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-alpine${ALPINE_VERSION} AS base
|
||||
FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-alpine${ALPINE_VERSION} AS toolchain
|
||||
|
||||
# Install repo tools and dependencies
|
||||
RUN --mount=type=cache,target=/etc/apk/cache apk add \
|
||||
build-base pkgconfig make jq bash \
|
||||
curl git file \
|
||||
llvm-dev clang clang-static lld
|
||||
|
||||
|
||||
# Developer tool versions
|
||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||
ENV BINSTALL_VERSION=1.13.0
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
ENV LDDTREE_VERSION=0.3.7
|
||||
|
||||
# Install unpackaged tools
|
||||
RUN <<EOF
|
||||
set -o xtrace
|
||||
curl --retry 5 -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
|
||||
cargo binstall --no-confirm cargo-sbom --version $CARGO_SBOM_VERSION
|
||||
cargo binstall --no-confirm lddtree --version $LDDTREE_VERSION
|
||||
EOF
|
||||
|
||||
# Set up xx (cross-compilation scripts)
|
||||
COPY --from=xx / /
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Install libraries linked by the binary
|
||||
RUN --mount=type=cache,target=/etc/apk/cache xx-apk add musl-dev gcc g++ liburing-dev
|
||||
|
||||
# Set up Rust toolchain
|
||||
WORKDIR /app
|
||||
COPY ./rust-toolchain.toml .
|
||||
RUN rustc --version \
|
||||
&& xx-cargo --setup-target-triple
|
||||
|
||||
# Build binary
|
||||
# We disable incremental compilation to save disk space, as it only produces a minimal speedup for this case.
|
||||
RUN echo "CARGO_INCREMENTAL=0" >> /etc/environment
|
||||
|
||||
# Configure pkg-config
|
||||
RUN <<EOF
|
||||
set -o xtrace
|
||||
if command -v "$(xx-info)-pkg-config" >/dev/null 2>/dev/null; then
|
||||
echo "PKG_CONFIG_LIBDIR=/usr/lib/$(xx-info)/pkgconfig" >> /etc/environment
|
||||
echo "PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /etc/environment
|
||||
fi
|
||||
echo "PKG_CONFIG_ALLOW_CROSS=true" >> /etc/environment
|
||||
EOF
|
||||
|
||||
# Configure cc to use clang version
|
||||
RUN <<EOF
|
||||
set -o xtrace
|
||||
echo "CC=clang" >> /etc/environment
|
||||
echo "CXX=clang++" >> /etc/environment
|
||||
EOF
|
||||
|
||||
# Cross-language LTO
|
||||
RUN <<EOF
|
||||
set -o xtrace
|
||||
echo "CFLAGS=-flto" >> /etc/environment
|
||||
echo "CXXFLAGS=-flto" >> /etc/environment
|
||||
# Linker is set to target-compatible clang by xx
|
||||
echo "RUSTFLAGS='-Clinker-plugin-lto -Clink-arg=-fuse-ld=lld'" >> /etc/environment
|
||||
EOF
|
||||
|
||||
# Apply CPU-specific optimizations if TARGET_CPU is provided
|
||||
ARG TARGET_CPU
|
||||
|
||||
RUN <<EOF
|
||||
set -o allexport
|
||||
set -o xtrace
|
||||
. /etc/environment
|
||||
if [ -n "${TARGET_CPU}" ]; then
|
||||
echo "CFLAGS='${CFLAGS} -march=${TARGET_CPU}'" >> /etc/environment
|
||||
echo "CXXFLAGS='${CXXFLAGS} -march=${TARGET_CPU}'" >> /etc/environment
|
||||
echo "RUSTFLAGS='${RUSTFLAGS} -C target-cpu=${TARGET_CPU}'" >> /etc/environment
|
||||
fi
|
||||
EOF
|
||||
|
||||
# Prepare output directories
|
||||
RUN mkdir /out
|
||||
|
||||
FROM toolchain AS builder
|
||||
|
||||
|
||||
# Get source
|
||||
COPY . .
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Verify environment configuration
|
||||
RUN xx-cargo --print-target-triple
|
||||
|
||||
# Conduwuit version info
|
||||
ARG GIT_COMMIT_HASH
|
||||
ARG GIT_COMMIT_HASH_SHORT
|
||||
ARG GIT_REMOTE_URL
|
||||
ARG GIT_REMOTE_COMMIT_URL
|
||||
ARG CONDUWUIT_VERSION_EXTRA
|
||||
ARG CONTINUWUITY_VERSION_EXTRA
|
||||
ENV GIT_COMMIT_HASH=$GIT_COMMIT_HASH
|
||||
ENV GIT_COMMIT_HASH_SHORT=$GIT_COMMIT_HASH_SHORT
|
||||
ENV GIT_REMOTE_URL=$GIT_REMOTE_URL
|
||||
ENV GIT_REMOTE_COMMIT_URL=$GIT_REMOTE_COMMIT_URL
|
||||
ENV CONDUWUIT_VERSION_EXTRA=$CONDUWUIT_VERSION_EXTRA
|
||||
ENV CONTINUWUITY_VERSION_EXTRA=$CONTINUWUITY_VERSION_EXTRA
|
||||
|
||||
ARG RUST_PROFILE=release
|
||||
|
||||
# Build the binary
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git/db \
|
||||
--mount=type=cache,target=/app/target,id=cargo-target-${TARGET_CPU}-${TARGETPLATFORM}-musl-${RUST_PROFILE} \
|
||||
bash <<'EOF'
|
||||
set -o allexport
|
||||
set -o xtrace
|
||||
. /etc/environment
|
||||
TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \
|
||||
jq -r ".target_directory"))
|
||||
mkdir /out/sbin
|
||||
PACKAGE=conduwuit
|
||||
xx-cargo build --locked --profile ${RUST_PROFILE} \
|
||||
-p $PACKAGE --no-default-features --features bindgen-static,release_max_log_level,standard;
|
||||
BINARIES=($(cargo metadata --no-deps --format-version 1 | \
|
||||
jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name"))
|
||||
for BINARY in "${BINARIES[@]}"; do
|
||||
echo $BINARY
|
||||
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY
|
||||
cp $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY /out/sbin/$BINARY
|
||||
done
|
||||
EOF
|
||||
|
||||
# Generate Software Bill of Materials (SBOM)
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git/db \
|
||||
bash <<'EOF'
|
||||
set -o xtrace
|
||||
mkdir /out/sbom
|
||||
typeset -A PACKAGES
|
||||
for BINARY in /out/sbin/*; do
|
||||
BINARY_BASE=$(basename ${BINARY})
|
||||
package=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.targets[] | select( .kind | map(. == \"bin\") | any ) | .name == \"$BINARY_BASE\") | .name")
|
||||
if [ -z "$package" ]; then
|
||||
continue
|
||||
fi
|
||||
PACKAGES[$package]=1
|
||||
done
|
||||
for PACKAGE in $(echo ${!PACKAGES[@]}); do
|
||||
echo $PACKAGE
|
||||
cargo sbom --cargo-package $PACKAGE > /out/sbom/$PACKAGE.spdx.json
|
||||
done
|
||||
EOF
|
||||
|
||||
# Extract dynamically linked dependencies
|
||||
RUN <<EOF
|
||||
set -o xtrace
|
||||
mkdir /out/libs
|
||||
mkdir /out/libs-root
|
||||
for BINARY in /out/sbin/*; do
|
||||
lddtree "$BINARY" | awk '{print $(NF-0) " " $1}' | sort -u -k 1,1 | awk '{print "install", "-D", $1, (($2 ~ /^\//) ? "/out/libs-root" $2 : "/out/libs/" $2)}' | xargs -I {} sh -c {}
|
||||
done
|
||||
EOF
|
||||
|
||||
FROM scratch
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# Copy root certs for tls into image
|
||||
# You can also mount the certs from the host
|
||||
# --volume /etc/ssl/certs:/etc/ssl/certs:ro
|
||||
COPY --from=base /etc/ssl/certs /etc/ssl/certs
|
||||
|
||||
# Copy our build
|
||||
COPY --from=builder /out/sbin/ /sbin/
|
||||
# Copy SBOM
|
||||
COPY --from=builder /out/sbom/ /sbom/
|
||||
|
||||
# Copy dynamic libraries to root
|
||||
COPY --from=builder /out/libs-root/ /
|
||||
COPY --from=builder /out/libs/ /usr/lib/
|
||||
|
||||
# Inform linker where to find libraries
|
||||
ENV LD_LIBRARY_PATH=/usr/lib
|
||||
|
||||
# Continuwuity default port
|
||||
EXPOSE 8008
|
||||
|
||||
CMD ["/sbin/conduwuit"]
|
||||
@@ -21,6 +21,7 @@ # Command-Line Help for `admin`
|
||||
* [`admin users list-joined-rooms`↴](#admin-users-list-joined-rooms)
|
||||
* [`admin users force-join-room`↴](#admin-users-force-join-room)
|
||||
* [`admin users force-leave-room`↴](#admin-users-force-leave-room)
|
||||
* [`admin users force-leave-remote-room`↴](#admin-users-force-leave-remote-room)
|
||||
* [`admin users force-demote`↴](#admin-users-force-demote)
|
||||
* [`admin users make-user-admin`↴](#admin-users-make-user-admin)
|
||||
* [`admin users put-room-tag`↴](#admin-users-put-room-tag)
|
||||
@@ -295,6 +296,7 @@ ###### **Subcommands:**
|
||||
* `list-joined-rooms` — - Lists all the rooms (local and remote) that the specified user is joined in
|
||||
* `force-join-room` — - Manually join a local user to a room
|
||||
* `force-leave-room` — - Manually leave a local user from a room
|
||||
* `force-leave-remote-room` — - Manually leave a remote room for a local user
|
||||
* `force-demote` — - Forces the specified user to drop their power levels to the room default, if their permissions allow and the auth check permits
|
||||
* `make-user-admin` — - Grant server-admin privileges to a user
|
||||
* `put-room-tag` — - Puts a room tag for the specified user and room ID
|
||||
@@ -449,6 +451,19 @@ ###### **Arguments:**
|
||||
|
||||
|
||||
|
||||
## `admin users force-leave-remote-room`
|
||||
|
||||
- Manually leave a remote room for a local user
|
||||
|
||||
**Usage:** `admin users force-leave-remote-room <USER_ID> <ROOM_ID>`
|
||||
|
||||
###### **Arguments:**
|
||||
|
||||
* `<USER_ID>`
|
||||
* `<ROOM_ID>`
|
||||
|
||||
|
||||
|
||||
## `admin users force-demote`
|
||||
|
||||
- Forces the specified user to drop their power levels to the room default, if their permissions allow and the auth check permits
|
||||
|
||||
@@ -12,6 +12,15 @@ services:
|
||||
#- ./continuwuity.toml:/etc/continuwuity.toml
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))"
|
||||
- "traefik.http.routers.continuwuity.entrypoints=websecure" # your HTTPS entry point
|
||||
- "traefik.http.routers.continuwuity.tls=true"
|
||||
- "traefik.http.routers.continuwuity.service=continuwuity"
|
||||
- "traefik.http.services.continuwuity.loadbalancer.server.port=6167"
|
||||
# possibly, depending on your config:
|
||||
# - "traefik.http.routers.continuwuity.tls.certresolver=letsencrypt"
|
||||
environment:
|
||||
CONTINUWUITY_SERVER_NAME: your.server.name.example # EDIT THIS
|
||||
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
|
||||
|
||||
@@ -12,6 +12,14 @@ services:
|
||||
#- ./continuwuity.toml:/etc/continuwuity.toml
|
||||
networks:
|
||||
- proxy
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))"
|
||||
- "traefik.http.routers.continuwuity.entrypoints=websecure"
|
||||
- "traefik.http.routers.continuwuity.tls.certresolver=letsencrypt"
|
||||
- "traefik.http.services.continuwuity.loadbalancer.server.port=6167"
|
||||
# Uncomment and adjust the following if you want to use middleware
|
||||
# - "traefik.http.routers.continuwuity.middlewares=secureHeaders@file"
|
||||
environment:
|
||||
CONTINUWUITY_SERVER_NAME: your.server.name.example # EDIT THIS
|
||||
CONTINUWUITY_TRUSTED_SERVERS: '["matrix.org"]'
|
||||
|
||||
@@ -44,7 +44,7 @@ ### Building with the Rust toolchain
|
||||
- (On linux) `pkg-config` on the compiling machine to allow finding `liburing`
|
||||
- A C++ compiler and (on linux) `libclang` for RocksDB
|
||||
|
||||
You can build Continuwuity using `cargo build --release --all-features`.
|
||||
You can build Continuwuity using `cargo build --release`.
|
||||
|
||||
### Building with Nix
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -6,8 +6,8 @@
|
||||
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"message": "🎉 Continuwuity v0.5.0-rc.6 is now available! This release includes improved knock-restricted room handling, automatic support contact configuration, and a new HTML landing page. Check [the release notes for full details](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.0-rc.6) and upgrade instructions."
|
||||
"id": 3,
|
||||
"message": "_taps microphone_ The Continuwuity 0.5.0-rc.7 release is now available, and it's better than ever! **177 commits**, **35 pull requests**, **11 contributors,** and a lot of new stuff!\n\nFor highlights, we've got:\n\n* 🕵️ Full Policy Server support to fight spam!\n* 🚀 Smarter room & space upgrades.\n* 🚫 User suspension tools for better moderation.\n* 🤖 reCaptcha support for safer open registration.\n* 🔍 Ability to disable read receipts & typing indicators.\n* ⚡ Sweeping performance improvements!\n\nGet the [full changelog and downloads on our Forgejo](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.0-rc.7) - and make sure you're in the [Announcements room](https://matrix.to/#/!releases:continuwuity.org/$hN9z6L2_dTAlPxFLAoXVfo_g8DyYXu4cpvWsSrWhmB0) to get stuff like this sooner."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+3
-3
@@ -83,7 +83,7 @@ env DIRENV_DEVSHELL=all-features \
|
||||
--workspace \
|
||||
--locked \
|
||||
--profile test \
|
||||
--all-features \
|
||||
--features full \
|
||||
--no-deps \
|
||||
--document-private-items \
|
||||
--color always
|
||||
@@ -96,7 +96,7 @@ script = """
|
||||
direnv exec . \
|
||||
cargo clippy \
|
||||
--workspace \
|
||||
--all-features \
|
||||
--features full \
|
||||
--locked \
|
||||
--profile test \
|
||||
--color=always \
|
||||
@@ -114,7 +114,7 @@ env DIRENV_DEVSHELL=all-features \
|
||||
--workspace \
|
||||
--locked \
|
||||
--profile test \
|
||||
--all-features \
|
||||
--features full \
|
||||
--color=always \
|
||||
-- \
|
||||
-D warnings
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
[Unit]
|
||||
Description=Continuwuity - Matrix homeserver
|
||||
Documentation=https://continuwuity.org/
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
Alias=matrix-conduwuit.service
|
||||
|
||||
[Service]
|
||||
DynamicUser=yes
|
||||
User=conduwuit
|
||||
Group=conduwuit
|
||||
Type=notify
|
||||
|
||||
Environment="CONTINUWUITY_CONFIG=/etc/conduwuit/conduwuit.toml"
|
||||
|
||||
Environment="CONTINUWUITY_LOG_TO_JOURNALD=true"
|
||||
Environment="CONTINUWUITY_JOURNALD_IDENTIFIER=%N"
|
||||
Environment="CONTINUWUITY_DATABASE_PATH=/var/lib/conduwuit"
|
||||
|
||||
ExecStart=/usr/bin/conduwuit
|
||||
|
||||
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
|
||||
ConfigurationDirectory=conduwuit
|
||||
RuntimeDirectory=conduwuit
|
||||
RuntimeDirectoryMode=0750
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
TimeoutStopSec=2m
|
||||
TimeoutStartSec=2m
|
||||
|
||||
StartLimitInterval=1m
|
||||
StartLimitBurst=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,80 @@
|
||||
# This should be run using rpkg-util: https://docs.pagure.org/rpkg-util
|
||||
# it requires Internet access and is not suitable for Fedora main repos
|
||||
# TODO: rpkg-util is no longer maintained, find a replacement
|
||||
|
||||
Name: continuwuity
|
||||
Version: {{{ git_repo_version }}}
|
||||
Release: 1%{?dist}
|
||||
Summary: Very cool Matrix chat homeserver written in Rust
|
||||
|
||||
License: Apache-2.0 AND MIT
|
||||
|
||||
URL: https://continuwuity.org
|
||||
VCS: {{{ git_repo_vcs }}}
|
||||
Source: {{{ git_repo_pack }}}
|
||||
|
||||
BuildRequires: cargo-rpm-macros >= 25
|
||||
BuildRequires: systemd-rpm-macros
|
||||
# Needed to build rust-librocksdb-sys
|
||||
BuildRequires: clang
|
||||
BuildRequires: liburing-devel
|
||||
|
||||
Requires: liburing
|
||||
Requires: glibc
|
||||
Requires: libstdc++
|
||||
|
||||
%global _description %{expand:
|
||||
A cool hard fork of Conduit, a Matrix homeserver written in Rust}
|
||||
|
||||
%description %{_description}
|
||||
|
||||
%prep
|
||||
{{{ git_repo_setup_macro }}}
|
||||
%cargo_prep -N
|
||||
# Perform an online build so Git dependencies can be retrieved
|
||||
sed -i 's/^offline = true$//' .cargo/config.toml
|
||||
|
||||
%build
|
||||
%cargo_build
|
||||
|
||||
# Here's the one legally required mystery incantation in this file.
|
||||
# Some of our dependencies have source files which are (for some reason) marked as executable.
|
||||
# Files in .cargo/registry/ are copied into /usr/src/ by the debuginfo machinery
|
||||
# at the end of the build step, and then the BRP shebang mangling script checks
|
||||
# the entire buildroot to find executable files, and fails the build because
|
||||
# it thinks Rust's file attributes are shebangs because they start with `#!`.
|
||||
# So we have to clear the executable bit on all of them before that happens.
|
||||
find .cargo/registry/ -executable -name "*.rs" -exec chmod -x {} +
|
||||
|
||||
# TODO: this fails currently because it's forced to run in offline mode
|
||||
# {cargo_license -- --no-dev} > LICENSE.dependencies
|
||||
|
||||
%install
|
||||
install -Dpm0755 target/rpm/conduwuit -t %{buildroot}%{_bindir}
|
||||
install -Dpm0644 fedora/conduwuit.service -t %{buildroot}%{_unitdir}
|
||||
install -Dpm0644 conduwuit-example.toml %{buildroot}%{_sysconfdir}/conduwuit/conduwuit.toml
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%license src/core/matrix/state_res/LICENSE
|
||||
%doc CODE_OF_CONDUCT.md
|
||||
%doc CONTRIBUTING.md
|
||||
%doc README.md
|
||||
%doc SECURITY.md
|
||||
%config %{_sysconfdir}/conduwuit/conduwuit.toml
|
||||
|
||||
%{_bindir}/conduwuit
|
||||
%{_unitdir}/conduwuit.service
|
||||
# Do not create /var/lib/conduwuit, systemd will create it if necessary
|
||||
|
||||
%post
|
||||
%systemd_post conduwuit.service
|
||||
|
||||
%preun
|
||||
%systemd_preun conduwuit.service
|
||||
|
||||
%postun
|
||||
%systemd_postun_with_restart conduwuit.service
|
||||
|
||||
%changelog
|
||||
{{{ git_repo_changelog }}}
|
||||
Generated
+12
-12
@@ -153,11 +153,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1751525020,
|
||||
"narHash": "sha256-oDO6lCYS5Bf4jUITChj9XV7k3TP38DE0Ckz5n5ORCME=",
|
||||
"lastModified": 1755585599,
|
||||
"narHash": "sha256-tl/0cnsqB/Yt7DbaGMel2RLa7QG5elA8lkaOXli6VdY=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "a1a5f92f47787e7df9f30e5e5ac13e679215aa1e",
|
||||
"rev": "6ed03ef4c8ec36d193c18e06b9ecddde78fb7e42",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -516,16 +516,16 @@
|
||||
"rocksdb": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1741308171,
|
||||
"narHash": "sha256-YdBvdQ75UJg5ffwNjxizpviCVwVDJnBkM8ZtGIduMgY=",
|
||||
"ref": "v9.11.1",
|
||||
"rev": "3ce04794bcfbbb0d2e6f81ae35fc4acf688b6986",
|
||||
"revCount": 13177,
|
||||
"lastModified": 1753385396,
|
||||
"narHash": "sha256-/Hvy1yTH/0D5aa7bc+/uqFugCQq4InTdwlRw88vA5IY=",
|
||||
"ref": "10.4.fb",
|
||||
"rev": "28d4b7276c16ed3e28af1bd96162d6442ce25923",
|
||||
"revCount": 13318,
|
||||
"type": "git",
|
||||
"url": "https://forgejo.ellis.link/continuwuation/rocksdb"
|
||||
},
|
||||
"original": {
|
||||
"ref": "v9.11.1",
|
||||
"ref": "10.4.fb",
|
||||
"type": "git",
|
||||
"url": "https://forgejo.ellis.link/continuwuation/rocksdb"
|
||||
}
|
||||
@@ -546,11 +546,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1751433876,
|
||||
"narHash": "sha256-IsdwOcvLLDDlkFNwhdD5BZy20okIQL01+UQ7Kxbqh8s=",
|
||||
"lastModified": 1755504847,
|
||||
"narHash": "sha256-VX0B9hwhJypCGqncVVLC+SmeMVd/GAYbJZ0MiiUn2Pk=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "11d45c881389dae90b0da5a94cde52c79d0fc7ef",
|
||||
"rev": "a905e3b21b144d77e1b304e49f3264f6f8d4db75",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
nix-filter.url = "github:numtide/nix-filter?ref=main";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs?ref=nixpkgs-unstable";
|
||||
rocksdb = {
|
||||
url = "git+https://forgejo.ellis.link/continuwuation/rocksdb?ref=v9.11.1";
|
||||
url = "git+https://forgejo.ellis.link/continuwuation/rocksdb?ref=10.4.fb";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
@@ -31,13 +31,17 @@
|
||||
inherit system;
|
||||
};
|
||||
|
||||
fnx = inputs.fenix.packages.${system};
|
||||
# The Rust toolchain to use
|
||||
toolchain = inputs.fenix.packages.${system}.fromToolchainFile {
|
||||
file = ./rust-toolchain.toml;
|
||||
toolchain = fnx.combine [
|
||||
(fnx.fromToolchainFile {
|
||||
file = ./rust-toolchain.toml;
|
||||
|
||||
# See also `rust-toolchain.toml`
|
||||
sha256 = "sha256-KUm16pHj+cRedf8vxs/Hd2YWxpOrWZ7UOrwhILdSJBU=";
|
||||
};
|
||||
# See also `rust-toolchain.toml`
|
||||
sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE=";
|
||||
})
|
||||
fnx.complete.rustfmt
|
||||
];
|
||||
|
||||
mkScope =
|
||||
pkgs:
|
||||
@@ -62,7 +66,7 @@
|
||||
}).overrideAttrs
|
||||
(old: {
|
||||
src = inputs.rocksdb;
|
||||
version = "v9.11.1";
|
||||
version = "v10.4.fb";
|
||||
cmakeFlags =
|
||||
pkgs.lib.subtractLists [
|
||||
# No real reason to have snappy or zlib, no one uses this
|
||||
|
||||
+56
-23
@@ -1,26 +1,59 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": [
|
||||
"at any time"
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": ["at any time"]
|
||||
},
|
||||
"nix": {
|
||||
"enabled": true
|
||||
},
|
||||
"labels": ["Dependencies", "Dependencies/Renovate"],
|
||||
"ignoreDeps": [
|
||||
"tikv-jemallocator",
|
||||
"tikv-jemalloc-sys",
|
||||
"tikv-jemalloc-ctl",
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
"opentelemetry-jaeger",
|
||||
"tracing-opentelemetry"
|
||||
],
|
||||
"github-actions": {
|
||||
"enabled": true,
|
||||
"managerFilePatterns": [
|
||||
"/(^|/)\\.forgejo/workflows/[^/]+\\.ya?ml$/",
|
||||
"/(^|/)\\.forgejo/actions/[^/]+/action\\.ya?ml$/",
|
||||
"/(^|/)\\.github/workflows/[^/]+\\.ya?ml$/",
|
||||
"/(^|/)\\.github/actions/[^/]+/action\\.ya?ml$/"
|
||||
]
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Batch minor and patch GitHub Actions updates",
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "github-actions-non-major"
|
||||
},
|
||||
{
|
||||
"description": "Group Rust toolchain updates into a single PR",
|
||||
"matchManagers": ["custom.regex"],
|
||||
"matchPackageNames": ["rust", "rustc", "cargo"],
|
||||
"groupName": "rust-toolchain"
|
||||
},
|
||||
{
|
||||
"description": "Group lockfile updates into a single PR",
|
||||
"matchUpdateTypes": ["lockFileMaintenance"],
|
||||
"groupName": "lockfile-maintenance"
|
||||
},
|
||||
{
|
||||
"description": "Batch patch-level Rust dependency updates",
|
||||
"matchManagers": ["cargo"],
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"groupName": "rust-patch-updates"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["cargo"],
|
||||
"prConcurrentLimit": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"nix": {
|
||||
"enabled": true
|
||||
},
|
||||
"labels": [
|
||||
"dependencies",
|
||||
"github_actions"
|
||||
],
|
||||
"ignoreDeps": [
|
||||
"tikv-jemllocator",
|
||||
"tikv-jemalloc-sys",
|
||||
"tikv-jemalloc-ctl",
|
||||
"opentelemetry-rust",
|
||||
"tracing-opentelemetry"
|
||||
]
|
||||
}
|
||||
|
||||
+5
-2
@@ -9,13 +9,16 @@
|
||||
# If you're having trouble making the relevant changes, bug a maintainer.
|
||||
|
||||
[toolchain]
|
||||
channel = "1.87.0"
|
||||
profile = "minimal"
|
||||
channel = "1.89.0"
|
||||
components = [
|
||||
# For rust-analyzer
|
||||
"rust-src",
|
||||
"rust-analyzer",
|
||||
# For CI and editors
|
||||
"rustfmt",
|
||||
"clippy",
|
||||
# you have to install rustfmt nightly yourself (if you're not on NixOS)
|
||||
#
|
||||
# The rust-toolchain.toml file doesn't provide any syntax for specifying components from different toolchains
|
||||
# "rustfmt"
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{collections::BTreeMap, fmt::Write as _};
|
||||
|
||||
use api::client::{
|
||||
full_user_deactivate, join_room_by_id_helper, leave_all_rooms, leave_room, update_avatar_url,
|
||||
update_displayname,
|
||||
full_user_deactivate, join_room_by_id_helper, leave_all_rooms, leave_room, remote_leave_room,
|
||||
update_avatar_url, update_displayname,
|
||||
};
|
||||
use conduwuit::{
|
||||
Err, Result, debug, debug_warn, error, info, is_equal_to,
|
||||
@@ -68,7 +68,8 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
|
||||
// Create user
|
||||
self.services
|
||||
.users
|
||||
.create(&user_id, Some(password.as_str()))?;
|
||||
.create(&user_id, Some(password.as_str()), None)
|
||||
.await?;
|
||||
|
||||
// Default to pretty displayname
|
||||
let mut displayname = user_id.localpart().to_owned();
|
||||
@@ -284,6 +285,7 @@ pub(super) async fn reset_password(&self, username: String, password: Option<Str
|
||||
.services
|
||||
.users
|
||||
.set_password(&user_id, Some(new_password.as_str()))
|
||||
.await
|
||||
{
|
||||
| Err(e) => return Err!("Couldn't reset the password for user {user_id}: {e}"),
|
||||
| Ok(()) => {
|
||||
@@ -924,3 +926,29 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn force_leave_remote_room(
|
||||
&self,
|
||||
user_id: String,
|
||||
room_id: OwnedRoomOrAliasId,
|
||||
) -> Result {
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
let (room_id, _) = self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_with_servers(&room_id, None)
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
remote_leave_room(self.services, &user_id, &room_id, None)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!("{user_id} has been joined to {room_id}.",))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -103,6 +103,12 @@ pub enum UserCommand {
|
||||
room_id: OwnedRoomOrAliasId,
|
||||
},
|
||||
|
||||
/// - Manually leave a remote room for a local user.
|
||||
ForceLeaveRemoteRoom {
|
||||
user_id: String,
|
||||
room_id: OwnedRoomOrAliasId,
|
||||
},
|
||||
|
||||
/// - Forces the specified user to drop their power levels to the room
|
||||
/// default, if their permissions allow and the auth check permits
|
||||
ForceDemote {
|
||||
|
||||
@@ -49,6 +49,9 @@ jemalloc_stats = [
|
||||
"conduwuit-core/jemalloc_stats",
|
||||
"conduwuit-service/jemalloc_stats",
|
||||
]
|
||||
ldap = [
|
||||
"conduwuit-service/ldap"
|
||||
]
|
||||
release_max_log_level = [
|
||||
"conduwuit-core/release_max_log_level",
|
||||
"conduwuit-service/release_max_log_level",
|
||||
|
||||
@@ -373,7 +373,7 @@ pub(crate) async fn register_route(
|
||||
let password = if is_guest { None } else { body.password.as_deref() };
|
||||
|
||||
// Create user
|
||||
services.users.create(&user_id, password)?;
|
||||
services.users.create(&user_id, password, None).await?;
|
||||
|
||||
// Default to pretty displayname
|
||||
let mut displayname = user_id.localpart().to_owned();
|
||||
@@ -659,7 +659,8 @@ pub(crate) async fn change_password_route(
|
||||
|
||||
services
|
||||
.users
|
||||
.set_password(sender_user, Some(&body.new_password))?;
|
||||
.set_password(sender_user, Some(&body.new_password))
|
||||
.await?;
|
||||
|
||||
if body.logout_devices {
|
||||
// Logout all devices except the current one
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mod suspend;
|
||||
|
||||
pub(crate) use self::suspend::*;
|
||||
@@ -0,0 +1,89 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::future::{join, join3};
|
||||
use ruma::api::client::admin::{get_suspended, set_suspended};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_matrix/client/v1/admin/suspend/{userId}`
|
||||
///
|
||||
/// Check the suspension status of a target user
|
||||
pub(crate) async fn get_suspended_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_suspended::v1::Request>,
|
||||
) -> Result<get_suspended::v1::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
let (admin, active) =
|
||||
join(services.users.is_admin(sender_user), services.users.is_active(&body.user_id)).await;
|
||||
if !admin {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
if !services.globals.user_is_local(&body.user_id) {
|
||||
return Err!(Request(InvalidParam("Can only check the suspended status of local users")));
|
||||
}
|
||||
if !active {
|
||||
return Err!(Request(NotFound("Unknown user")));
|
||||
}
|
||||
Ok(get_suspended::v1::Response::new(
|
||||
services.users.is_suspended(&body.user_id).await?,
|
||||
))
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/v1/admin/suspend/{userId}`
|
||||
///
|
||||
/// Set the suspension status of a target user
|
||||
pub(crate) async fn put_suspended_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<set_suspended::v1::Request>,
|
||||
) -> Result<set_suspended::v1::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
let (sender_admin, active, target_admin) = join3(
|
||||
services.users.is_admin(sender_user),
|
||||
services.users.is_active(&body.user_id),
|
||||
services.users.is_admin(&body.user_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
if !sender_admin {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
if !services.globals.user_is_local(&body.user_id) {
|
||||
return Err!(Request(InvalidParam("Can only set the suspended status of local users")));
|
||||
}
|
||||
if !active {
|
||||
return Err!(Request(NotFound("Unknown user")));
|
||||
}
|
||||
if body.user_id == *sender_user {
|
||||
return Err!(Request(Forbidden("You cannot suspend yourself")));
|
||||
}
|
||||
if target_admin {
|
||||
return Err!(Request(Forbidden("You cannot suspend another server administrator")));
|
||||
}
|
||||
if services.users.is_suspended(&body.user_id).await? == body.suspended {
|
||||
// No change
|
||||
return Ok(set_suspended::v1::Response::new(body.suspended));
|
||||
}
|
||||
|
||||
let action = if body.suspended {
|
||||
services
|
||||
.users
|
||||
.suspend_account(&body.user_id, sender_user)
|
||||
.await;
|
||||
"suspended"
|
||||
} else {
|
||||
services.users.unsuspend_account(&body.user_id).await;
|
||||
"unsuspended"
|
||||
};
|
||||
|
||||
if services.config.admin_room_notices {
|
||||
// Notify the admin room that an account has been un/suspended
|
||||
services
|
||||
.admin
|
||||
.send_text(&format!("{} has been {} by {}.", body.user_id, action, sender_user))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(set_suspended::v1::Response::new(body.suspended))
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
/// of this server.
|
||||
pub(crate) async fn get_capabilities_route(
|
||||
State(services): State<crate::State>,
|
||||
_body: Ruma<get_capabilities::v3::Request>,
|
||||
body: Ruma<get_capabilities::v3::Request>,
|
||||
) -> Result<get_capabilities::v3::Response> {
|
||||
let available: BTreeMap<RoomVersionId, RoomVersionStability> =
|
||||
Server::available_room_versions().collect();
|
||||
@@ -45,5 +45,14 @@ pub(crate) async fn get_capabilities_route(
|
||||
json!({"enabled": services.config.forget_forced_upon_leave}),
|
||||
)?;
|
||||
|
||||
if services
|
||||
.users
|
||||
.is_admin(body.sender_user.as_ref().unwrap())
|
||||
.await
|
||||
{
|
||||
// Advertise suspension API
|
||||
capabilities.set("uk.timedout.msc4323", json!({"suspend":true, "lock": false}))?;
|
||||
}
|
||||
|
||||
Ok(get_capabilities::v3::Response { capabilities })
|
||||
}
|
||||
|
||||
@@ -156,31 +156,34 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||
.await?;
|
||||
|
||||
let mut servers = body.via.clone();
|
||||
servers.extend(
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await,
|
||||
);
|
||||
if servers.is_empty() {
|
||||
debug!("No via servers provided for join, injecting some.");
|
||||
servers.extend(
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await,
|
||||
);
|
||||
|
||||
servers.extend(
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.invite_state(sender_user, &room_id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter_map(|event| event.get_field("sender").ok().flatten())
|
||||
.filter_map(|sender: &str| UserId::parse(sender).ok())
|
||||
.map(|user| user.server_name().to_owned()),
|
||||
);
|
||||
servers.extend(
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.invite_state(sender_user, &room_id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter_map(|event| event.get_field("sender").ok().flatten())
|
||||
.filter_map(|sender: &str| UserId::parse(sender).ok())
|
||||
.map(|user| user.server_name().to_owned()),
|
||||
);
|
||||
|
||||
if let Some(server) = room_id.server_name() {
|
||||
servers.push(server.to_owned());
|
||||
if let Some(server) = room_id.server_name() {
|
||||
servers.push(server.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
servers.sort_unstable();
|
||||
|
||||
@@ -215,7 +215,7 @@ pub async fn leave_room(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remote_leave_room(
|
||||
pub async fn remote_leave_room(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
};
|
||||
pub use self::{
|
||||
join::join_room_by_id_helper,
|
||||
leave::{leave_all_rooms, leave_room},
|
||||
leave::{leave_all_rooms, leave_room, remote_leave_room},
|
||||
};
|
||||
use crate::{Ruma, client::full_user_deactivate};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
ref_at,
|
||||
utils::{
|
||||
IterStream, ReadyExt,
|
||||
result::{FlatOk, LogErr},
|
||||
result::LogErr,
|
||||
stream::{BroadbandExt, TryIgnore, WidebandExt},
|
||||
},
|
||||
};
|
||||
@@ -35,6 +35,7 @@
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
use super::utils::{count_to_token, parse_pagination_token as parse_token};
|
||||
use crate::Ruma;
|
||||
|
||||
/// list of safe and common non-state events to ignore if the user is ignored
|
||||
@@ -84,14 +85,14 @@ pub(crate) async fn get_message_events_route(
|
||||
let from: PduCount = body
|
||||
.from
|
||||
.as_deref()
|
||||
.map(str::parse)
|
||||
.map(parse_token)
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| match body.dir {
|
||||
| Direction::Forward => PduCount::min(),
|
||||
| Direction::Backward => PduCount::max(),
|
||||
});
|
||||
|
||||
let to: Option<PduCount> = body.to.as_deref().map(str::parse).flat_ok();
|
||||
let to: Option<PduCount> = body.to.as_deref().map(parse_token).transpose()?;
|
||||
|
||||
let limit: usize = body
|
||||
.limit
|
||||
@@ -180,8 +181,8 @@ pub(crate) async fn get_message_events_route(
|
||||
.collect();
|
||||
|
||||
Ok(get_message_events::v3::Response {
|
||||
start: from.to_string(),
|
||||
end: next_token.as_ref().map(ToString::to_string),
|
||||
start: count_to_token(from),
|
||||
end: next_token.map(count_to_token),
|
||||
chunk,
|
||||
state,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub(super) mod account;
|
||||
pub(super) mod account_data;
|
||||
pub(super) mod admin;
|
||||
pub(super) mod alias;
|
||||
pub(super) mod appservice;
|
||||
pub(super) mod backup;
|
||||
@@ -36,12 +37,14 @@
|
||||
pub(super) mod unstable;
|
||||
pub(super) mod unversioned;
|
||||
pub(super) mod user_directory;
|
||||
pub(super) mod utils;
|
||||
pub(super) mod voip;
|
||||
pub(super) mod well_known;
|
||||
|
||||
pub use account::full_user_deactivate;
|
||||
pub(super) use account::*;
|
||||
pub(super) use account_data::*;
|
||||
pub(super) use admin::*;
|
||||
pub(super) use alias::*;
|
||||
pub(super) use appservice::*;
|
||||
pub(super) use backup::*;
|
||||
@@ -54,7 +57,7 @@
|
||||
pub(super) use media::*;
|
||||
pub(super) use media_legacy::*;
|
||||
pub(super) use membership::*;
|
||||
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room};
|
||||
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room, remote_leave_room};
|
||||
pub(super) use message::*;
|
||||
pub(super) use openid::*;
|
||||
pub(super) use presence::*;
|
||||
|
||||
@@ -90,7 +90,7 @@ pub(crate) async fn get_displayname_route(
|
||||
.await
|
||||
{
|
||||
if !services.users.exists(&body.user_id).await {
|
||||
services.users.create(&body.user_id, None)?;
|
||||
services.users.create(&body.user_id, None, None).await?;
|
||||
}
|
||||
|
||||
services
|
||||
@@ -189,7 +189,7 @@ pub(crate) async fn get_avatar_url_route(
|
||||
.await
|
||||
{
|
||||
if !services.users.exists(&body.user_id).await {
|
||||
services.users.create(&body.user_id, None)?;
|
||||
services.users.create(&body.user_id, None, None).await?;
|
||||
}
|
||||
|
||||
services
|
||||
@@ -248,7 +248,7 @@ pub(crate) async fn get_profile_route(
|
||||
.await
|
||||
{
|
||||
if !services.users.exists(&body.user_id).await {
|
||||
services.users.create(&body.user_id, None)?;
|
||||
services.users.create(&body.user_id, None, None).await?;
|
||||
}
|
||||
|
||||
services
|
||||
|
||||
+54
-13
@@ -18,6 +18,7 @@
|
||||
events::{TimelineEventType, relation::RelationType},
|
||||
};
|
||||
|
||||
use super::utils::{count_to_token, parse_pagination_token as parse_token};
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
|
||||
@@ -110,14 +111,14 @@ async fn paginate_relations_with_filter(
|
||||
dir: Direction,
|
||||
) -> Result<get_relating_events::v1::Response> {
|
||||
let start: PduCount = from
|
||||
.map(str::parse)
|
||||
.map(parse_token)
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| match dir {
|
||||
| Direction::Forward => PduCount::min(),
|
||||
| Direction::Backward => PduCount::max(),
|
||||
});
|
||||
|
||||
let to: Option<PduCount> = to.map(str::parse).flat_ok();
|
||||
let to: Option<PduCount> = to.map(parse_token).transpose()?;
|
||||
|
||||
// Use limit or else 30, with maximum 100
|
||||
let limit: usize = limit
|
||||
@@ -129,6 +130,11 @@ async fn paginate_relations_with_filter(
|
||||
// Spec (v1.10) recommends depth of at least 3
|
||||
let depth: u8 = if recurse { 3 } else { 1 };
|
||||
|
||||
// Check if this is a thread request
|
||||
let is_thread = filter_rel_type
|
||||
.as_ref()
|
||||
.is_some_and(|rel| *rel == RelationType::Thread);
|
||||
|
||||
let events: Vec<_> = services
|
||||
.rooms
|
||||
.pdu_metadata
|
||||
@@ -152,23 +158,58 @@ async fn paginate_relations_with_filter(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let next_batch = match dir {
|
||||
| Direction::Forward => events.last(),
|
||||
| Direction::Backward => events.first(),
|
||||
// For threads, check if we should include the root event
|
||||
let mut root_event = None;
|
||||
if is_thread && dir == Direction::Backward {
|
||||
// Check if we've reached the beginning of the thread
|
||||
// (fewer events than requested means we've exhausted the thread)
|
||||
if events.len() < limit {
|
||||
// Try to get the thread root event
|
||||
if let Ok(root_pdu) = services.rooms.timeline.get_pdu(target).await {
|
||||
// Check visibility
|
||||
if services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, room_id, target)
|
||||
.await
|
||||
{
|
||||
// Store the root event to add to the response
|
||||
root_event = Some(root_pdu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.map(at!(0))
|
||||
.as_ref()
|
||||
.map(ToString::to_string);
|
||||
|
||||
// Determine if there are more events to fetch
|
||||
let has_more = if root_event.is_some() {
|
||||
false // We've included the root, no more events
|
||||
} else {
|
||||
// Check if we got a full page of results (might be more)
|
||||
events.len() >= limit
|
||||
};
|
||||
|
||||
let next_batch = if has_more {
|
||||
match dir {
|
||||
| Direction::Forward => events.last(),
|
||||
| Direction::Backward => events.first(),
|
||||
}
|
||||
.map(|(count, _)| count_to_token(*count))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Build the response chunk with thread root if needed
|
||||
let chunk: Vec<_> = root_event
|
||||
.into_iter()
|
||||
.map(Event::into_format)
|
||||
.chain(events.into_iter().map(at!(1)).map(Event::into_format))
|
||||
.collect();
|
||||
|
||||
Ok(get_relating_events::v1::Response {
|
||||
next_batch,
|
||||
prev_batch: from.map(Into::into),
|
||||
recursion_depth: recurse.then_some(depth.into()),
|
||||
chunk: events
|
||||
.into_iter()
|
||||
.map(at!(1))
|
||||
.map(Event::into_format)
|
||||
.collect(),
|
||||
chunk,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+156
-70
@@ -3,13 +3,14 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Error, Result, debug, err, info, utils,
|
||||
utils::{ReadyExt, hash},
|
||||
Err, Error, Result, debug, err, info,
|
||||
utils::{self, ReadyExt, hash},
|
||||
};
|
||||
use conduwuit_service::uiaa::SESSION_ID_LENGTH;
|
||||
use conduwuit_core::{debug_error, debug_warn};
|
||||
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
UserId,
|
||||
OwnedUserId, UserId,
|
||||
api::client::{
|
||||
session::{
|
||||
get_login_token,
|
||||
@@ -49,6 +50,154 @@ pub(crate) async fn get_login_types_route(
|
||||
]))
|
||||
}
|
||||
|
||||
/// Authenticates the given user by its ID and its password.
|
||||
///
|
||||
/// Returns the user ID if successful, and an error otherwise.
|
||||
#[tracing::instrument(skip_all, fields(%user_id), name = "password")]
|
||||
pub(crate) async fn password_login(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
lowercased_user_id: &UserId,
|
||||
password: &str,
|
||||
) -> Result<OwnedUserId> {
|
||||
// Restrict login to accounts only of type 'password', including untyped
|
||||
// legacy accounts which are equivalent to 'password'.
|
||||
if services
|
||||
.users
|
||||
.origin(user_id)
|
||||
.await
|
||||
.is_ok_and(|origin| origin != "password")
|
||||
{
|
||||
return Err!(Request(Forbidden("Account does not permit password login.")));
|
||||
}
|
||||
|
||||
let (hash, user_id) = match services.users.password_hash(user_id).await {
|
||||
| Ok(hash) => (hash, user_id),
|
||||
| Err(_) => services
|
||||
.users
|
||||
.password_hash(lowercased_user_id)
|
||||
.await
|
||||
.map(|hash| (hash, lowercased_user_id))
|
||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?,
|
||||
};
|
||||
|
||||
if hash.is_empty() {
|
||||
return Err!(Request(UserDeactivated("The user has been deactivated")));
|
||||
}
|
||||
|
||||
hash::verify_password(password, &hash)
|
||||
.inspect_err(|e| debug_error!("{e}"))
|
||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
||||
|
||||
Ok(user_id.to_owned())
|
||||
}
|
||||
|
||||
/// Authenticates the given user through the configured LDAP server.
|
||||
///
|
||||
/// Creates the user if the user is found in the LDAP and do not already have an
|
||||
/// account.
|
||||
#[tracing::instrument(skip_all, fields(%user_id), name = "ldap")]
|
||||
pub(super) async fn ldap_login(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
lowercased_user_id: &UserId,
|
||||
password: &str,
|
||||
) -> Result<OwnedUserId> {
|
||||
let (user_dn, is_ldap_admin) = match services.config.ldap.bind_dn.as_ref() {
|
||||
| Some(bind_dn) if bind_dn.contains("{username}") =>
|
||||
(bind_dn.replace("{username}", lowercased_user_id.localpart()), false),
|
||||
| _ => {
|
||||
debug!("Searching user in LDAP");
|
||||
|
||||
let dns = services.users.search_ldap(user_id).await?;
|
||||
if dns.len() >= 2 {
|
||||
return Err!(Ldap("LDAP search returned two or more results"));
|
||||
}
|
||||
|
||||
let Some((user_dn, is_admin)) = dns.first() else {
|
||||
return password_login(services, user_id, lowercased_user_id, password).await;
|
||||
};
|
||||
|
||||
(user_dn.clone(), *is_admin)
|
||||
},
|
||||
};
|
||||
|
||||
let user_id = services
|
||||
.users
|
||||
.auth_ldap(&user_dn, password)
|
||||
.await
|
||||
.map(|()| lowercased_user_id.to_owned())?;
|
||||
|
||||
// LDAP users are automatically created on first login attempt. This is a very
|
||||
// common feature that can be seen on many services using a LDAP provider for
|
||||
// their users (synapse, Nextcloud, Jellyfin, ...).
|
||||
//
|
||||
// LDAP users are crated with a dummy password but non empty because an empty
|
||||
// password is reserved for deactivated accounts. The conduwuit password field
|
||||
// will never be read to login a LDAP user so it's not an issue.
|
||||
if !services.users.exists(lowercased_user_id).await {
|
||||
services
|
||||
.users
|
||||
.create(lowercased_user_id, Some("*"), Some("ldap"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await;
|
||||
|
||||
if is_ldap_admin && !is_conduwuit_admin {
|
||||
services.admin.make_user_admin(lowercased_user_id).await?;
|
||||
} else if !is_ldap_admin && is_conduwuit_admin {
|
||||
services.admin.revoke_admin(lowercased_user_id).await?;
|
||||
}
|
||||
|
||||
Ok(user_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_login(
|
||||
services: &Services,
|
||||
body: &Ruma<login::v3::Request>,
|
||||
identifier: Option<&uiaa::UserIdentifier>,
|
||||
password: &str,
|
||||
user: Option<&String>,
|
||||
) -> Result<OwnedUserId> {
|
||||
debug!("Got password login type");
|
||||
let user_id =
|
||||
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
UserId::parse_with_server_name(user_id, &services.config.server_name)
|
||||
} else if let Some(user) = user {
|
||||
UserId::parse_with_server_name(user, &services.config.server_name)
|
||||
} else {
|
||||
return Err!(Request(Unknown(
|
||||
debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)")
|
||||
)));
|
||||
}
|
||||
.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
|
||||
|
||||
let lowercased_user_id = UserId::parse_with_server_name(
|
||||
user_id.localpart().to_lowercase(),
|
||||
&services.config.server_name,
|
||||
)?;
|
||||
|
||||
if !services.globals.user_is_local(&user_id)
|
||||
|| !services.globals.user_is_local(&lowercased_user_id)
|
||||
{
|
||||
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
|
||||
}
|
||||
|
||||
if cfg!(feature = "ldap") && services.config.ldap.enable {
|
||||
match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await {
|
||||
| Ok(user_id) => Ok(user_id),
|
||||
| Err(err) if services.config.ldap.ldap_only => Err(err),
|
||||
| Err(err) => {
|
||||
debug_warn!("{err}");
|
||||
password_login(services, &user_id, &lowercased_user_id, password).await
|
||||
},
|
||||
}
|
||||
} else {
|
||||
password_login(services, &user_id, &lowercased_user_id, password).await
|
||||
}
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/login`
|
||||
///
|
||||
/// Authenticates the user and returns an access token it can use in subsequent
|
||||
@@ -80,70 +229,7 @@ pub(crate) async fn login_route(
|
||||
password,
|
||||
user,
|
||||
..
|
||||
}) => {
|
||||
debug!("Got password login type");
|
||||
let user_id =
|
||||
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
UserId::parse_with_server_name(user_id, &services.config.server_name)
|
||||
} else if let Some(user) = user {
|
||||
UserId::parse_with_server_name(user, &services.config.server_name)
|
||||
} else {
|
||||
return Err!(Request(Unknown(
|
||||
debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)")
|
||||
)));
|
||||
}
|
||||
.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
|
||||
|
||||
let lowercased_user_id = UserId::parse_with_server_name(
|
||||
user_id.localpart().to_lowercase(),
|
||||
&services.config.server_name,
|
||||
)?;
|
||||
|
||||
if !services.globals.user_is_local(&user_id)
|
||||
|| !services.globals.user_is_local(&lowercased_user_id)
|
||||
{
|
||||
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
|
||||
}
|
||||
|
||||
// first try the username as-is
|
||||
let hash = services
|
||||
.users
|
||||
.password_hash(&user_id)
|
||||
.await
|
||||
.inspect_err(|e| debug!("{e}"));
|
||||
|
||||
match hash {
|
||||
| Ok(hash) => {
|
||||
if hash.is_empty() {
|
||||
return Err!(Request(UserDeactivated("The user has been deactivated")));
|
||||
}
|
||||
|
||||
hash::verify_password(password, &hash)
|
||||
.inspect_err(|e| debug!("{e}"))
|
||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(_e) => {
|
||||
let hash_lowercased_user_id = services
|
||||
.users
|
||||
.password_hash(&lowercased_user_id)
|
||||
.await
|
||||
.inspect_err(|e| debug!("{e}"))
|
||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
||||
|
||||
if hash_lowercased_user_id.is_empty() {
|
||||
return Err!(Request(UserDeactivated("The user has been deactivated")));
|
||||
}
|
||||
|
||||
hash::verify_password(password, &hash_lowercased_user_id)
|
||||
.inspect_err(|e| debug!("{e}"))
|
||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
||||
|
||||
lowercased_user_id
|
||||
},
|
||||
}
|
||||
},
|
||||
}) => handle_login(&services, &body, identifier.as_ref(), password, user.as_ref()).await?,
|
||||
| login::v3::LoginInfo::Token(login::v3::Token { token }) => {
|
||||
debug!("Got token login type");
|
||||
if !services.server.config.login_via_existing_session {
|
||||
@@ -198,8 +284,8 @@ pub(crate) async fn login_route(
|
||||
.clone()
|
||||
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
||||
|
||||
// Generate a new token for the device
|
||||
let token = utils::random_string(TOKEN_LENGTH);
|
||||
// Generate a new token for the device (ensuring no collisions)
|
||||
let token = services.users.generate_unique_token().await;
|
||||
|
||||
// Determine if device_id was provided and exists in the db for this user
|
||||
let device_exists = if body.device_id.is_some() {
|
||||
|
||||
@@ -430,7 +430,7 @@ async fn handle_left_room(
|
||||
.ok();
|
||||
|
||||
// Left before last sync
|
||||
if Some(since) >= left_count {
|
||||
if (Some(since) >= left_count && !include_leave) || Some(next_batch) < left_count {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
type TodoRooms = BTreeMap<OwnedRoomId, (BTreeSet<TypeStateKey>, usize, u64)>;
|
||||
const SINGLE_CONNECTION_SYNC: &str = "single_connection_sync";
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
/// POST `/_matrix/client/unstable/org.matrix.msc3575/sync`
|
||||
///
|
||||
/// Sliding Sync endpoint (future endpoint: `/_matrix/client/v4/sync`)
|
||||
|
||||
@@ -292,7 +292,7 @@ pub(crate) async fn get_timezone_key_route(
|
||||
.await
|
||||
{
|
||||
if !services.users.exists(&body.user_id).await {
|
||||
services.users.create(&body.user_id, None)?;
|
||||
services.users.create(&body.user_id, None, None).await?;
|
||||
}
|
||||
|
||||
services
|
||||
@@ -352,7 +352,7 @@ pub(crate) async fn get_profile_key_route(
|
||||
.await
|
||||
{
|
||||
if !services.users.exists(&body.user_id).await {
|
||||
services.users.create(&body.user_id, None)?;
|
||||
services.users.create(&body.user_id, None, None).await?;
|
||||
}
|
||||
|
||||
services
|
||||
|
||||
@@ -58,6 +58,7 @@ pub(crate) async fn get_supported_versions_route(
|
||||
("uk.tcpip.msc4133".to_owned(), true), /* Extending User Profile API with Key:Value Pairs (https://github.com/matrix-org/matrix-spec-proposals/pull/4133) */
|
||||
("us.cloke.msc4175".to_owned(), true), /* Profile field for user time zone (https://github.com/matrix-org/matrix-spec-proposals/pull/4175) */
|
||||
("org.matrix.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
|
||||
("uk.timedout.msc4323".to_owned(), true), /* agnostic suspend (https://github.com/matrix-org/matrix-spec-proposals/pull/4323) */
|
||||
]),
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
use conduwuit::{
|
||||
Result, err,
|
||||
matrix::pdu::{PduCount, ShortEventId},
|
||||
};
|
||||
|
||||
/// Parse a pagination token, trying ShortEventId first, then falling back to
|
||||
/// PduCount
|
||||
pub(crate) fn parse_pagination_token(token: &str) -> Result<PduCount> {
|
||||
// Try parsing as ShortEventId first
|
||||
if let Ok(shorteventid) = token.parse::<ShortEventId>() {
|
||||
// ShortEventId maps directly to a PduCount in our database
|
||||
Ok(PduCount::Normal(shorteventid))
|
||||
} else if let Ok(count) = token.parse::<u64>() {
|
||||
// Fallback to PduCount for backwards compatibility
|
||||
Ok(PduCount::Normal(count))
|
||||
} else if let Ok(count) = token.parse::<i64>() {
|
||||
// Also handle negative counts for backfilled events
|
||||
Ok(PduCount::from_signed(count))
|
||||
} else {
|
||||
Err(err!(Request(InvalidParam("Invalid pagination token"))))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a PduCount to a token string (using the underlying ShortEventId)
|
||||
pub(crate) fn count_to_token(count: PduCount) -> String {
|
||||
// The PduCount's unsigned value IS the ShortEventId
|
||||
count.into_unsigned().to_string()
|
||||
}
|
||||
@@ -184,6 +184,8 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
"/_matrix/client/unstable/im.nheko.summary/rooms/:room_id_or_alias/summary",
|
||||
get(client::get_room_summary_legacy)
|
||||
)
|
||||
.ruma_route(&client::get_suspended_status)
|
||||
.ruma_route(&client::put_suspended_status)
|
||||
.ruma_route(&client::well_known_support)
|
||||
.ruma_route(&client::well_known_client)
|
||||
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||
|
||||
+31
-11
@@ -5,6 +5,14 @@
|
||||
typed_header::TypedHeaderRejectionReason,
|
||||
};
|
||||
use conduwuit::{Err, Error, Result, debug_error, err, warn};
|
||||
use futures::{
|
||||
TryFutureExt,
|
||||
future::{
|
||||
Either::{Left, Right},
|
||||
select_ok,
|
||||
},
|
||||
pin_mut,
|
||||
};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
|
||||
api::{
|
||||
@@ -54,17 +62,7 @@ pub(super) async fn auth(
|
||||
| None => request.query.access_token.as_deref(),
|
||||
};
|
||||
|
||||
let token = if let Some(token) = token {
|
||||
match services.appservice.find_from_token(token).await {
|
||||
| Some(reg_info) => Token::Appservice(Box::new(reg_info)),
|
||||
| _ => match services.users.find_from_token(token).await {
|
||||
| Ok((user_id, device_id)) => Token::User((user_id, device_id)),
|
||||
| _ => Token::Invalid,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Token::None
|
||||
};
|
||||
let token = find_token(services, token).await?;
|
||||
|
||||
if metadata.authentication == AuthScheme::None {
|
||||
match metadata {
|
||||
@@ -342,3 +340,25 @@ async fn parse_x_matrix(request: &mut Request) -> Result<XMatrix> {
|
||||
|
||||
Ok(x_matrix)
|
||||
}
|
||||
|
||||
async fn find_token(services: &Services, token: Option<&str>) -> Result<Token> {
|
||||
let Some(token) = token else {
|
||||
return Ok(Token::None);
|
||||
};
|
||||
|
||||
let user_token = services.users.find_from_token(token).map_ok(Token::User);
|
||||
|
||||
let appservice_token = services
|
||||
.appservice
|
||||
.find_from_token(token)
|
||||
.map_ok(Box::new)
|
||||
.map_ok(Token::Appservice);
|
||||
|
||||
pin_mut!(user_token, appservice_token);
|
||||
// Returns Ok if either token type succeeds, Err only if both fail
|
||||
match select_ok([Left(user_token), Right(appservice_token)]).await {
|
||||
| Err(e) if !e.is_not_found() => Err(e),
|
||||
| Ok((token, _)) => Ok(token),
|
||||
| _ => Ok(Token::Invalid),
|
||||
}
|
||||
}
|
||||
|
||||
+123
-2
@@ -1,3 +1,4 @@
|
||||
#![allow(clippy::doc_link_with_quotes)]
|
||||
pub mod check;
|
||||
pub mod manager;
|
||||
pub mod proxy;
|
||||
@@ -125,9 +126,11 @@ pub struct Config {
|
||||
/// This is the only directory where continuwuity will save its data,
|
||||
/// including media. Note: this was previously "/var/lib/matrix-conduit".
|
||||
///
|
||||
/// YOU NEED TO EDIT THIS.
|
||||
/// YOU NEED TO EDIT THIS, UNLESS you are running continuwuity as a `systemd` service.
|
||||
/// The service file sets it to `/var/lib/conduwuit` using an environment variable
|
||||
/// and also grants write access.
|
||||
///
|
||||
/// example: "/var/lib/continuwuity"
|
||||
/// example: "/var/lib/conduwuit"
|
||||
pub database_path: PathBuf,
|
||||
|
||||
/// continuwuity supports online database backups using RocksDB's Backup
|
||||
@@ -1947,6 +1950,10 @@ pub struct Config {
|
||||
pub allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure:
|
||||
bool,
|
||||
|
||||
// external structure; separate section
|
||||
#[serde(default)]
|
||||
pub ldap: LdapConfig,
|
||||
|
||||
// external structure; separate section
|
||||
#[serde(default)]
|
||||
pub blurhashing: BlurhashConfig,
|
||||
@@ -2041,6 +2048,114 @@ pub struct BlurhashConfig {
|
||||
pub blurhash_max_raw_size: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.ldap")]
|
||||
pub struct LdapConfig {
|
||||
/// Whether to enable LDAP login.
|
||||
///
|
||||
/// example: "true"
|
||||
#[serde(default)]
|
||||
pub enable: bool,
|
||||
|
||||
/// Whether to force LDAP authentication or authorize classical password
|
||||
/// login.
|
||||
///
|
||||
/// example: "true"
|
||||
#[serde(default)]
|
||||
pub ldap_only: bool,
|
||||
|
||||
/// URI of the LDAP server.
|
||||
///
|
||||
/// example: "ldap://ldap.example.com:389"
|
||||
///
|
||||
/// default: ""
|
||||
#[serde(default)]
|
||||
pub uri: Option<Url>,
|
||||
|
||||
/// Root of the searches.
|
||||
///
|
||||
/// example: "ou=users,dc=example,dc=org"
|
||||
///
|
||||
/// default: ""
|
||||
#[serde(default)]
|
||||
pub base_dn: String,
|
||||
|
||||
/// Bind DN if anonymous search is not enabled.
|
||||
///
|
||||
/// You can use the variable `{username}` that will be replaced by the
|
||||
/// entered username. In such case, the password used to bind will be the
|
||||
/// one provided for the login and not the one given by
|
||||
/// `bind_password_file`. Beware: automatically granting admin rights will
|
||||
/// not work if you use this direct bind instead of a LDAP search.
|
||||
///
|
||||
/// example: "cn=ldap-reader,dc=example,dc=org" or
|
||||
/// "cn={username},ou=users,dc=example,dc=org"
|
||||
///
|
||||
/// default: ""
|
||||
#[serde(default)]
|
||||
pub bind_dn: Option<String>,
|
||||
|
||||
/// Path to a file on the system that contains the password for the
|
||||
/// `bind_dn`.
|
||||
///
|
||||
/// The server must be able to access the file, and it must not be empty.
|
||||
///
|
||||
/// default: ""
|
||||
#[serde(default)]
|
||||
pub bind_password_file: Option<PathBuf>,
|
||||
|
||||
/// Search filter to limit user searches.
|
||||
///
|
||||
/// You can use the variable `{username}` that will be replaced by the
|
||||
/// entered username for more complex filters.
|
||||
///
|
||||
/// example: "(&(objectClass=person)(memberOf=matrix))"
|
||||
///
|
||||
/// default: "(objectClass=*)"
|
||||
#[serde(default = "default_ldap_search_filter")]
|
||||
pub filter: String,
|
||||
|
||||
/// Attribute to use to uniquely identify the user.
|
||||
///
|
||||
/// example: "uid" or "cn"
|
||||
///
|
||||
/// default: "uid"
|
||||
#[serde(default = "default_ldap_uid_attribute")]
|
||||
pub uid_attribute: String,
|
||||
|
||||
/// Attribute containing the display name of the user.
|
||||
///
|
||||
/// example: "givenName" or "sn"
|
||||
///
|
||||
/// default: "givenName"
|
||||
#[serde(default = "default_ldap_name_attribute")]
|
||||
pub name_attribute: String,
|
||||
|
||||
/// Root of the searches for admin users.
|
||||
///
|
||||
/// Defaults to `base_dn` if empty.
|
||||
///
|
||||
/// example: "ou=admins,dc=example,dc=org"
|
||||
///
|
||||
/// default: ""
|
||||
#[serde(default)]
|
||||
pub admin_base_dn: String,
|
||||
|
||||
/// The LDAP search filter to find administrative users for continuwuity.
|
||||
///
|
||||
/// If left blank, administrative state must be configured manually for each
|
||||
/// user.
|
||||
///
|
||||
/// You can use the variable `{username}` that will be replaced by the
|
||||
/// entered username for more complex filters.
|
||||
///
|
||||
/// example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
|
||||
///
|
||||
/// default: ""
|
||||
#[serde(default)]
|
||||
pub admin_filter: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(transparent)]
|
||||
struct ListeningPort {
|
||||
@@ -2430,3 +2545,9 @@ pub(super) fn default_blurhash_x_component() -> u32 { 4 }
|
||||
pub(super) fn default_blurhash_y_component() -> u32 { 3 }
|
||||
|
||||
// end recommended & blurhashing defaults
|
||||
|
||||
fn default_ldap_search_filter() -> String { "(objectClass=*)".to_owned() }
|
||||
|
||||
fn default_ldap_uid_attribute() -> String { String::from("uid") }
|
||||
|
||||
fn default_ldap_name_attribute() -> String { String::from("givenName") }
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ pub fn trap() {
|
||||
|
||||
#[must_use]
|
||||
pub fn panic_str(p: &Box<dyn Any + Send>) -> &'static str {
|
||||
p.downcast_ref::<&str>().copied().unwrap_or_default()
|
||||
(**p).downcast_ref::<&str>().copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
||||
@@ -110,6 +110,8 @@ pub enum Error {
|
||||
InconsistentRoomState(&'static str, ruma::OwnedRoomId),
|
||||
#[error(transparent)]
|
||||
IntoHttp(#[from] ruma::api::error::IntoHttpError),
|
||||
#[error("{0}")]
|
||||
Ldap(Cow<'static, str>),
|
||||
#[error(transparent)]
|
||||
Mxc(#[from] ruma::MxcUriError),
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
/// Experimental, partially supported room versions
|
||||
pub const UNSTABLE_ROOM_VERSIONS: &[RoomVersionId] =
|
||||
&[RoomVersionId::V2, RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5];
|
||||
&[RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5];
|
||||
|
||||
type RoomVersion = (RoomVersionId, RoomVersionStability);
|
||||
|
||||
|
||||
@@ -44,6 +44,14 @@ zstd_compression = [
|
||||
"conduwuit-core/zstd_compression",
|
||||
"rust-rocksdb/zstd",
|
||||
]
|
||||
bindgen-static = [
|
||||
# "bindgen/static"
|
||||
# "clang-sys/static"
|
||||
"rust-rocksdb/bindgen-static"
|
||||
]
|
||||
bindgen-runtime = [
|
||||
"rust-rocksdb/bindgen-runtime"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
async-channel.workspace = true
|
||||
|
||||
@@ -19,7 +19,7 @@ pub trait Get<'a, K, S>
|
||||
S: Stream<Item = K> + Send + 'a,
|
||||
K: AsRef<[u8]> + Send + Sync + 'a,
|
||||
{
|
||||
fn get(self, map: &'a Arc<super::Map>) -> impl Stream<Item = Result<Handle<'_>>> + Send + 'a;
|
||||
fn get(self, map: &'a Arc<super::Map>) -> impl Stream<Item = Result<Handle<'a>>> + Send + 'a;
|
||||
}
|
||||
|
||||
impl<'a, K, S> Get<'a, K, S> for S
|
||||
@@ -29,7 +29,7 @@ impl<'a, K, S> Get<'a, K, S> for S
|
||||
K: AsRef<[u8]> + Send + Sync + 'a,
|
||||
{
|
||||
#[inline]
|
||||
fn get(self, map: &'a Arc<super::Map>) -> impl Stream<Item = Result<Handle<'_>>> + Send + 'a {
|
||||
fn get(self, map: &'a Arc<super::Map>) -> impl Stream<Item = Result<Handle<'a>>> + Send + 'a {
|
||||
map.get_batch(self)
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ fn get(self, map: &'a Arc<super::Map>) -> impl Stream<Item = Result<Handle<'_>>>
|
||||
pub(crate) fn get_batch<'a, S, K>(
|
||||
self: &'a Arc<Self>,
|
||||
keys: S,
|
||||
) -> impl Stream<Item = Result<Handle<'_>>> + Send + 'a
|
||||
) -> impl Stream<Item = Result<Handle<'a>>> + Send + 'a
|
||||
where
|
||||
S: Stream<Item = K> + Send + 'a,
|
||||
K: AsRef<[u8]> + Send + Sync + 'a,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
use crate::{keyval, keyval::Key, stream};
|
||||
|
||||
#[implement(super::Map)]
|
||||
pub fn keys<'a, K>(self: &'a Arc<Self>) -> impl Stream<Item = Result<Key<'_, K>>> + Send
|
||||
pub fn keys<'a, K>(self: &'a Arc<Self>) -> impl Stream<Item = Result<Key<'a, K>>> + Send
|
||||
where
|
||||
K: Deserialize<'a> + Send,
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
pub fn keys_from<'a, K, P>(
|
||||
self: &'a Arc<Self>,
|
||||
from: &P,
|
||||
) -> impl Stream<Item = Result<Key<'_, K>>> + Send + use<'a, K, P>
|
||||
) -> impl Stream<Item = Result<Key<'a, K>>> + Send + use<'a, K, P>
|
||||
where
|
||||
P: Serialize + ?Sized + Debug,
|
||||
K: Deserialize<'a> + Send,
|
||||
@@ -40,7 +40,7 @@ pub fn keys_from_raw<P>(
|
||||
pub fn keys_raw_from<'a, K, P>(
|
||||
self: &'a Arc<Self>,
|
||||
from: &P,
|
||||
) -> impl Stream<Item = Result<Key<'_, K>>> + Send + use<'a, K, P>
|
||||
) -> impl Stream<Item = Result<Key<'a, K>>> + Send + use<'a, K, P>
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync,
|
||||
K: Deserialize<'a> + Send,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
pub fn keys_prefix<'a, K, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &P,
|
||||
) -> impl Stream<Item = Result<Key<'_, K>>> + Send + use<'a, K, P>
|
||||
) -> impl Stream<Item = Result<Key<'a, K>>> + Send + use<'a, K, P>
|
||||
where
|
||||
P: Serialize + ?Sized + Debug,
|
||||
K: Deserialize<'a> + Send,
|
||||
@@ -37,7 +37,7 @@ pub fn keys_prefix_raw<P>(
|
||||
pub fn keys_raw_prefix<'a, K, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &'a P,
|
||||
) -> impl Stream<Item = Result<Key<'_, K>>> + Send + 'a
|
||||
) -> impl Stream<Item = Result<Key<'a, K>>> + Send + 'a
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync + 'a,
|
||||
K: Deserialize<'a> + Send + 'a,
|
||||
@@ -50,7 +50,7 @@ pub fn keys_raw_prefix<'a, K, P>(
|
||||
pub fn raw_keys_prefix<'a, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &'a P,
|
||||
) -> impl Stream<Item = Result<Key<'_>>> + Send + 'a
|
||||
) -> impl Stream<Item = Result<Key<'a>>> + Send + 'a
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync + 'a,
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ pub trait Qry<'a, K, S>
|
||||
S: Stream<Item = K> + Send + 'a,
|
||||
K: Serialize + Debug,
|
||||
{
|
||||
fn qry(self, map: &'a Arc<super::Map>) -> impl Stream<Item = Result<Handle<'_>>> + Send + 'a;
|
||||
fn qry(self, map: &'a Arc<super::Map>) -> impl Stream<Item = Result<Handle<'a>>> + Send + 'a;
|
||||
}
|
||||
|
||||
impl<'a, K, S> Qry<'a, K, S> for S
|
||||
@@ -27,7 +27,7 @@ impl<'a, K, S> Qry<'a, K, S> for S
|
||||
K: Serialize + Debug + 'a,
|
||||
{
|
||||
#[inline]
|
||||
fn qry(self, map: &'a Arc<super::Map>) -> impl Stream<Item = Result<Handle<'_>>> + Send + 'a {
|
||||
fn qry(self, map: &'a Arc<super::Map>) -> impl Stream<Item = Result<Handle<'a>>> + Send + 'a {
|
||||
map.qry_batch(self)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ fn qry(self, map: &'a Arc<super::Map>) -> impl Stream<Item = Result<Handle<'_>>>
|
||||
pub(crate) fn qry_batch<'a, S, K>(
|
||||
self: &'a Arc<Self>,
|
||||
keys: S,
|
||||
) -> impl Stream<Item = Result<Handle<'_>>> + Send + 'a
|
||||
) -> impl Stream<Item = Result<Handle<'a>>> + Send + 'a
|
||||
where
|
||||
S: Stream<Item = K> + Send + 'a,
|
||||
K: Serialize + Debug + 'a,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
use crate::{keyval, keyval::Key, stream};
|
||||
|
||||
#[implement(super::Map)]
|
||||
pub fn rev_keys<'a, K>(self: &'a Arc<Self>) -> impl Stream<Item = Result<Key<'_, K>>> + Send
|
||||
pub fn rev_keys<'a, K>(self: &'a Arc<Self>) -> impl Stream<Item = Result<Key<'a, K>>> + Send
|
||||
where
|
||||
K: Deserialize<'a> + Send,
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
pub fn rev_keys_from<'a, K, P>(
|
||||
self: &'a Arc<Self>,
|
||||
from: &P,
|
||||
) -> impl Stream<Item = Result<Key<'_, K>>> + Send + use<'a, K, P>
|
||||
) -> impl Stream<Item = Result<Key<'a, K>>> + Send + use<'a, K, P>
|
||||
where
|
||||
P: Serialize + ?Sized + Debug,
|
||||
K: Deserialize<'a> + Send,
|
||||
@@ -41,7 +41,7 @@ pub fn rev_keys_from_raw<P>(
|
||||
pub fn rev_keys_raw_from<'a, K, P>(
|
||||
self: &'a Arc<Self>,
|
||||
from: &P,
|
||||
) -> impl Stream<Item = Result<Key<'_, K>>> + Send + use<'a, K, P>
|
||||
) -> impl Stream<Item = Result<Key<'a, K>>> + Send + use<'a, K, P>
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync,
|
||||
K: Deserialize<'a> + Send,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
pub fn rev_keys_prefix<'a, K, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &P,
|
||||
) -> impl Stream<Item = Result<Key<'_, K>>> + Send + use<'a, K, P>
|
||||
) -> impl Stream<Item = Result<Key<'a, K>>> + Send + use<'a, K, P>
|
||||
where
|
||||
P: Serialize + ?Sized + Debug,
|
||||
K: Deserialize<'a> + Send,
|
||||
@@ -37,7 +37,7 @@ pub fn rev_keys_prefix_raw<P>(
|
||||
pub fn rev_keys_raw_prefix<'a, K, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &'a P,
|
||||
) -> impl Stream<Item = Result<Key<'_, K>>> + Send + 'a
|
||||
) -> impl Stream<Item = Result<Key<'a, K>>> + Send + 'a
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync + 'a,
|
||||
K: Deserialize<'a> + Send + 'a,
|
||||
@@ -50,7 +50,7 @@ pub fn rev_keys_raw_prefix<'a, K, P>(
|
||||
pub fn rev_raw_keys_prefix<'a, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &'a P,
|
||||
) -> impl Stream<Item = Result<Key<'_>>> + Send + 'a
|
||||
) -> impl Stream<Item = Result<Key<'a>>> + Send + 'a
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync + 'a,
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#[implement(super::Map)]
|
||||
pub fn rev_stream<'a, K, V>(
|
||||
self: &'a Arc<Self>,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_, K, V>>> + Send
|
||||
) -> impl Stream<Item = Result<KeyVal<'a, K, V>>> + Send
|
||||
where
|
||||
K: Deserialize<'a> + Send,
|
||||
V: Deserialize<'a> + Send,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
pub fn rev_stream_from<'a, K, V, P>(
|
||||
self: &'a Arc<Self>,
|
||||
from: &P,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_, K, V>>> + Send + use<'a, K, V, P>
|
||||
) -> impl Stream<Item = Result<KeyVal<'a, K, V>>> + Send + use<'a, K, V, P>
|
||||
where
|
||||
P: Serialize + ?Sized + Debug,
|
||||
K: Deserialize<'a> + Send,
|
||||
@@ -55,7 +55,7 @@ pub fn rev_stream_from_raw<P>(
|
||||
pub fn rev_stream_raw_from<'a, K, V, P>(
|
||||
self: &'a Arc<Self>,
|
||||
from: &P,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_, K, V>>> + Send + use<'a, K, V, P>
|
||||
) -> impl Stream<Item = Result<KeyVal<'a, K, V>>> + Send + use<'a, K, V, P>
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync,
|
||||
K: Deserialize<'a> + Send,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
pub fn rev_stream_prefix<'a, K, V, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &P,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_, K, V>>> + Send + use<'a, K, V, P>
|
||||
) -> impl Stream<Item = Result<KeyVal<'a, K, V>>> + Send + use<'a, K, V, P>
|
||||
where
|
||||
P: Serialize + ?Sized + Debug,
|
||||
K: Deserialize<'a> + Send,
|
||||
@@ -50,7 +50,7 @@ pub fn rev_stream_prefix_raw<P>(
|
||||
pub fn rev_stream_raw_prefix<'a, K, V, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &'a P,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_, K, V>>> + Send + 'a
|
||||
) -> impl Stream<Item = Result<KeyVal<'a, K, V>>> + Send + 'a
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync + 'a,
|
||||
K: Deserialize<'a> + Send + 'a,
|
||||
@@ -68,7 +68,7 @@ pub fn rev_stream_raw_prefix<'a, K, V, P>(
|
||||
pub fn rev_raw_stream_prefix<'a, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &'a P,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_>>> + Send + 'a
|
||||
) -> impl Stream<Item = Result<KeyVal<'a>>> + Send + 'a
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync + 'a,
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#[implement(super::Map)]
|
||||
pub fn stream<'a, K, V>(
|
||||
self: &'a Arc<Self>,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_, K, V>>> + Send
|
||||
) -> impl Stream<Item = Result<KeyVal<'a, K, V>>> + Send
|
||||
where
|
||||
K: Deserialize<'a> + Send,
|
||||
V: Deserialize<'a> + Send,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
pub fn stream_from<'a, K, V, P>(
|
||||
self: &'a Arc<Self>,
|
||||
from: &P,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_, K, V>>> + Send + use<'a, K, V, P>
|
||||
) -> impl Stream<Item = Result<KeyVal<'a, K, V>>> + Send + use<'a, K, V, P>
|
||||
where
|
||||
P: Serialize + ?Sized + Debug,
|
||||
K: Deserialize<'a> + Send,
|
||||
@@ -53,7 +53,7 @@ pub fn stream_from_raw<P>(
|
||||
pub fn stream_raw_from<'a, K, V, P>(
|
||||
self: &'a Arc<Self>,
|
||||
from: &P,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_, K, V>>> + Send + use<'a, K, V, P>
|
||||
) -> impl Stream<Item = Result<KeyVal<'a, K, V>>> + Send + use<'a, K, V, P>
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync,
|
||||
K: Deserialize<'a> + Send,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
pub fn stream_prefix<'a, K, V, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &P,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_, K, V>>> + Send + use<'a, K, V, P>
|
||||
) -> impl Stream<Item = Result<KeyVal<'a, K, V>>> + Send + use<'a, K, V, P>
|
||||
where
|
||||
P: Serialize + ?Sized + Debug,
|
||||
K: Deserialize<'a> + Send,
|
||||
@@ -50,7 +50,7 @@ pub fn stream_prefix_raw<P>(
|
||||
pub fn stream_raw_prefix<'a, K, V, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &'a P,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_, K, V>>> + Send + 'a
|
||||
) -> impl Stream<Item = Result<KeyVal<'a, K, V>>> + Send + 'a
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync + 'a,
|
||||
K: Deserialize<'a> + Send + 'a,
|
||||
@@ -68,7 +68,7 @@ pub fn stream_raw_prefix<'a, K, V, P>(
|
||||
pub fn raw_stream_prefix<'a, P>(
|
||||
self: &'a Arc<Self>,
|
||||
prefix: &'a P,
|
||||
) -> impl Stream<Item = Result<KeyVal<'_>>> + Send + 'a
|
||||
) -> impl Stream<Item = Result<KeyVal<'a>>> + Send + 'a
|
||||
where
|
||||
P: AsRef<[u8]> + ?Sized + Debug + Sync + 'a,
|
||||
{
|
||||
|
||||
@@ -374,6 +374,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
|
||||
name: "userid_masterkeyid",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "userid_origin",
|
||||
..descriptor::RANDOM
|
||||
},
|
||||
Descriptor {
|
||||
name: "userid_password",
|
||||
..descriptor::RANDOM
|
||||
|
||||
@@ -443,7 +443,7 @@ pub(crate) fn into_send_seek(result: stream::State<'_>) -> stream::State<'static
|
||||
unsafe { std::mem::transmute(result) }
|
||||
}
|
||||
|
||||
fn into_recv_seek(result: stream::State<'static>) -> stream::State<'_> {
|
||||
fn into_recv_seek(result: stream::State<'static>) -> stream::State<'static> {
|
||||
// SAFETY: This is to receive the State from the channel; see above.
|
||||
unsafe { std::mem::transmute(result) }
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ fn ser_array() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
#[ignore = "arrayvec deserialization is not implemented (separators)"]
|
||||
fn de_array() {
|
||||
let a: u64 = 123_456;
|
||||
let b: u64 = 987_654;
|
||||
@@ -358,7 +358,7 @@ fn de_array() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
#[ignore = "Nested sequences are not supported"]
|
||||
fn de_complex() {
|
||||
type Key<'a> = (&'a UserId, ArrayVec<u64, 2>, &'a RoomId);
|
||||
|
||||
|
||||
+30
-2
@@ -43,6 +43,11 @@ assets = [
|
||||
|
||||
[features]
|
||||
default = [
|
||||
"standard",
|
||||
"release_max_log_level",
|
||||
"bindgen-runtime", # replace with bindgen-static on alpine
|
||||
]
|
||||
standard = [
|
||||
"blurhashing",
|
||||
"brotli_compression",
|
||||
"element_hacks",
|
||||
@@ -51,11 +56,19 @@ default = [
|
||||
"jemalloc",
|
||||
"jemalloc_conf",
|
||||
"journald",
|
||||
"ldap",
|
||||
"media_thumbnail",
|
||||
"release_max_log_level",
|
||||
"systemd",
|
||||
"url_preview",
|
||||
"zstd_compression",
|
||||
"zstd_compression"
|
||||
]
|
||||
full = [
|
||||
"standard",
|
||||
# "hardened_malloc", # Conflicts with jemalloc
|
||||
"jemalloc_prof",
|
||||
"perf_measurements",
|
||||
"tokio_console"
|
||||
# sentry_telemetry
|
||||
]
|
||||
|
||||
blurhashing = [
|
||||
@@ -102,6 +115,9 @@ jemalloc_stats = [
|
||||
jemalloc_conf = [
|
||||
"conduwuit-core/jemalloc_conf",
|
||||
]
|
||||
ldap = [
|
||||
"conduwuit-api/ldap",
|
||||
]
|
||||
media_thumbnail = [
|
||||
"conduwuit-service/media_thumbnail",
|
||||
]
|
||||
@@ -161,6 +177,18 @@ zstd_compression = [
|
||||
conduwuit_mods = [
|
||||
"conduwuit-core/conduwuit_mods",
|
||||
]
|
||||
bindgen-static = [
|
||||
# "bindgen/static"
|
||||
# "clang-sys/static"
|
||||
"conduwuit-database/bindgen-static"
|
||||
]
|
||||
bindgen-runtime = [
|
||||
"conduwuit-database/bindgen-runtime"
|
||||
]
|
||||
|
||||
[build-dependencies]
|
||||
# bindgen = {version = "0.71.1", default-features = false}
|
||||
# clang-sys = {version = "1", default-features = false}
|
||||
|
||||
[dependencies]
|
||||
conduwuit-admin.workspace = true
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
type MakeService = IntoMakeServiceWithConnectInfo<Router, net::SocketAddr>;
|
||||
|
||||
const NULL_ADDR: net::SocketAddr = net::SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0);
|
||||
const NULL_ADDR: net::SocketAddr = net::SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0);
|
||||
const FINI_POLL_INTERVAL: Duration = Duration::from_millis(750);
|
||||
|
||||
#[tracing::instrument(skip_all, level = "debug")]
|
||||
|
||||
@@ -53,6 +53,9 @@ jemalloc_stats = [
|
||||
"conduwuit-core/jemalloc_stats",
|
||||
"conduwuit-database/jemalloc_stats",
|
||||
]
|
||||
ldap = [
|
||||
"dep:ldap3"
|
||||
]
|
||||
media_thumbnail = [
|
||||
"dep:image",
|
||||
]
|
||||
@@ -89,6 +92,8 @@ image.workspace = true
|
||||
image.optional = true
|
||||
ipaddress.workspace = true
|
||||
itertools.workspace = true
|
||||
ldap3.workspace = true
|
||||
ldap3.optional = true
|
||||
log.workspace = true
|
||||
loole.workspace = true
|
||||
lru-cache.workspace = true
|
||||
|
||||
@@ -38,7 +38,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
|
||||
|
||||
// Create a user for the server
|
||||
let server_user = services.globals.server_user.as_ref();
|
||||
services.users.create(server_user, None)?;
|
||||
services.users.create(server_user, None, None).await?;
|
||||
|
||||
let create_content = {
|
||||
use RoomVersionId::*;
|
||||
|
||||
+117
-16
@@ -4,14 +4,14 @@
|
||||
use std::{collections::BTreeMap, iter::IntoIterator, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use conduwuit::{Result, err, utils::stream::IterStream};
|
||||
use conduwuit::{Err, Result, err, utils::stream::IterStream};
|
||||
use database::Map;
|
||||
use futures::{Future, FutureExt, Stream, TryStreamExt};
|
||||
use ruma::{RoomAliasId, RoomId, UserId, api::appservice::Registration};
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
|
||||
pub use self::{namespace_regex::NamespaceRegex, registration_info::RegistrationInfo};
|
||||
use crate::{Dep, sending};
|
||||
use crate::{Dep, globals, sending, users};
|
||||
|
||||
pub struct Service {
|
||||
registration_info: RwLock<Registrations>,
|
||||
@@ -20,7 +20,9 @@ pub struct Service {
|
||||
}
|
||||
|
||||
struct Services {
|
||||
globals: Dep<globals::Service>,
|
||||
sending: Dep<sending::Service>,
|
||||
users: Dep<users::Service>,
|
||||
}
|
||||
|
||||
struct Data {
|
||||
@@ -35,7 +37,9 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
Ok(Arc::new(Self {
|
||||
registration_info: RwLock::new(BTreeMap::new()),
|
||||
services: Services {
|
||||
globals: args.depend::<globals::Service>("globals"),
|
||||
sending: args.depend::<sending::Service>("sending"),
|
||||
users: args.depend::<users::Service>("users"),
|
||||
},
|
||||
db: Data {
|
||||
id_appserviceregistrations: args.db["id_appserviceregistrations"].clone(),
|
||||
@@ -44,23 +48,93 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
}
|
||||
|
||||
async fn worker(self: Arc<Self>) -> Result {
|
||||
// Inserting registrations into cache
|
||||
self.iter_db_ids()
|
||||
.try_for_each(async |appservice| {
|
||||
self.registration_info
|
||||
.write()
|
||||
.await
|
||||
.insert(appservice.0, appservice.1.try_into()?);
|
||||
// First, collect all appservices to check for token conflicts
|
||||
let appservices: Vec<(String, Registration)> = self.iter_db_ids().try_collect().await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
// Check for appservice-to-appservice token conflicts
|
||||
for i in 0..appservices.len() {
|
||||
for j in i.saturating_add(1)..appservices.len() {
|
||||
if appservices[i].1.as_token == appservices[j].1.as_token {
|
||||
return Err!(Database(error!(
|
||||
"Token collision detected: Appservices '{}' and '{}' have the same token",
|
||||
appservices[i].0, appservices[j].0
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process each appservice
|
||||
for (id, registration) in appservices {
|
||||
// During startup, resolve any token collisions in favour of appservices
|
||||
// by logging out conflicting user devices
|
||||
if let Ok((user_id, device_id)) = self
|
||||
.services
|
||||
.users
|
||||
.find_from_token(®istration.as_token)
|
||||
.await
|
||||
{
|
||||
conduwuit::warn!(
|
||||
"Token collision detected during startup: Appservice '{}' token was also \
|
||||
used by user '{}' device '{}'. Logging out the user device to resolve \
|
||||
conflict.",
|
||||
id,
|
||||
user_id.localpart(),
|
||||
device_id
|
||||
);
|
||||
|
||||
self.services
|
||||
.users
|
||||
.remove_device(&user_id, &device_id)
|
||||
.await;
|
||||
}
|
||||
|
||||
self.start_appservice(id, registration).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||
}
|
||||
|
||||
impl Service {
|
||||
/// Starts an appservice, ensuring its sender_localpart user exists and is
|
||||
/// active. Creates the user if it doesn't exist, or reactivates it if it
|
||||
/// was deactivated. Then registers the appservice in memory for request
|
||||
/// handling.
|
||||
async fn start_appservice(&self, id: String, registration: Registration) -> Result {
|
||||
let appservice_user_id = UserId::parse_with_server_name(
|
||||
registration.sender_localpart.as_str(),
|
||||
self.services.globals.server_name(),
|
||||
)?;
|
||||
|
||||
if !self.services.users.exists(&appservice_user_id).await {
|
||||
self.services
|
||||
.users
|
||||
.create(&appservice_user_id, None, None)
|
||||
.await?;
|
||||
} else if self
|
||||
.services
|
||||
.users
|
||||
.is_deactivated(&appservice_user_id)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// Reactivate the appservice user if it was accidentally deactivated
|
||||
self.services
|
||||
.users
|
||||
.set_password(&appservice_user_id, None)
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.registration_info
|
||||
.write()
|
||||
.await
|
||||
.insert(id, registration.try_into()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Registers an appservice and returns the ID to the caller
|
||||
pub async fn register_appservice(
|
||||
&self,
|
||||
@@ -68,15 +142,40 @@ pub async fn register_appservice(
|
||||
appservice_config_body: &str,
|
||||
) -> Result {
|
||||
//TODO: Check for collisions between exclusive appservice namespaces
|
||||
self.registration_info
|
||||
.write()
|
||||
|
||||
// Check for token collision with other appservices (allow re-registration of
|
||||
// same appservice)
|
||||
if let Ok(existing) = self.find_from_token(®istration.as_token).await {
|
||||
if existing.registration.id != registration.id {
|
||||
return Err(err!(Request(InvalidParam(
|
||||
"Cannot register appservice: Token is already used by appservice '{}'. \
|
||||
Please generate a different token.",
|
||||
existing.registration.id
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent token collision with existing user tokens
|
||||
if self
|
||||
.services
|
||||
.users
|
||||
.find_from_token(®istration.as_token)
|
||||
.await
|
||||
.insert(registration.id.clone(), registration.clone().try_into()?);
|
||||
.is_ok()
|
||||
{
|
||||
return Err(err!(Request(InvalidParam(
|
||||
"Cannot register appservice: The provided token is already in use by a user \
|
||||
device. Please generate a different token for the appservice."
|
||||
))));
|
||||
}
|
||||
|
||||
self.db
|
||||
.id_appserviceregistrations
|
||||
.insert(®istration.id, appservice_config_body);
|
||||
|
||||
self.start_appservice(registration.id.clone(), registration.clone())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -113,12 +212,14 @@ pub async fn get_registration(&self, id: &str) -> Option<Registration> {
|
||||
.map(|info| info.registration)
|
||||
}
|
||||
|
||||
pub async fn find_from_token(&self, token: &str) -> Option<RegistrationInfo> {
|
||||
/// Returns Result to match users::find_from_token for select_ok usage
|
||||
pub async fn find_from_token(&self, token: &str) -> Result<RegistrationInfo> {
|
||||
self.read()
|
||||
.await
|
||||
.values()
|
||||
.find(|info| info.registration.as_token == token)
|
||||
.cloned()
|
||||
.ok_or_else(|| err!(Request(NotFound("Appservice token not found"))))
|
||||
}
|
||||
|
||||
/// Checks if a given user id matches any exclusive appservice regex
|
||||
|
||||
@@ -41,6 +41,11 @@ async fn worker(self: Arc<Self>) -> Result {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.services.config.ldap.enable {
|
||||
warn!("emergency password feature not available with LDAP enabled.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.set_emergency_access().await.inspect_err(|e| {
|
||||
error!("Could not set the configured emergency password for the server user: {e}");
|
||||
})
|
||||
@@ -57,7 +62,8 @@ async fn set_emergency_access(&self) -> Result {
|
||||
|
||||
self.services
|
||||
.users
|
||||
.set_password(server_user, self.services.config.emergency_password.as_deref())?;
|
||||
.set_password(server_user, self.services.config.emergency_password.as_deref())
|
||||
.await?;
|
||||
|
||||
let (ruleset, pwd_set) = match self.services.config.emergency_password {
|
||||
| Some(_) => (Ruleset::server_default(server_user), true),
|
||||
|
||||
@@ -215,8 +215,8 @@ async fn db_lt_12(services: &Services) -> Result<()> {
|
||||
for username in &services
|
||||
.users
|
||||
.list_local_users()
|
||||
.map(UserId::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<OwnedUserId>>()
|
||||
.await
|
||||
{
|
||||
let user = match UserId::parse_with_server_name(username.as_str(), &services.server.name)
|
||||
@@ -295,8 +295,8 @@ async fn db_lt_13(services: &Services) -> Result<()> {
|
||||
for username in &services
|
||||
.users
|
||||
.list_local_users()
|
||||
.map(UserId::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<OwnedUserId>>()
|
||||
.await
|
||||
{
|
||||
let user = match UserId::parse_with_server_name(username.as_str(), &services.server.name)
|
||||
|
||||
@@ -183,8 +183,8 @@ pub async fn unset_all_presence(&self) {
|
||||
.services
|
||||
.users
|
||||
.list_local_users()
|
||||
.map(UserId::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<OwnedUserId>>()
|
||||
.await
|
||||
{
|
||||
let presence = self.db.get_presence(user_id).await;
|
||||
|
||||
@@ -178,7 +178,7 @@ pub async fn get_pushers(&self, sender: &UserId) -> Vec<Pusher> {
|
||||
pub fn get_pushkeys<'a>(
|
||||
&'a self,
|
||||
sender: &'a UserId,
|
||||
) -> impl Stream<Item = &str> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a str> + Send + 'a {
|
||||
let prefix = (sender, Interfix);
|
||||
self.db
|
||||
.senderkey_pusher
|
||||
|
||||
@@ -178,7 +178,7 @@ pub async fn resolve_local_alias(&self, alias: &RoomAliasId) -> Result<OwnedRoom
|
||||
pub fn local_aliases_for_room<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
) -> impl Stream<Item = &RoomAliasId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a RoomAliasId> + Send + 'a {
|
||||
let prefix = (room_id, Interfix);
|
||||
self.db
|
||||
.aliasid_alias
|
||||
@@ -188,7 +188,9 @@ pub fn local_aliases_for_room<'a>(
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self), level = "debug")]
|
||||
pub fn all_local_aliases<'a>(&'a self) -> impl Stream<Item = (&RoomId, &str)> + Send + 'a {
|
||||
pub fn all_local_aliases<'a>(
|
||||
&'a self,
|
||||
) -> impl Stream<Item = (&'a RoomId, &'a str)> + Send + 'a {
|
||||
self.db
|
||||
.alias_roomid
|
||||
.stream()
|
||||
|
||||
@@ -60,10 +60,13 @@ pub(super) fn get_relations<'a>(
|
||||
target: ShortEventId,
|
||||
from: PduCount,
|
||||
dir: Direction,
|
||||
) -> impl Stream<Item = (PduCount, impl Event)> + Send + '_ {
|
||||
) -> impl Stream<Item = (PduCount, impl Event)> + Send + 'a {
|
||||
// Query from exact position then filter excludes it (saturating_inc could skip
|
||||
// events at min/max boundaries)
|
||||
let from_unsigned = from.into_unsigned();
|
||||
let mut current = ArrayVec::<u8, 16>::new();
|
||||
current.extend(target.to_be_bytes());
|
||||
current.extend(from.saturating_inc(dir).into_unsigned().to_be_bytes());
|
||||
current.extend(from_unsigned.to_be_bytes());
|
||||
let current = current.as_slice();
|
||||
match dir {
|
||||
| Direction::Forward => self.tofrom_relation.raw_keys_from(current).boxed(),
|
||||
@@ -73,6 +76,17 @@ pub(super) fn get_relations<'a>(
|
||||
.ready_take_while(move |key| key.starts_with(&target.to_be_bytes()))
|
||||
.map(|to_from| u64_from_u8(&to_from[8..16]))
|
||||
.map(PduCount::from_unsigned)
|
||||
.ready_filter(move |count| {
|
||||
if from == PduCount::min() || from == PduCount::max() {
|
||||
true
|
||||
} else {
|
||||
let count_unsigned = count.into_unsigned();
|
||||
match dir {
|
||||
| Direction::Forward => count_unsigned > from_unsigned,
|
||||
| Direction::Backward => count_unsigned < from_unsigned,
|
||||
}
|
||||
}
|
||||
})
|
||||
.wide_filter_map(move |shorteventid| async move {
|
||||
let pdu_id: RawPduId = PduId { shortroomid, shorteventid }.into();
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ pub(super) fn readreceipts_since<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
since: u64,
|
||||
) -> impl Stream<Item = ReceiptItem<'_>> + Send + 'a {
|
||||
) -> impl Stream<Item = ReceiptItem<'a>> + Send + 'a {
|
||||
type Key<'a> = (&'a RoomId, u64, &'a UserId);
|
||||
type KeyVal<'a> = (Key<'a>, CanonicalJsonObject);
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ pub fn readreceipts_since<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
since: u64,
|
||||
) -> impl Stream<Item = ReceiptItem<'_>> + Send + 'a {
|
||||
) -> impl Stream<Item = ReceiptItem<'a>> + Send + 'a {
|
||||
self.db.readreceipts_since(room_id, since)
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ pub fn deindex_pdu(&self, shortroomid: ShortRoomId, pdu_id: &RawPduId, message_b
|
||||
pub async fn search_pdus<'a>(
|
||||
&'a self,
|
||||
query: &'a RoomQuery<'a>,
|
||||
) -> Result<(usize, impl Stream<Item = impl Event + use<>> + Send + '_)> {
|
||||
) -> Result<(usize, impl Stream<Item = impl Event + use<>> + Send + 'a)> {
|
||||
let pdu_ids: Vec<_> = self.search_pdu_ids(query).await?.collect().await;
|
||||
|
||||
let filter = &query.criteria.filter;
|
||||
@@ -137,10 +137,10 @@ pub async fn search_pdus<'a>(
|
||||
// result is modeled as a stream such that callers don't have to be refactored
|
||||
// though an additional async/wrap still exists for now
|
||||
#[implement(Service)]
|
||||
pub async fn search_pdu_ids(
|
||||
&self,
|
||||
query: &RoomQuery<'_>,
|
||||
) -> Result<impl Stream<Item = RawPduId> + Send + '_ + use<'_>> {
|
||||
pub async fn search_pdu_ids<'a>(
|
||||
&'a self,
|
||||
query: &'a RoomQuery<'_>,
|
||||
) -> Result<impl Stream<Item = RawPduId> + Send + 'a + use<'a>> {
|
||||
let shortroomid = self.services.short.get_shortroomid(query.room_id).await?;
|
||||
|
||||
let pdu_ids = self.search_pdu_ids_query_room(query, shortroomid).await;
|
||||
@@ -173,7 +173,7 @@ fn search_pdu_ids_query_words<'a>(
|
||||
&'a self,
|
||||
shortroomid: ShortRoomId,
|
||||
word: &'a str,
|
||||
) -> impl Stream<Item = RawPduId> + Send + '_ {
|
||||
) -> impl Stream<Item = RawPduId> + Send + 'a {
|
||||
self.search_pdu_ids_query_word(shortroomid, word)
|
||||
.map(move |key| -> RawPduId {
|
||||
let key = &key[prefix_len(word)..];
|
||||
@@ -183,11 +183,11 @@ fn search_pdu_ids_query_words<'a>(
|
||||
|
||||
/// Iterate over raw database results for a word
|
||||
#[implement(Service)]
|
||||
fn search_pdu_ids_query_word(
|
||||
&self,
|
||||
fn search_pdu_ids_query_word<'a>(
|
||||
&'a self,
|
||||
shortroomid: ShortRoomId,
|
||||
word: &str,
|
||||
) -> impl Stream<Item = Val<'_>> + Send + '_ + use<'_> {
|
||||
word: &'a str,
|
||||
) -> impl Stream<Item = Val<'a>> + Send + 'a + use<'a> {
|
||||
// rustc says const'ing this not yet stable
|
||||
let end_id: RawPduId = PduId {
|
||||
shortroomid,
|
||||
|
||||
@@ -62,7 +62,7 @@ pub async fn get_or_create_shorteventid(&self, event_id: &EventId) -> ShortEvent
|
||||
pub fn multi_get_or_create_shorteventid<'a, I>(
|
||||
&'a self,
|
||||
event_ids: I,
|
||||
) -> impl Stream<Item = ShortEventId> + Send + '_
|
||||
) -> impl Stream<Item = ShortEventId> + Send + 'a
|
||||
where
|
||||
I: Iterator<Item = &'a EventId> + Clone + Debug + Send + 'a,
|
||||
{
|
||||
|
||||
@@ -388,7 +388,7 @@ pub async fn get_room_shortstatehash(&self, room_id: &RoomId) -> Result<ShortSta
|
||||
pub fn get_forward_extremities<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
) -> impl Stream<Item = &EventId> + Send + '_ {
|
||||
) -> impl Stream<Item = &'a EventId> + Send + 'a {
|
||||
let prefix = (room_id, Interfix);
|
||||
|
||||
self.db
|
||||
|
||||
@@ -144,7 +144,7 @@ pub fn get_appservice_in_room_cache_usage(&self) -> (usize, usize) {
|
||||
pub fn room_servers<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
) -> impl Stream<Item = &ServerName> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a ServerName> + Send + 'a {
|
||||
let prefix = (room_id, Interfix);
|
||||
self.db
|
||||
.roomserverids
|
||||
@@ -167,7 +167,7 @@ pub async fn server_in_room<'a>(&'a self, server: &'a ServerName, room_id: &'a R
|
||||
pub fn server_rooms<'a>(
|
||||
&'a self,
|
||||
server: &'a ServerName,
|
||||
) -> impl Stream<Item = &RoomId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a RoomId> + Send + 'a {
|
||||
let prefix = (server, Interfix);
|
||||
self.db
|
||||
.serverroomids
|
||||
@@ -202,7 +202,7 @@ pub fn get_shared_rooms<'a>(
|
||||
&'a self,
|
||||
user_a: &'a UserId,
|
||||
user_b: &'a UserId,
|
||||
) -> impl Stream<Item = &RoomId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a RoomId> + Send + 'a {
|
||||
use conduwuit::utils::set;
|
||||
|
||||
let a = self.rooms_joined(user_a);
|
||||
@@ -216,7 +216,7 @@ pub fn get_shared_rooms<'a>(
|
||||
pub fn room_members<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
) -> impl Stream<Item = &UserId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a UserId> + Send + 'a {
|
||||
let prefix = (room_id, Interfix);
|
||||
self.db
|
||||
.roomuserid_joined
|
||||
@@ -239,7 +239,7 @@ pub async fn room_joined_count(&self, room_id: &RoomId) -> Result<u64> {
|
||||
pub fn local_users_in_room<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
) -> impl Stream<Item = &UserId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a UserId> + Send + 'a {
|
||||
self.room_members(room_id)
|
||||
.ready_filter(|user| self.services.globals.user_is_local(user))
|
||||
}
|
||||
@@ -251,7 +251,7 @@ pub fn local_users_in_room<'a>(
|
||||
pub fn active_local_users_in_room<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
) -> impl Stream<Item = &UserId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a UserId> + Send + 'a {
|
||||
self.local_users_in_room(room_id)
|
||||
.filter(|user| self.services.users.is_active(user))
|
||||
}
|
||||
@@ -273,7 +273,7 @@ pub async fn room_invited_count(&self, room_id: &RoomId) -> Result<u64> {
|
||||
pub fn room_useroncejoined<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
) -> impl Stream<Item = &UserId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a UserId> + Send + 'a {
|
||||
let prefix = (room_id, Interfix);
|
||||
self.db
|
||||
.roomuseroncejoinedids
|
||||
@@ -288,7 +288,7 @@ pub fn room_useroncejoined<'a>(
|
||||
pub fn room_members_invited<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
) -> impl Stream<Item = &UserId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a UserId> + Send + 'a {
|
||||
let prefix = (room_id, Interfix);
|
||||
self.db
|
||||
.roomuserid_invitecount
|
||||
@@ -303,7 +303,7 @@ pub fn room_members_invited<'a>(
|
||||
pub fn room_members_knocked<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
) -> impl Stream<Item = &UserId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a UserId> + Send + 'a {
|
||||
let prefix = (room_id, Interfix);
|
||||
self.db
|
||||
.roomuserid_knockedcount
|
||||
@@ -347,7 +347,7 @@ pub async fn get_left_count(&self, room_id: &RoomId, user_id: &UserId) -> Result
|
||||
pub fn rooms_joined<'a>(
|
||||
&'a self,
|
||||
user_id: &'a UserId,
|
||||
) -> impl Stream<Item = &RoomId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a RoomId> + Send + 'a {
|
||||
self.db
|
||||
.userroomid_joined
|
||||
.keys_raw_prefix(user_id)
|
||||
|
||||
@@ -49,7 +49,7 @@ pub async fn update_membership(
|
||||
#[allow(clippy::collapsible_if)]
|
||||
if !self.services.globals.user_is_local(user_id) {
|
||||
if !self.services.users.exists(user_id).await {
|
||||
self.services.users.create(user_id, None)?;
|
||||
self.services.users.create(user_id, None, None).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ pub async fn servers_route_via(&self, room_id: &RoomId) -> Result<Vec<OwnedServe
|
||||
pub fn servers_invite_via<'a>(
|
||||
&'a self,
|
||||
room_id: &'a RoomId,
|
||||
) -> impl Stream<Item = &ServerName> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a ServerName> + Send + 'a {
|
||||
type KeyVal<'a> = (Ignore, Vec<&'a ServerName>);
|
||||
|
||||
self.db
|
||||
|
||||
@@ -165,25 +165,6 @@ pub async fn create_hash_and_sign_event(
|
||||
return Err!(Request(Forbidden("Event is not authorized.")));
|
||||
}
|
||||
|
||||
// Check with the policy server
|
||||
match self
|
||||
.services
|
||||
.event_handler
|
||||
.ask_policy_server(&pdu, room_id)
|
||||
.await
|
||||
{
|
||||
| Ok(true) => {},
|
||||
| Ok(false) => {
|
||||
return Err!(Request(Forbidden(debug_warn!(
|
||||
"Policy server marked this event as spam"
|
||||
))));
|
||||
},
|
||||
| Err(e) => {
|
||||
// fail open
|
||||
warn!("Failed to check event with policy server: {e}");
|
||||
},
|
||||
}
|
||||
|
||||
// Hash and sign
|
||||
let mut pdu_json = utils::to_canonical_object(&pdu).map_err(|e| {
|
||||
err!(Request(BadJson(warn!("Failed to convert PDU to canonical JSON: {e}"))))
|
||||
@@ -222,6 +203,25 @@ pub async fn create_hash_and_sign_event(
|
||||
|
||||
pdu_json.insert("event_id".into(), CanonicalJsonValue::String(pdu.event_id.clone().into()));
|
||||
|
||||
// Check with the policy server
|
||||
match self
|
||||
.services
|
||||
.event_handler
|
||||
.ask_policy_server(&pdu, room_id)
|
||||
.await
|
||||
{
|
||||
| Ok(true) => {},
|
||||
| Ok(false) => {
|
||||
return Err!(Request(Forbidden(debug_warn!(
|
||||
"Policy server marked this event as spam"
|
||||
))));
|
||||
},
|
||||
| Err(e) => {
|
||||
// fail open
|
||||
warn!("Failed to check event with policy server: {e}");
|
||||
},
|
||||
}
|
||||
|
||||
// Generate short event id
|
||||
let _shorteventid = self
|
||||
.services
|
||||
|
||||
+241
-10
@@ -1,11 +1,19 @@
|
||||
#[cfg(feature = "ldap")]
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::BTreeMap, mem, sync::Arc};
|
||||
|
||||
#[cfg(feature = "ldap")]
|
||||
use conduwuit::result::LogErr;
|
||||
use conduwuit::{
|
||||
Err, Error, Result, Server, at, debug_warn, err, trace,
|
||||
Err, Error, Result, Server, at, debug_warn, err, is_equal_to, trace,
|
||||
utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted},
|
||||
};
|
||||
#[cfg(feature = "ldap")]
|
||||
use conduwuit_core::{debug, error};
|
||||
use database::{Deserialized, Ignore, Interfix, Json, Map};
|
||||
use futures::{Stream, StreamExt, TryFutureExt};
|
||||
#[cfg(feature = "ldap")]
|
||||
use ldap3::{LdapConnAsync, Scope, SearchEntry};
|
||||
use ruma::{
|
||||
DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId,
|
||||
OneTimeKeyName, OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedUserId, RoomId, UInt, UserId,
|
||||
@@ -19,7 +27,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Dep, account_data, admin, globals, rooms};
|
||||
use crate::{Dep, account_data, admin, appservice, globals, rooms};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserSuspension {
|
||||
@@ -40,6 +48,7 @@ struct Services {
|
||||
server: Arc<Server>,
|
||||
account_data: Dep<account_data::Service>,
|
||||
admin: Dep<admin::Service>,
|
||||
appservice: Dep<appservice::Service>,
|
||||
globals: Dep<globals::Service>,
|
||||
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||
state_cache: Dep<rooms::state_cache::Service>,
|
||||
@@ -62,6 +71,7 @@ struct Data {
|
||||
userid_displayname: Arc<Map>,
|
||||
userid_lastonetimekeyupdate: Arc<Map>,
|
||||
userid_masterkeyid: Arc<Map>,
|
||||
userid_origin: Arc<Map>,
|
||||
userid_password: Arc<Map>,
|
||||
userid_suspension: Arc<Map>,
|
||||
userid_selfsigningkeyid: Arc<Map>,
|
||||
@@ -76,6 +86,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
server: args.server.clone(),
|
||||
account_data: args.depend::<account_data::Service>("account_data"),
|
||||
admin: args.depend::<admin::Service>("admin"),
|
||||
appservice: args.depend::<appservice::Service>("appservice"),
|
||||
globals: args.depend::<globals::Service>("globals"),
|
||||
state_accessor: args
|
||||
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
||||
@@ -98,6 +109,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
userid_displayname: args.db["userid_displayname"].clone(),
|
||||
userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(),
|
||||
userid_masterkeyid: args.db["userid_masterkeyid"].clone(),
|
||||
userid_origin: args.db["userid_origin"].clone(),
|
||||
userid_password: args.db["userid_password"].clone(),
|
||||
userid_suspension: args.db["userid_suspension"].clone(),
|
||||
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
|
||||
@@ -134,9 +146,21 @@ pub async fn is_admin(&self, user_id: &UserId) -> bool {
|
||||
}
|
||||
|
||||
/// Create a new user account on this homeserver.
|
||||
///
|
||||
/// User origin is by default "password" (meaning that it will login using
|
||||
/// its user_id/password). Users with other origins (currently only "ldap"
|
||||
/// is available) have special login processes.
|
||||
#[inline]
|
||||
pub fn create(&self, user_id: &UserId, password: Option<&str>) -> Result<()> {
|
||||
self.set_password(user_id, password)
|
||||
pub async fn create(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
password: Option<&str>,
|
||||
origin: Option<&str>,
|
||||
) -> Result<()> {
|
||||
self.db
|
||||
.userid_origin
|
||||
.insert(user_id, origin.unwrap_or("password"));
|
||||
self.set_password(user_id, password).await
|
||||
}
|
||||
|
||||
/// Deactivate account
|
||||
@@ -150,7 +174,7 @@ pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> {
|
||||
// result in an empty string, so the user will not be able to log in again.
|
||||
// Systems like changing the password without logging in should check if the
|
||||
// account is deactivated.
|
||||
self.set_password(user_id, None)?;
|
||||
self.set_password(user_id, None).await?;
|
||||
|
||||
// TODO: Unhook 3PID
|
||||
Ok(())
|
||||
@@ -251,13 +275,34 @@ pub fn list_local_users(&self) -> impl Stream<Item = &UserId> + Send + '_ {
|
||||
.ready_filter_map(|(u, p): (&UserId, &[u8])| (!p.is_empty()).then_some(u))
|
||||
}
|
||||
|
||||
/// Returns the origin of the user (password/LDAP/...).
|
||||
pub async fn origin(&self, user_id: &UserId) -> Result<String> {
|
||||
self.db.userid_origin.get(user_id).await.deserialized()
|
||||
}
|
||||
|
||||
/// Returns the password hash for the given user.
|
||||
pub async fn password_hash(&self, user_id: &UserId) -> Result<String> {
|
||||
self.db.userid_password.get(user_id).await.deserialized()
|
||||
}
|
||||
|
||||
/// Hash and set the user's password to the Argon2 hash
|
||||
pub fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> {
|
||||
pub async fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> {
|
||||
// Cannot change the password of a LDAP user. There are two special cases :
|
||||
// - a `None` password can be used to deactivate a LDAP user
|
||||
// - a "*" password is used as the default password of an active LDAP user
|
||||
if cfg!(feature = "ldap")
|
||||
&& password.is_some_and(|pwd| pwd != "*")
|
||||
&& self
|
||||
.db
|
||||
.userid_origin
|
||||
.get(user_id)
|
||||
.await
|
||||
.deserialized::<String>()
|
||||
.is_ok_and(is_equal_to!("ldap"))
|
||||
{
|
||||
return Err!(Request(InvalidParam("Cannot change password of a LDAP user")));
|
||||
}
|
||||
|
||||
password
|
||||
.map(utils::hash::password)
|
||||
.transpose()
|
||||
@@ -377,7 +422,7 @@ pub async fn remove_device(&self, user_id: &UserId, device_id: &DeviceId) {
|
||||
pub fn all_device_ids<'a>(
|
||||
&'a self,
|
||||
user_id: &'a UserId,
|
||||
) -> impl Stream<Item = &DeviceId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a DeviceId> + Send + 'a {
|
||||
let prefix = (user_id, Interfix);
|
||||
self.db
|
||||
.userdeviceid_metadata
|
||||
@@ -391,6 +436,31 @@ pub async fn get_token(&self, user_id: &UserId, device_id: &DeviceId) -> Result<
|
||||
self.db.userdeviceid_token.qry(&key).await.deserialized()
|
||||
}
|
||||
|
||||
/// Generate a unique access token that doesn't collide with existing tokens
|
||||
pub async fn generate_unique_token(&self) -> String {
|
||||
loop {
|
||||
let token = utils::random_string(32);
|
||||
|
||||
// Check for collision with appservice tokens
|
||||
if self
|
||||
.services
|
||||
.appservice
|
||||
.find_from_token(&token)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for collision with user tokens
|
||||
if self.db.token_userdeviceid.get(&token).await.is_ok() {
|
||||
continue;
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the access token of one device.
|
||||
pub async fn set_token(
|
||||
&self,
|
||||
@@ -407,6 +477,19 @@ pub async fn set_token(
|
||||
)));
|
||||
}
|
||||
|
||||
// Check for token collision with appservices
|
||||
if self
|
||||
.services
|
||||
.appservice
|
||||
.find_from_token(token)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err!(Request(InvalidParam(
|
||||
"Token conflicts with an existing appservice token"
|
||||
)));
|
||||
}
|
||||
|
||||
// Remove old token
|
||||
if let Ok(old_token) = self.db.userdeviceid_token.qry(&key).await {
|
||||
self.db.token_userdeviceid.remove(&old_token);
|
||||
@@ -687,7 +770,7 @@ pub fn keys_changed<'a>(
|
||||
user_id: &'a UserId,
|
||||
from: u64,
|
||||
to: Option<u64>,
|
||||
) -> impl Stream<Item = &UserId> + Send + 'a {
|
||||
) -> impl Stream<Item = &'a UserId> + Send + 'a {
|
||||
self.keys_changed_user_or_room(user_id.as_str(), from, to)
|
||||
.map(|(user_id, ..)| user_id)
|
||||
}
|
||||
@@ -698,7 +781,7 @@ pub fn room_keys_changed<'a>(
|
||||
room_id: &'a RoomId,
|
||||
from: u64,
|
||||
to: Option<u64>,
|
||||
) -> impl Stream<Item = (&UserId, u64)> + Send + 'a {
|
||||
) -> impl Stream<Item = (&'a UserId, u64)> + Send + 'a {
|
||||
self.keys_changed_user_or_room(room_id.as_str(), from, to)
|
||||
}
|
||||
|
||||
@@ -707,7 +790,7 @@ fn keys_changed_user_or_room<'a>(
|
||||
user_or_room_id: &'a str,
|
||||
from: u64,
|
||||
to: Option<u64>,
|
||||
) -> impl Stream<Item = (&UserId, u64)> + Send + 'a {
|
||||
) -> impl Stream<Item = (&'a UserId, u64)> + Send + 'a {
|
||||
type KeyVal<'a> = ((&'a str, u64), &'a UserId);
|
||||
|
||||
let to = to.unwrap_or(u64::MAX);
|
||||
@@ -1092,6 +1175,154 @@ pub fn set_timezone(&self, user_id: &UserId, timezone: Option<String>) {
|
||||
self.db.useridprofilekey_value.del(key);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ldap"))]
|
||||
pub async fn search_ldap(&self, _user_id: &UserId) -> Result<Vec<(String, bool)>> {
|
||||
Err!(FeatureDisabled("ldap"))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ldap")]
|
||||
pub async fn search_ldap(&self, user_id: &UserId) -> Result<Vec<(String, bool)>> {
|
||||
let localpart = user_id.localpart().to_owned();
|
||||
let lowercased_localpart = localpart.to_lowercase();
|
||||
|
||||
let config = &self.services.server.config.ldap;
|
||||
let uri = config
|
||||
.uri
|
||||
.as_ref()
|
||||
.ok_or_else(|| err!(Ldap(error!("LDAP URI is not configured."))))?;
|
||||
|
||||
debug!(?uri, "LDAP creating connection...");
|
||||
let (conn, mut ldap) = LdapConnAsync::new(uri.as_str())
|
||||
.await
|
||||
.map_err(|e| err!(Ldap(error!(?user_id, "LDAP connection setup error: {e}"))))?;
|
||||
|
||||
let driver = self.services.server.runtime().spawn(async move {
|
||||
match conn.drive().await {
|
||||
| Err(e) => error!("LDAP connection error: {e}"),
|
||||
| Ok(()) => debug!("LDAP connection completed."),
|
||||
}
|
||||
});
|
||||
|
||||
match (&config.bind_dn, &config.bind_password_file) {
|
||||
| (Some(bind_dn), Some(bind_password_file)) => {
|
||||
let bind_pw = String::from_utf8(std::fs::read(bind_password_file)?)?;
|
||||
ldap.simple_bind(bind_dn, bind_pw.trim())
|
||||
.await
|
||||
.and_then(ldap3::LdapResult::success)
|
||||
.map_err(|e| err!(Ldap(error!("LDAP bind error: {e}"))))?;
|
||||
},
|
||||
| (..) => {},
|
||||
}
|
||||
|
||||
let attr = [&config.uid_attribute, &config.name_attribute];
|
||||
|
||||
let user_filter = &config.filter.replace("{username}", &lowercased_localpart);
|
||||
|
||||
let (entries, _result) = ldap
|
||||
.search(&config.base_dn, Scope::Subtree, user_filter, &attr)
|
||||
.await
|
||||
.and_then(ldap3::SearchResult::success)
|
||||
.inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Search"))
|
||||
.map_err(|e| err!(Ldap(error!(?attr, ?user_filter, "LDAP search error: {e}"))))?;
|
||||
|
||||
let mut dns: HashMap<String, bool> = entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
let search_entry = SearchEntry::construct(entry);
|
||||
debug!(?search_entry, "LDAP search entry");
|
||||
search_entry
|
||||
.attrs
|
||||
.get(&config.uid_attribute)
|
||||
.into_iter()
|
||||
.chain(search_entry.attrs.get(&config.name_attribute))
|
||||
.any(|ids| ids.contains(&localpart) || ids.contains(&lowercased_localpart))
|
||||
.then_some((search_entry.dn, false))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !config.admin_filter.is_empty() {
|
||||
let admin_base_dn = if config.admin_base_dn.is_empty() {
|
||||
&config.base_dn
|
||||
} else {
|
||||
&config.admin_base_dn
|
||||
};
|
||||
|
||||
let admin_filter = &config
|
||||
.admin_filter
|
||||
.replace("{username}", &lowercased_localpart);
|
||||
|
||||
let (admin_entries, _result) = ldap
|
||||
.search(admin_base_dn, Scope::Subtree, admin_filter, &attr)
|
||||
.await
|
||||
.and_then(ldap3::SearchResult::success)
|
||||
.inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Admin Search"))
|
||||
.map_err(|e| {
|
||||
err!(Ldap(error!(?attr, ?admin_filter, "Ldap admin search error: {e}")))
|
||||
})?;
|
||||
|
||||
dns.extend(admin_entries.into_iter().filter_map(|entry| {
|
||||
let search_entry = SearchEntry::construct(entry);
|
||||
debug!(?search_entry, "LDAP search entry");
|
||||
search_entry
|
||||
.attrs
|
||||
.get(&config.uid_attribute)
|
||||
.into_iter()
|
||||
.chain(search_entry.attrs.get(&config.name_attribute))
|
||||
.any(|ids| ids.contains(&localpart) || ids.contains(&lowercased_localpart))
|
||||
.then_some((search_entry.dn, true))
|
||||
}));
|
||||
}
|
||||
|
||||
ldap.unbind()
|
||||
.await
|
||||
.map_err(|e| err!(Ldap(error!("LDAP unbind error: {e}"))))?;
|
||||
|
||||
driver.await.log_err().ok();
|
||||
|
||||
Ok(dns.drain().collect())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ldap"))]
|
||||
pub async fn auth_ldap(&self, _user_dn: &str, _password: &str) -> Result {
|
||||
Err!(FeatureDisabled("ldap"))
|
||||
}
|
||||
|
||||
#[cfg(feature = "ldap")]
|
||||
pub async fn auth_ldap(&self, user_dn: &str, password: &str) -> Result {
|
||||
let config = &self.services.server.config.ldap;
|
||||
let uri = config
|
||||
.uri
|
||||
.as_ref()
|
||||
.ok_or_else(|| err!(Ldap(error!("LDAP URI is not configured."))))?;
|
||||
|
||||
debug!(?uri, "LDAP creating connection...");
|
||||
let (conn, mut ldap) = LdapConnAsync::new(uri.as_str())
|
||||
.await
|
||||
.map_err(|e| err!(Ldap(error!(?user_dn, "LDAP connection setup error: {e}"))))?;
|
||||
|
||||
let driver = self.services.server.runtime().spawn(async move {
|
||||
match conn.drive().await {
|
||||
| Err(e) => error!("LDAP connection error: {e}"),
|
||||
| Ok(()) => debug!("LDAP connection completed."),
|
||||
}
|
||||
});
|
||||
|
||||
ldap.simple_bind(user_dn, password)
|
||||
.await
|
||||
.and_then(ldap3::LdapResult::success)
|
||||
.map_err(|e| {
|
||||
err!(Request(Forbidden(debug_error!("LDAP authentication error: {e}"))))
|
||||
})?;
|
||||
|
||||
ldap.unbind()
|
||||
.await
|
||||
.map_err(|e| err!(Ldap(error!("LDAP unbind error: {e}"))))?;
|
||||
|
||||
driver.await.log_err().ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_master_key(
|
||||
|
||||
@@ -48,7 +48,7 @@ vector () {
|
||||
VECTOR_OPTS=$@
|
||||
element "$TOOLCHAIN" $VECTOR_OPTS --no-default-features
|
||||
element "$TOOLCHAIN" $VECTOR_OPTS --features=default
|
||||
element "$TOOLCHAIN" $VECTOR_OPTS --all-features
|
||||
element "$TOOLCHAIN" $VECTOR_OPTS --features full
|
||||
}
|
||||
|
||||
matrix () {
|
||||
|
||||
Reference in New Issue
Block a user