Compare commits

..

27 Commits

Author SHA1 Message Date
nexy7574
446db274a3 fix(hydra): Always append the current extremity to leaves 2025-09-17 21:32:46 +01:00
nexy7574
6840ec45f7 fix(hydra): Use an enum instead of a float for stateres version 2025-09-17 21:32:46 +01:00
nexy7574
c4a2773230 fix(hydra): Fix rocksdb compile errors 2025-09-17 21:32:46 +01:00
nexy7574
502fbbf0cd chore(hydra): Bump ruwuma.. again 2025-09-17 21:32:46 +01:00
nexy7574
19bd8a3c05 chore(hydra): Bump ruwuma *correctly*
# Conflicts:
#	Cargo.lock
#	Cargo.toml
2025-09-17 21:32:46 +01:00
nexy7574
8ae73d455f chore(hydra): Bump ruwuma 2025-09-17 21:32:46 +01:00
nexy7574
ccb112ef05 fix(hydra): Fix unknown join rule processing, again 2025-09-17 21:32:46 +01:00
nexy7574
b00f6ffbed fix(hydra): Fix unknown join rule processing 2025-09-17 21:32:46 +01:00
nexy7574
2e252f0841 fix(hydra): Don't use 2.1 in v6 room in test_event_sort 2025-09-17 21:32:46 +01:00
nexy7574
936f0a669b fix(hydra): Fix state resolution v2.0 bug 2025-09-17 21:32:46 +01:00
nexy7574
35b7b45ea0 fix(hydra): Fix v11 room support 2025-09-17 21:32:46 +01:00
nexy7574
ff92573103 fix(hydra): Correctly create short state hash for <v12 rooms 2025-09-17 21:32:46 +01:00
nexy7574
4ed19a1630 fix(hydra): Own it 2025-09-17 21:32:46 +01:00
nexy7574
a35f009d41 fix(hydra): Idk maybe this will fix it 2025-09-17 21:32:46 +01:00
nexy7574
540cd28d44 fix(hydra): Incorrect "auth event for incorrect room" 2025-09-17 21:32:46 +01:00
nexy7574
344e1e7d76 fix(hydra): Restricted join -> join is allowed 2025-09-17 21:32:46 +01:00
nexy7574
4446e96889 feat(hydra): Bump database version 2025-09-17 21:32:46 +01:00
nexy7574
edb92f021b fix(hydra): Tackle most open code review comments 2025-09-17 21:32:46 +01:00
nexy7574
40ebe37992 fix(hydra): Stop enforcing unfederated v12 rooms 2025-09-17 21:32:46 +01:00
nexy7574
a27659c73f fix(hydra): Don't serialise null room IDs in PDUs 2025-09-17 21:32:46 +01:00
nexy7574
fa460fe97c style: Post-rebase formatting 2025-09-17 21:32:46 +01:00
nexy7574
c2620ba57b style(hydra): Satisfy clippy's twisted and confusing demands 2025-09-17 21:32:46 +01:00
nexy7574
4024349424 fix(hydra): Backfill server selection in v12 2025-09-17 21:32:46 +01:00
nexy7574
240088c1f5 fix(hydra): Unable to parse backfilled incoming create events 2025-09-17 21:32:46 +01:00
nexy7574
91229ac3bf fix(hydra): Working? State res v2.1 2025-09-17 21:32:46 +01:00
nexy7574
854e5f7199 style: Reformat and whatnot 2025-09-17 21:32:46 +01:00
nexy7574
96a58f6d69 feat(hydra): Initial public commit for v12 support
# Conflicts:
#	src/core/info/room_version.rs
#	src/service/rooms/timeline/create.rs

# Conflicts:
#	Cargo.lock
#	src/core/matrix/state_res/event_auth.rs

# Conflicts:
#	Cargo.toml
2025-09-17 21:32:46 +01:00
54 changed files with 1114 additions and 2227 deletions

View File

@@ -61,16 +61,14 @@ runs:
id: meta
uses: docker/metadata-action@v5
with:
flavor: |
suffix=${{ inputs.tag_suffix }},onlatest=true
tags: |
type=semver,pattern={{version}},prefix=v
type=semver,pattern={{major}}.{{minor}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.0.') }},prefix=v
type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }},prefix=v
type=ref,event=branch,prefix=${{ format('refs/heads/{0}', github.event.repository.default_branch) != github.ref && 'branch-' || '' }},
type=ref,event=pr
type=sha,format=short
type=raw,value=latest${{ inputs.tag_suffix }},enable=${{ startsWith(github.ref, 'refs/tags/v') }},priority=1100
type=semver,pattern={{version}},prefix=v,suffix=${{ inputs.tag_suffix }}
type=semver,pattern={{major}}.{{minor}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.0.') }},prefix=v,suffix=${{ inputs.tag_suffix }}
type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }},prefix=v,suffix=${{ inputs.tag_suffix }}
type=ref,event=branch,prefix=${{ format('refs/heads/{0}', github.event.repository.default_branch) != github.ref && 'branch-' || '' }},suffix=${{ inputs.tag_suffix }}
type=ref,event=pr,suffix=${{ inputs.tag_suffix }}
type=sha,format=short,suffix=${{ inputs.tag_suffix }}
type=raw,value=latest${{ inputs.tag_suffix }},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
images: ${{ inputs.images }}
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
env:
@@ -83,7 +81,6 @@ runs:
env:
IMAGES: ${{ inputs.images }}
run: |
set -o xtrace
IFS=$'\n'
IMAGES_LIST=($IMAGES)
ANNOTATIONS_LIST=($DOCKER_METADATA_OUTPUT_ANNOTATIONS)
@@ -101,7 +98,6 @@ runs:
env:
IMAGES: ${{ inputs.images }}
run: |
set -o xtrace
IMAGES_LIST=($IMAGES)
for REPO in "${IMAGES_LIST[@]}"; do
docker buildx imagetools inspect $REPO:${{ steps.meta.outputs.version }}

View File

@@ -0,0 +1,58 @@
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 }}
node_major:
description: 'Major version of Node.js if available (e.g. 22)'
value: ${{ steps.detect.outputs.node_major }}
node_version:
description: 'Full Node.js version if available (e.g. 22.19.0)'
value: ${{ steps.detect.outputs.node_version }}
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}"
# Detect Node.js version if available
if command -v node >/dev/null 2>&1; then
NODE_VERSION=$(node --version | sed 's/v//')
NODE_MAJOR=$(echo $NODE_VERSION | cut -d. -f1)
echo "node_version=${NODE_VERSION}" >> $GITHUB_OUTPUT
echo "node_major=${NODE_MAJOR}" >> $GITHUB_OUTPUT
echo "🔍 Detected Node.js: v${NODE_VERSION}"
else
echo "node_version=" >> $GITHUB_OUTPUT
echo "node_major=" >> $GITHUB_OUTPUT
echo "🔍 Node.js not found"
fi
# Set OS 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}"

View File

@@ -121,7 +121,7 @@ runs:
.cargo/git/checkouts
.cargo/registry
.cargo/registry/src
key: continuwuity-rust-registry-image-${{hashFiles('**/Cargo.lock') }}
key: rust-registry-image-${{hashFiles('**/Cargo.lock') }}
- name: Cache cargo target
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
@@ -130,7 +130,7 @@ runs:
with:
path: |
cargo-target${{ env.CPU_SUFFIX }}-${{ inputs.slug }}-${{ inputs.profile }}
key: continuwuity-cargo-target${{ env.CPU_SUFFIX }}-${{ inputs.slug }}-${{ inputs.profile }}-${{hashFiles('**/Cargo.lock') }}-${{steps.rust-toolchain.outputs.rustc_version}}
key: cargo-target${{ env.CPU_SUFFIX }}-${{ inputs.slug }}-${{ inputs.profile }}-${{hashFiles('**/Cargo.lock') }}-${{steps.rust-toolchain.outputs.rustc_version}}
- name: Cache apt cache
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
@@ -139,7 +139,7 @@ runs:
with:
path: |
var-cache-apt-${{ inputs.slug }}
key: continuwuity-var-cache-apt-${{ inputs.slug }}
key: var-cache-apt-${{ inputs.slug }}
- name: Cache apt lib
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
@@ -148,7 +148,7 @@ runs:
with:
path: |
var-lib-apt-${{ inputs.slug }}
key: continuwuity-var-lib-apt-${{ inputs.slug }}
key: var-lib-apt-${{ inputs.slug }}
- name: inject cache into docker
if: ${{ env.BUILDKIT_ENDPOINT == '' }}

View File

@@ -40,7 +40,7 @@ runs:
!~/.rustup/tmp
!~/.rustup/downloads
# Requires repo to be cloned if toolchain is not specified
key: continuwuity-${{ runner.os }}-rustup-${{ inputs.toolchain || hashFiles('**/rust-toolchain.toml') }}
key: ${{ runner.os }}-rustup-${{ inputs.toolchain || hashFiles('**/rust-toolchain.toml') }}
- name: Install Rust toolchain
if: steps.rustup-version.outputs.version == ''
shell: bash

View File

@@ -29,7 +29,7 @@ runs:
steps:
- name: Detect runner OS
id: runner-os
uses: https://git.tomfos.tr/actions/detect-versions@v1
uses: ./.forgejo/actions/detect-runner-os
- name: Configure cross-compilation architecture
if: inputs.dpkg-arch != ''
@@ -69,7 +69,7 @@ runs:
/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: continuwuity-llvm-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-v${{ inputs.llvm-version }}-${{ hashFiles('**/Cargo.lock', 'rust-toolchain.toml') }}
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

View File

@@ -39,7 +39,7 @@ runs:
steps:
- name: Detect runner OS
id: runner-os
uses: https://git.tomfos.tr/actions/detect-versions@v1
uses: ./.forgejo/actions/detect-runner-os
- name: Configure Cargo environment
shell: bash
@@ -73,9 +73,9 @@ runs:
.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: continuwuity-cargo-registry-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ github.workflow }}
key: cargo-registry-${{ steps.runner-os.outputs.slug }}-${{ github.workflow }}
restore-keys: |
continuwuity-cargo-registry-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-
cargo-registry-${{ steps.runner-os.outputs.slug }}-
- name: Cache toolchain binaries
id: toolchain-cache
@@ -86,42 +86,29 @@ runs:
.rustup/toolchains
.rustup/update-hashes
# Shared toolchain cache across all Rust versions
key: continuwuity-toolchain-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}
key: toolchain-${{ steps.runner-os.outputs.slug }}
- name: Setup sccache
uses: https://git.tomfos.tr/tom/sccache-action@v1
- name: Cache dependencies
id: deps-cache
- name: Cache build artifacts
id: build-cache
uses: actions/cache@v4
with:
path: |
target/**/.fingerprint
target/**/deps
target/**/*.d
target/**/.cargo-lock
target/**/CACHEDIR.TAG
target/**/.rustc_info.json
/timelord/
# Dependencies cache - based on Cargo.lock, survives source code changes
key: >-
continuwuity-deps-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}
restore-keys: |
continuwuity-deps-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-
- name: Cache incremental compilation
id: incremental-cache
uses: actions/cache@v4
with:
path: |
!target/**/deps/*.rlib
target/**/build
target/**/.fingerprint
target/**/incremental
# Incremental cache - based on source code changes
target/**/*.d
/timelord/
# Build artifacts - cache per code change, restore from deps when code changes
key: >-
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}-${{ hashFiles('**/*.rs', '**/Cargo.toml') }}
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: |
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}-
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-
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

View File

@@ -36,7 +36,7 @@ runs:
path: |
/usr/share/rust/.cargo/bin
~/.cargo/bin
key: continuwuity-timelord-binaries
key: timelord-binaries-v3
- name: Check if binaries need installation
shell: bash
@@ -82,7 +82,7 @@ runs:
path: |
/usr/share/rust/.cargo/bin
~/.cargo/bin
key: continuwuity-timelord-binaries
key: timelord-binaries-v3
- name: Restore timelord cache with fallbacks
@@ -92,7 +92,7 @@ runs:
path: ${{ env.TIMELORD_CACHE_PATH }}
key: ${{ env.TIMELORD_KEY }}
restore-keys: |
continuwuity-timelord-${{ github.repository }}-
timelord-v1-${{ github.repository }}-
- name: Initialize timestamps on complete cache miss
if: steps.timelord-restore.outputs.cache-hit != 'true'

View File

@@ -1,148 +0,0 @@
name: Build / Debian DEB
concurrency:
group: "build-debian-${{ forge.ref }}"
cancel-in-progress: true
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
schedule:
- cron: '30 0 * * *'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
container: ["ubuntu-latest", "ubuntu-previous", "debian-latest", "debian-oldstable"]
container:
image: "ghcr.io/tcpipuk/act-runner:${{ matrix.container }}"
steps:
- name: Get Debian version
id: debian-version
run: |
VERSION=$(cat /etc/debian_version)
DISTRIBUTION=$(lsb_release -sc 2>/dev/null)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "distribution=$DISTRIBUTION" >> $GITHUB_OUTPUT
echo "Debian distribution: $DISTRIBUTION ($VERSION)"
- name: Checkout repository with full history
uses: https://code.forgejo.org/actions/checkout@v5
with:
fetch-depth: 0
- name: Cache Cargo registry
uses: https://code.forgejo.org/actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: cargo-debian-${{ steps.debian-version.outputs.distribution }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-debian-${{ steps.debian-version.outputs.distribution }}-
- name: Setup sccache
uses: https://git.tomfos.tr/tom/sccache-action@v1
- name: Configure sccache environment
run: |
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
echo "CMAKE_C_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV
echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV
echo "SCCACHE_CACHE_SIZE=10G" >> $GITHUB_ENV
# Aggressive GC since cache restores don't increment counter
echo "CARGO_INCREMENTAL_GC_TRIGGER=5" >> $GITHUB_ENV
- name: Setup Rust nightly
uses: ./.forgejo/actions/setup-rust
with:
rust-version: nightly
github-token: ${{ secrets.GH_PUBLIC_RO }}
- name: Get package version and component
id: package-meta
run: |
BASE_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"conduwuit\").version" | sed 's/[^a-zA-Z0-9.+]/~/g')
# VERSION is the package version, COMPONENT is used in
# apt's repository config like a git repo branch
if [[ "${{ forge.ref }}" == "refs/tags/"* ]]; then
# Use the "stable" component for tagged releases
COMPONENT="stable"
VERSION=$BASE_VERSION
else
# Use the "dev" component for development builds
SHA=$(echo "${{ forge.sha }}" | cut -c1-7)
DATE=$(date +%Y%m%d)
if [ "${{ forge.ref_name }}" = "main" ]; then
COMPONENT="dev"
else
# Use the sanitized ref name as the component for feature branches
COMPONENT="dev-$(echo '${{ forge.ref_name }}' | sed 's/[^a-zA-Z0-9.+]/-/g' | tr '[:upper:]' '[:lower:]' | cut -c1-30)"
fi
CLEAN_COMPONENT=$(echo $COMPONENT | sed 's/[^a-zA-Z0-9.+]/~/g')
VERSION="$BASE_VERSION~git$DATE.$SHA-$CLEAN_COMPONENT"
fi
echo "component=$COMPONENT" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Component: $COMPONENT"
echo "Version: $VERSION"
- name: Install cargo-deb
run: |
if command -v cargo-deb &> /dev/null; then
echo "cargo-deb already available"
else
echo "Installing cargo-deb"
cargo-binstall -y --no-symlinks cargo-deb
fi
- name: Install build dependencies
run: |
apt-get update -y
# Build dependencies for rocksdb
apt-get install -y clang liburing-dev
- name: Run cargo-deb
id: cargo-deb
run: |
DEB_PATH=$(cargo deb --deb-version ${{ steps.package-meta.outputs.version }})
echo "path=$DEB_PATH" >> $GITHUB_OUTPUT
- name: Test deb installation
run: |
echo "Installing: ${{ steps.cargo-deb.outputs.path }}"
apt-get install -y ${{ steps.cargo-deb.outputs.path }}
dpkg -s continuwuity
[ -f /usr/bin/conduwuit ] && echo "✅ Binary installed successfully"
[ -f /usr/lib/systemd/system/conduwuit.service ] && echo "✅ Systemd service installed"
[ -f /etc/conduwuit/conduwuit.toml ] && echo "✅ Config file installed"
- name: Upload deb artifact
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: continuwuity-${{ steps.debian-version.outputs.distribution }}
path: ${{ steps.cargo-deb.outputs.path }}
- name: Publish to Forgejo package registry
if: ${{ forge.event_name == 'push' || forge.event_name == 'workflow_dispatch' || forge.event_name == 'schedule' }}
run: |
OWNER="continuwuation"
DISTRIBUTION=${{ steps.debian-version.outputs.distribution }}
COMPONENT=${{ steps.package-meta.outputs.component }}
DEB=${{ steps.cargo-deb.outputs.path }}
echo "Publishing: $DEB in component $COMPONENT for distribution $DISTRIBUTION"
curl --fail-with-body \
-X PUT \
-H "Authorization: token ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}" \
--upload-file "$DEB" \
"${{ forge.server_url }}/api/packages/$OWNER/debian/pool/$DISTRIBUTION/$COMPONENT/upload"

View File

@@ -1,389 +0,0 @@
name: Build / Fedora RPM
concurrency:
group: "build-fedora-${{ github.ref }}"
cancel-in-progress: true
on:
push:
tags:
- "v*.*.*"
# paths:
# - 'pkg/fedora/**'
# - 'src/**'
# - 'Cargo.toml'
# - 'Cargo.lock'
# - '.forgejo/workflows/build-fedora.yml'
workflow_dispatch:
schedule:
- cron: '30 0 * * *'
jobs:
build:
runs-on: fedora-latest
steps:
- name: Detect Fedora version
id: fedora
run: |
VERSION=$(rpm -E %fedora)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Fedora version: $VERSION"
- name: Checkout repository with full history
uses: https://code.forgejo.org/actions/checkout@v5
with:
fetch-depth: 0
- name: Cache DNF packages
uses: https://code.forgejo.org/actions/cache@v4
with:
path: |
/var/cache/dnf
/var/cache/yum
key: dnf-fedora${{ steps.fedora.outputs.version }}-${{ hashFiles('pkg/fedora/continuwuity.spec.rpkg') }}-v1
restore-keys: |
dnf-fedora${{ steps.fedora.outputs.version }}-
- name: Cache Cargo registry
uses: https://code.forgejo.org/actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: cargo-fedora${{ steps.fedora.outputs.version }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-fedora${{ steps.fedora.outputs.version }}-
- name: Cache Rust build dependencies
uses: https://code.forgejo.org/actions/cache@v4
with:
path: |
~/rpmbuild/BUILD/*/target/release/deps
~/rpmbuild/BUILD/*/target/release/build
~/rpmbuild/BUILD/*/target/release/.fingerprint
~/rpmbuild/BUILD/*/target/release/incremental
key: rust-deps-fedora${{ steps.fedora.outputs.version }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
rust-deps-fedora${{ steps.fedora.outputs.version }}-
- name: Setup sccache
uses: https://git.tomfos.tr/tom/sccache-action@v1
- name: Configure sccache environment
run: |
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
echo "CMAKE_C_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV
echo "CMAKE_CXX_COMPILER_LAUNCHER=sccache" >> $GITHUB_ENV
echo "SCCACHE_CACHE_SIZE=10G" >> $GITHUB_ENV
# Aggressive GC since cache restores don't increment counter
echo "CARGO_INCREMENTAL_GC_TRIGGER=5" >> $GITHUB_ENV
- name: Install base RPM tools
run: |
dnf install -y --setopt=keepcache=1 \
fedora-packager \
python3-pip \
rpm-sign \
rpkg \
wget
- name: Setup build environment and build SRPM
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git config --global user.email "ci@continuwuity.org"
git config --global user.name "Continuwuity"
rpmdev-setuptree
cd "$GITHUB_WORKSPACE"
# Determine release suffix and version based on ref type and branch
if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
# Tags get clean version numbers for stable releases
RELEASE_SUFFIX=""
TAG_NAME="${{ github.ref_name }}"
# Extract version from tag (remove v prefix if present)
TAG_VERSION=$(echo "$TAG_NAME" | sed 's/^v//')
# Create spec file with tag version
sed -e "s/^Version:.*$/Version: $TAG_VERSION/" \
-e "s/^Release:.*$/Release: 1%{?dist}/" \
pkg/fedora/continuwuity.spec.rpkg > continuwuity.spec.rpkg
elif [ "${{ github.ref_name }}" = "main" ]; then
# Main branch gets .dev suffix
RELEASE_SUFFIX=".dev"
# Replace the Release line to include our suffix
sed "s/^Release:.*$/Release: 1${RELEASE_SUFFIX}%{?dist}/" \
pkg/fedora/continuwuity.spec.rpkg > continuwuity.spec.rpkg
else
# Other branches get sanitized branch name as suffix
SAFE_BRANCH=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9]/_/g' | cut -c1-20)
RELEASE_SUFFIX=".${SAFE_BRANCH}"
# Replace the Release line to include our suffix
sed "s/^Release:.*$/Release: 1${RELEASE_SUFFIX}%{?dist}/" \
pkg/fedora/continuwuity.spec.rpkg > continuwuity.spec.rpkg
fi
rpkg srpm --outdir "$HOME/rpmbuild/SRPMS"
ls -la $HOME/rpmbuild/SRPMS/
- name: Install build dependencies from SRPM
run: |
SRPM=$(find "$HOME/rpmbuild/SRPMS" -name "*.src.rpm" | head -1)
if [ -z "$SRPM" ]; then
echo "Error: No SRPM file found"
exit 1
fi
echo "Installing build dependencies from: $(basename $SRPM)"
dnf builddep -y "$SRPM"
- name: Build RPM from SRPM
run: |
SRPM=$(find "$HOME/rpmbuild/SRPMS" -name "*.src.rpm" | head -1)
if [ -z "$SRPM" ]; then
echo "Error: No SRPM file found"
exit 1
fi
echo "Building from SRPM: $SRPM"
rpmbuild --rebuild "$SRPM" \
--define "_topdir $HOME/rpmbuild" \
--define "_sourcedir $GITHUB_WORKSPACE" \
--nocheck # Skip %check section to avoid test dependencies
- name: Test RPM installation
run: |
# Find the main binary RPM (exclude debug and source RPMs)
RPM=$(find "$HOME/rpmbuild/RPMS" -name "continuwuity-*.rpm" \
! -name "*debuginfo*" \
! -name "*debugsource*" \
! -name "*.src.rpm" | head -1)
if [ -z "$RPM" ]; then
echo "Error: No binary RPM file found"
exit 1
fi
echo "Testing installation of: $RPM"
# Dry run first
rpm -qpi "$RPM"
echo ""
rpm -qpl "$RPM"
# Actually install it
dnf install -y "$RPM"
# Verify installation
rpm -qa | grep continuwuity
# Check that the binary exists
[ -f /usr/bin/conduwuit ] && echo "✅ Binary installed successfully"
[ -f /usr/lib/systemd/system/conduwuit.service ] && echo "✅ Systemd service installed"
[ -f /etc/conduwuit/conduwuit.toml ] && echo "✅ Config file installed"
- name: List built packages
run: |
echo "Binary RPMs:"
find "$HOME/rpmbuild/RPMS" -name "*.rpm" -type f -exec ls -la {} \;
echo ""
echo "Source RPMs:"
find "$HOME/rpmbuild/SRPMS" -name "*.rpm" -type f -exec ls -la {} \;
- name: Collect artifacts
run: |
mkdir -p artifacts
find "$HOME/rpmbuild/RPMS" -name "*.rpm" -type f -exec cp {} artifacts/ \;
find "$HOME/rpmbuild/SRPMS" -name "*.rpm" -type f -exec cp {} artifacts/ \;
cd artifacts
echo "Build Information:" > BUILD_INFO.txt
echo "==================" >> BUILD_INFO.txt
echo "Git commit: ${{ github.sha }}" >> BUILD_INFO.txt
echo "Git branch: ${{ github.ref_name }}" >> BUILD_INFO.txt
echo "Build date: $(date -u +%Y-%m-%d_%H:%M:%S_UTC)" >> BUILD_INFO.txt
echo "" >> BUILD_INFO.txt
echo "Package contents:" >> BUILD_INFO.txt
echo "-----------------" >> BUILD_INFO.txt
for rpm in *.rpm; do
echo "" >> BUILD_INFO.txt
echo "File: $rpm" >> BUILD_INFO.txt
rpm -qpi "$rpm" 2>/dev/null | grep -E "^(Name|Version|Release|Architecture|Size)" >> BUILD_INFO.txt
done
ls -la
- name: Upload binary RPM artifact
run: |
# Find the main binary RPM (exclude debug and source RPMs)
BIN_RPM=$(find artifacts -name "continuwuity-*.rpm" \
! -name "*debuginfo*" \
! -name "*debugsource*" \
! -name "*.src.rpm" \
-type f)
mkdir -p upload-bin
cp $BIN_RPM upload-bin/
- name: Upload binary RPM
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: continuwuity
path: upload-bin/
- name: Upload debug RPM artifact
uses: https://code.forgejo.org/actions/upload-artifact@v3
with:
name: continuwuity-debug
path: artifacts/*debuginfo*.rpm
- name: Publish to RPM Package Registry
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' }}
run: |
# Find the main binary RPM (exclude debug and source RPMs)
RPM=$(find artifacts -name "continuwuity-*.rpm" \
! -name "*debuginfo*" \
! -name "*debugsource*" \
! -name "*.src.rpm" \
-type f | head -1)
if [ -z "$RPM" ]; then
echo "No binary RPM found to publish"
exit 0
fi
RPM_BASENAME=$(basename "$RPM")
echo "Publishing: $RPM_BASENAME"
# Determine the group based on ref type and branch
if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
GROUP="stable"
# For tags, extract the tag name for version info
TAG_NAME="${{ github.ref_name }}"
elif [ "${{ github.ref_name }}" = "main" ]; then
GROUP="dev"
else
# Use sanitized branch name as group for feature branches
GROUP=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9]/-/g' | tr '[:upper:]' '[:lower:]' | cut -c1-30)
fi
PACKAGE_INFO=$(rpm -qpi "$RPM" 2>/dev/null)
PACKAGE_NAME=$(echo "$PACKAGE_INFO" | grep "^Name" | awk '{print $3}')
PACKAGE_VERSION=$(echo "$PACKAGE_INFO" | grep "^Version" | awk '{print $3}')
PACKAGE_RELEASE=$(echo "$PACKAGE_INFO" | grep "^Release" | awk '{print $3}')
PACKAGE_ARCH=$(echo "$PACKAGE_INFO" | grep "^Architecture" | awk '{print $2}')
# Full version includes release
FULL_VERSION="${PACKAGE_VERSION}-${PACKAGE_RELEASE}"
# Forgejo's RPM registry cannot overwrite existing packages, so we must delete first
# 404 is OK if package doesn't exist yet
echo "Removing any existing package: $PACKAGE_NAME-$FULL_VERSION.$PACKAGE_ARCH"
RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}" \
"https://forgejo.ellis.link/api/packages/continuwuation/rpm/$GROUP/package/$PACKAGE_NAME/$FULL_VERSION/$PACKAGE_ARCH")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
if [ "$HTTP_CODE" != "204" ] && [ "$HTTP_CODE" != "404" ]; then
echo "ERROR: Failed to delete package (HTTP $HTTP_CODE)"
echo "$RESPONSE" | head -n -1
exit 1
fi
curl --fail-with-body \
-X PUT \
-H "Authorization: token ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/x-rpm" \
-T "$RPM" \
"https://forgejo.ellis.link/api/packages/continuwuation/rpm/$GROUP/upload?sign=true"
echo ""
echo "✅ Published binary RPM to: https://forgejo.ellis.link/continuwuation/-/packages/rpm/continuwuity/"
echo "Group: $GROUP"
# Upload debug RPMs to separate group
DEBUG_RPMS=$(find artifacts -name "*debuginfo*.rpm")
if [ -n "$DEBUG_RPMS" ]; then
echo ""
echo "Publishing debug RPMs to group: ${GROUP}-debug"
for DEBUG_RPM in $DEBUG_RPMS; do
echo "Publishing: $(basename "$DEBUG_RPM")"
DEBUG_INFO=$(rpm -qpi "$DEBUG_RPM" 2>/dev/null)
DEBUG_NAME=$(echo "$DEBUG_INFO" | grep "^Name" | awk '{print $3}')
DEBUG_VERSION=$(echo "$DEBUG_INFO" | grep "^Version" | awk '{print $3}')
DEBUG_RELEASE=$(echo "$DEBUG_INFO" | grep "^Release" | awk '{print $3}')
DEBUG_ARCH=$(echo "$DEBUG_INFO" | grep "^Architecture" | awk '{print $2}')
DEBUG_FULL_VERSION="${DEBUG_VERSION}-${DEBUG_RELEASE}"
# Must delete existing package first (Forgejo limitation)
RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}" \
"https://forgejo.ellis.link/api/packages/continuwuation/rpm/${GROUP}-debug/package/$DEBUG_NAME/$DEBUG_FULL_VERSION/$DEBUG_ARCH")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
if [ "$HTTP_CODE" != "204" ] && [ "$HTTP_CODE" != "404" ]; then
echo "ERROR: Failed to delete debug package (HTTP $HTTP_CODE)"
echo "$RESPONSE" | head -n -1
exit 1
fi
curl --fail-with-body \
-X PUT \
-H "Authorization: token ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/x-rpm" \
-T "$DEBUG_RPM" \
"https://forgejo.ellis.link/api/packages/continuwuation/rpm/${GROUP}-debug/upload?sign=true"
done
echo "✅ Published debug RPMs to group: ${GROUP}-debug"
fi
# Also upload the SRPM to separate group
SRPM=$(find artifacts -name "*.src.rpm" | head -1)
if [ -n "$SRPM" ]; then
echo ""
echo "Publishing source RPM: $(basename "$SRPM")"
echo "Publishing to group: ${GROUP}-src"
SRPM_INFO=$(rpm -qpi "$SRPM" 2>/dev/null)
SRPM_NAME=$(echo "$SRPM_INFO" | grep "^Name" | awk '{print $3}')
SRPM_VERSION=$(echo "$SRPM_INFO" | grep "^Version" | awk '{print $3}')
SRPM_RELEASE=$(echo "$SRPM_INFO" | grep "^Release" | awk '{print $3}')
SRPM_FULL_VERSION="${SRPM_VERSION}-${SRPM_RELEASE}"
# Must delete existing SRPM first (Forgejo limitation)
echo "Removing any existing SRPM: $SRPM_NAME-$SRPM_FULL_VERSION.src"
RESPONSE=$(curl -s -w "\n%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}" \
"https://forgejo.ellis.link/api/packages/continuwuation/rpm/${GROUP}-src/package/$SRPM_NAME/$SRPM_FULL_VERSION/src")
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
if [ "$HTTP_CODE" != "204" ] && [ "$HTTP_CODE" != "404" ]; then
echo "ERROR: Failed to delete SRPM (HTTP $HTTP_CODE)"
echo "$RESPONSE" | head -n -1
exit 1
fi
curl --fail-with-body \
-X PUT \
-H "Authorization: token ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/x-rpm" \
-T "$SRPM" \
"https://forgejo.ellis.link/api/packages/continuwuation/rpm/${GROUP}-src/upload?sign=true"
echo "✅ Published source RPM to group: ${GROUP}-src"
fi

View File

@@ -51,7 +51,7 @@ jobs:
- name: Detect runner environment
id: runner-env
uses: https://git.tomfos.tr/actions/detect-versions@v1
uses: ./.forgejo/actions/detect-runner-os
- name: Setup Node.js
if: steps.runner-env.outputs.node_major == '' || steps.runner-env.outputs.node_major < '20'
@@ -63,7 +63,9 @@ jobs:
uses: actions/cache@v3
with:
path: ~/.npm
key: continuwuity-${{ steps.runner-env.outputs.slug }}-${{ steps.runner-env.outputs.arch }}-node-${{ steps.runner-env.outputs.node_version }}
key: ${{ steps.runner-env.outputs.slug }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ steps.runner-env.outputs.slug }}-node-
- name: Install dependencies
run: npm install --save-dev wrangler@latest

View File

@@ -23,8 +23,6 @@ on:
- "renovate.json"
- "pkg/**"
- "docs/**"
tags:
- "v*.*.*"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

View File

@@ -43,7 +43,7 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:41.146.4@sha256:bb70194b7405faf10a6f279b60caa10403a440ba37d158c5a4ef0ae7b67a0f92
image: ghcr.io/renovatebot/renovate:41.115.6@sha256:70c89592d424a54bedf7538c5bea2e43f4d66ce2c8b74d1356d4cf0ee9ed7ec0
options: --tmpfs /tmp:exec
steps:
- name: Checkout
@@ -59,27 +59,27 @@ jobs:
with:
path: |
/tmp/renovate/cache/renovate/repository
key: renovate-repo-cache-${{ github.run_id }}
key: repo-cache-${{ github.run_id }}
restore-keys: |
renovate-repo-cache-
repo-cache-
- name: Restore renovate package cache
uses: actions/cache/restore@v4
with:
path: |
/tmp/renovate/cache/renovate/renovate-cache-sqlite
key: renovate-package-cache-${{ github.run_id }}
key: package-cache-${{ github.run_id }}
restore-keys: |
renovate-package-cache-
package-cache-
- name: Restore renovate OSV cache
uses: actions/cache/restore@v4
with:
path: |
/tmp/osv
key: renovate-osv-cache-${{ github.run_id }}
key: osv-cache-${{ github.run_id }}
restore-keys: |
renovate-osv-cache-
osv-cache-
- name: Self-hosted Renovate
run: renovate
@@ -113,7 +113,7 @@ jobs:
with:
path: |
/tmp/renovate/cache/renovate/repository
key: renovate-repo-cache-${{ github.run_id }}
key: repo-cache-${{ github.run_id }}
- name: Save renovate package cache
if: always()
@@ -121,7 +121,7 @@ jobs:
with:
path: |
/tmp/renovate/cache/renovate/renovate-cache-sqlite
key: renovate-package-cache-${{ github.run_id }}
key: package-cache-${{ github.run_id }}
- name: Save renovate OSV cache
if: always()
@@ -129,4 +129,4 @@ jobs:
with:
path: |
/tmp/osv
key: renovate-osv-cache-${{ github.run_id }}
key: osv-cache-${{ github.run_id }}

View File

@@ -1,108 +0,0 @@
name: Update flake hashes
on:
workflow_dispatch:
pull_request:
paths:
- "Cargo.lock"
- "Cargo.toml"
- "rust-toolchain.toml"
- ".forgejo/workflows/update-flake-hashes.yml"
jobs:
update-flake-hashes:
runs-on: ubuntu-latest
steps:
- uses: https://code.forgejo.org/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 0
fetch-tags: false
fetch-single-branch: true
submodules: false
persist-credentials: false
- uses: https://github.com/cachix/install-nix-action@7ab6e7fd29da88e74b1e314a4ae9ac6b5cda3801 # v31.8.0
with:
nix_path: nixpkgs=channel:nixos-unstable
# We can skip getting a toolchain hash if this was ran as a dispatch with the intent
# to update just the rocksdb hash. If this was ran as a dispatch and the toolchain
# files are changed, we still update them, as well as the rocksdb import.
- name: Detect changed files
id: changes
run: |
git fetch origin ${{ github.base_ref }} --depth=1 || true
if [ -n "${{ github.event.pull_request.base.sha }}" ]; then
base=${{ github.event.pull_request.base.sha }}
else
base=$(git rev-parse HEAD~1)
fi
echo "Base: $base"
echo "HEAD: $(git rev-parse HEAD)"
git diff --name-only $base HEAD > changed_files.txt
echo "files=$(cat changed_files.txt)" >> $FORGEJO_OUTPUT
- name: Get new toolchain hash
if: contains(steps.changes.outputs.files, 'Cargo.toml') || contains(steps.changes.outputs.files, 'Cargo.lock') || contains(steps.changes.outputs.files, 'rust-toolchain.toml')
run: |
# Set the current sha256 to an empty hash to make `nix build` calculate a new one
awk '/fromToolchainFile *\{/{found=1; print; next} found && /sha256 =/{sub(/sha256 = .*/, "sha256 = pkgsHost.lib.fakeSha256;"); found=0} 1' flake.nix > temp.nix && mv temp.nix flake.nix
# Build continuwuity and filter for the new hash
# We do `|| true` because we want this to fail without stopping the workflow
nix build .#default 2>&1 | tee >(grep 'got:' | awk '{print $2}' > new_toolchain_hash.txt) || true
# Place the new hash in place of the empty hash
new_hash=$(cat new_toolchain_hash.txt)
sed -i "s|pkgsHost.lib.fakeSha256|\"$new_hash\"|" flake.nix
echo "New hash:"
awk -F'"' '/fromToolchainFile/{found=1; next} found && /sha256 =/{print $2; found=0}' flake.nix
echo "Expected new hash:"
cat new_toolchain_hash.txt
rm new_toolchain_hash.txt
- name: Get new rocksdb hash
run: |
# Set the current sha256 to an empty hash to make `nix build` calculate a new one
awk '/repo = "rocksdb";/{found=1; print; next} found && /sha256 =/{sub(/sha256 = .*/, "sha256 = pkgsHost.lib.fakeSha256;"); found=0} 1' flake.nix > temp.nix && mv temp.nix flake.nix
# Build continuwuity and filter for the new hash
# We do `|| true` because we want this to fail without stopping the workflow
nix build .#default 2>&1 | tee >(grep 'got:' | awk '{print $2}' > new_rocksdb_hash.txt) || true
# Place the new hash in place of the empty hash
new_hash=$(cat new_rocksdb_hash.txt)
sed -i "s|pkgsHost.lib.fakeSha256|\"$new_hash\"|" flake.nix
echo "New hash:"
awk -F'"' '/repo = "rocksdb";/{found=1; next} found && /sha256 =/{print $2; found=0}' flake.nix
echo "Expected new hash:"
cat new_rocksdb_hash.txt
rm new_rocksdb_hash.txt
- name: Show diff
run: git diff flake.nix
- name: Push changes
run: |
set -euo pipefail
if git diff --quiet --exit-code; then
echo "No changes to commit."
exit 0
fi
git config user.email "renovate@mail.ellis.link"
git config user.name "renovate"
REF="${{ github.head_ref }}"
git fetch origin "$REF"
git checkout "$REF"
git commit -a -m "chore(Nix): Updated flake hashes"
git push origin HEAD:refs/heads/"$REF"

1041
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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.8"
version = "0.5.0-rc.7"
[workspace.metadata.crane]
name = "conduwuit"
@@ -381,7 +381,6 @@ features = [
"unstable-msc4095",
"unstable-msc4121",
"unstable-msc4125",
"unstable-msc4155",
"unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4210", # remove legacy mentions
@@ -551,12 +550,9 @@ features = ["std"]
version = "1.0.2"
[workspace.dependencies.ldap3]
version = "0.12.0"
version = "0.11.5"
default-features = false
features = ["sync", "tls-rustls", "rustls-provider"]
[workspace.dependencies.resolv-conf]
version = "0.7.5"
features = ["sync", "tls-rustls"]
#
# Patches
@@ -602,6 +598,12 @@ rev = "9c8e51510c35077df888ee72a36b4b05637147da"
git = "https://forgejo.ellis.link/continuwuation/hyper-util"
rev = "e4ae7628fe4fcdacef9788c4c8415317a4489941"
# Allows no-aaaa option in resolv.conf
# Use 1-indexed line numbers when displaying parse error messages
[patch.crates-io.resolv-conf]
git = "https://forgejo.ellis.link/continuwuation/resolv-conf"
rev = "ebbbec1cb965b487a0150f5d007e96c05e3d72af"
#
# Our crates
#

View File

@@ -48,7 +48,7 @@ EOF
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.15.7
ENV BINSTALL_VERSION=1.15.4
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
@@ -166,7 +166,7 @@ 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=continuwuity-cargo-target-${TARGET_CPU}-${TARGETPLATFORM}-${RUST_PROFILE} \
--mount=type=cache,target=/app/target,id=cargo-target-${TARGET_CPU}-${TARGETPLATFORM}-${RUST_PROFILE} \
bash <<'EOF'
set -o allexport
set -o xtrace

View File

@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.15.7
ENV BINSTALL_VERSION=1.15.4
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
@@ -122,7 +122,7 @@ 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=continuwuity-cargo-target-${TARGET_CPU}-${TARGETPLATFORM}-musl-${RUST_PROFILE} \
--mount=type=cache,target=/app/target,id=cargo-target-${TARGET_CPU}-${TARGETPLATFORM}-musl-${RUST_PROFILE} \
bash <<'EOF'
set -o allexport
set -o xtrace

View File

@@ -10,7 +10,6 @@ # Summary
- [Kubernetes](deploying/kubernetes.md)
- [Arch Linux](deploying/arch-linux.md)
- [Debian](deploying/debian.md)
- [Fedora](deploying/fedora.md)
- [FreeBSD](deploying/freebsd.md)
- [TURN](turn.md)
- [Appservices](appservices.md)

View File

@@ -1,201 +0,0 @@
# RPM Installation Guide
Continuwuity is available as RPM packages for Fedora, RHEL, and compatible distributions.
The RPM packaging files are maintained in the `fedora/` directory:
- `continuwuity.spec.rpkg` - RPM spec file using rpkg macros for building from git
- `continuwuity.service` - Systemd service file for the server
- `RPM-GPG-KEY-continuwuity.asc` - GPG public key for verifying signed packages
RPM packages built by CI are signed with our GPG key (Ed25519, ID: `5E0FF73F411AAFCA`).
```bash
# Import the signing key
sudo rpm --import https://forgejo.ellis.link/continuwuation/continuwuity/raw/branch/main/fedora/RPM-GPG-KEY-continuwuity.asc
# Verify a downloaded package
rpm --checksig continuwuity-*.rpm
```
## Installation methods
**Stable releases** (recommended)
```bash
# Add the repository and install
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable/continuwuation.repo
sudo dnf install continuwuity
```
**Development builds** from main branch
```bash
# Add the dev repository and install
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/dev/continuwuation.repo
sudo dnf install continuwuity
```
**Feature branch builds** (example: `tom/new-feature`)
```bash
# Branch names are sanitized (slashes become hyphens, lowercase only)
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/tom-new-feature/continuwuation.repo
sudo dnf install continuwuity
```
**Direct installation** without adding repository
```bash
# Latest stable release
sudo dnf install https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable/continuwuity
# Latest development build
sudo dnf install https://forgejo.ellis.link/api/packages/continuwuation/rpm/dev/continuwuity
# Specific feature branch
sudo dnf install https://forgejo.ellis.link/api/packages/continuwuation/rpm/branch-name/continuwuity
```
**Manual repository configuration** (alternative method)
```bash
cat << 'EOF' | sudo tee /etc/yum.repos.d/continuwuity.repo
[continuwuity]
name=Continuwuity - Matrix homeserver
baseurl=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable
enabled=1
gpgcheck=1
gpgkey=https://forgejo.ellis.link/continuwuation/continuwuity/raw/branch/main/fedora/RPM-GPG-KEY-continuwuity.asc
EOF
sudo dnf install continuwuity
```
## Package management
**Automatic updates** with DNF Automatic
```bash
# Install and configure
sudo dnf install dnf-automatic
sudo nano /etc/dnf/automatic.conf # Set: apply_updates = yes
sudo systemctl enable --now dnf-automatic.timer
```
**Manual updates**
```bash
# Check for updates
sudo dnf check-update continuwuity
# Update to latest version
sudo dnf update continuwuity
```
**Switching channels** (stable/dev/feature branches)
```bash
# List enabled repositories
dnf repolist | grep continuwuation
# Disable current repository
sudo dnf config-manager --set-disabled continuwuation-stable # or -dev, or branch name
# Enable desired repository
sudo dnf config-manager --set-enabled continuwuation-dev # or -stable, or branch name
# Update to the new channel's version
sudo dnf update continuwuity
```
**Verifying installation**
```bash
# Check installed version
rpm -q continuwuity
# View package information
rpm -qi continuwuity
# List installed files
rpm -ql continuwuity
# Verify package integrity
rpm -V continuwuity
```
## Service management and removal
**Systemd service commands**
```bash
# Start the service
sudo systemctl start conduwuit
# Enable on boot
sudo systemctl enable conduwuit
# Check status
sudo systemctl status conduwuit
# View logs
sudo journalctl -u conduwuit -f
```
**Uninstallation**
```bash
# Stop and disable the service
sudo systemctl stop conduwuit
sudo systemctl disable conduwuit
# Remove the package
sudo dnf remove continuwuity
# Remove the repository (optional)
sudo rm /etc/yum.repos.d/continuwuation-*.repo
```
## Troubleshooting
**GPG key errors**: Temporarily disable GPG checking
```bash
sudo dnf --nogpgcheck install continuwuity
```
**Repository metadata issues**: Clear and rebuild cache
```bash
sudo dnf clean all
sudo dnf makecache
```
**Finding specific versions**
```bash
# List all available versions
dnf --showduplicates list continuwuity
# Install a specific version
sudo dnf install continuwuity-<version>
```
## Building locally
Build the RPM locally using rpkg:
```bash
# Install dependencies
sudo dnf install rpkg rpm-build cargo-rpm-macros systemd-rpm-macros
# Clone the repository
git clone https://forgejo.ellis.link/continuwuation/continuwuity.git
cd continuwuity
# Build SRPM
rpkg srpm
# Build RPM
rpmbuild --rebuild *.src.rpm
```

36
flake.lock generated
View File

@@ -10,11 +10,11 @@
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1758711588,
"narHash": "sha256-0nZlCCDC5PfndsQJXXtcyrtrfW49I3KadGMDlutzaGU=",
"lastModified": 1756403898,
"narHash": "sha256-S4SJDmVTtbcXaJkYrMFkcA5SDrpfRHlBbzwp6IRRPAw=",
"owner": "zhaofengli",
"repo": "attic",
"rev": "12cbeca141f46e1ade76728bce8adc447f2166c6",
"rev": "2524dd1c007bc7a0a9e9c863a1b02de8d54b319b",
"type": "github"
},
"original": {
@@ -99,11 +99,11 @@
},
"crane_2": {
"locked": {
"lastModified": 1759893430,
"narHash": "sha256-yAy4otLYm9iZ+NtQwTMEbqHwswSFUbhn7x826RR6djw=",
"lastModified": 1757183466,
"narHash": "sha256-kTdCCMuRE+/HNHES5JYsbRHmgtr+l9mOtf5dpcMppVc=",
"owner": "ipetkov",
"repo": "crane",
"rev": "1979a2524cb8c801520bd94c38bb3d5692419d93",
"rev": "d599ae4847e7f87603e7082d73ca673aa93c916d",
"type": "github"
},
"original": {
@@ -152,11 +152,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1760337631,
"narHash": "sha256-3nvEN2lEpWtM1x7nfuiwpYHLNDgEUiWeBbyvy4vtVw8=",
"lastModified": 1757400094,
"narHash": "sha256-5Rcs6juMoMTaMJSR1glravl4QB9yLAFBD8s7KLi4kdQ=",
"owner": "nix-community",
"repo": "fenix",
"rev": "fee7cf67cbd80a74460563388ac358b394014238",
"rev": "0682b9b518792c9428865c511a4c40c9ad85c243",
"type": "github"
},
"original": {
@@ -370,11 +370,11 @@
},
"nix-filter": {
"locked": {
"lastModified": 1757882181,
"narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
"lastModified": 1731533336,
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
"type": "github"
},
"original": {
@@ -455,11 +455,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1760256791,
"narHash": "sha256-uTpzDHRASEDeFUuToWSQ46Re8beXyG9dx4W36FQa0/c=",
"lastModified": 1757034884,
"narHash": "sha256-PgLSZDBEWUHpfTRfFyklmiiLBE1i1aGCtz4eRA3POao=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "832e3b6db48508ae436c2c7bfc0cf914eac6938e",
"rev": "ca77296380960cd497a765102eeb1356eb80fed0",
"type": "github"
},
"original": {
@@ -484,11 +484,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1760260966,
"narHash": "sha256-pOVvZz/aa+laeaUKyE6PtBevdo4rywMwjhWdSZE/O1c=",
"lastModified": 1757362324,
"narHash": "sha256-/PAhxheUq4WBrW5i/JHzcCqK5fGWwLKdH6/Lu1tyS18=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "c5181dbbe33af6f21b9d83e02fdb6fda298a3b65",
"rev": "9edc9cbe5d8e832b5864e09854fa94861697d2fd",
"type": "github"
},
"original": {

View File

@@ -65,10 +65,10 @@
domain = "forgejo.ellis.link";
owner = "continuwuation";
repo = "rocksdb";
rev = "10.5.fb";
sha256 = "sha256-X4ApGLkHF9ceBtBg77dimEpu720I79ffLoyPa8JMHaU=";
rev = "10.4.fb";
sha256 = "sha256-/Hvy1yTH/0D5aa7bc+/uqFugCQq4InTdwlRw88vA5IY=";
};
version = "v10.5.fb";
version = "v10.4.fb";
cmakeFlags =
pkgs.lib.subtractLists [
# No real reason to have snappy or zlib, no one uses this

View File

@@ -1,29 +1,13 @@
# Continuwuity for Debian
This document provides information about downloading and deploying the Debian package. You can also use this guide for other deb-based distributions such as Ubuntu.
This document provides information about downloading and deploying the Debian package. You can also use this guide for other `apt`-based distributions such as Ubuntu.
### Installation
To add the Continuwuation apt repository:
```bash
# Replace with `"dev"` for bleeding-edge builds at your own risk
export COMPONENT="stable"
# Import the Continuwuation signing key
sudo curl https://forgejo.ellis.link/api/packages/continuwuation/debian/repository.key -o /etc/apt/keyrings/forgejo-continuwuation.asc
# Add a new apt source list pointing to the repository
echo "deb [signed-by=/etc/apt/keyrings/forgejo-continuwuation.asc] https://forgejo.ellis.link/api/packages/continuwuation/debian $(lsb_release -sc) $COMPONENT" | sudo tee /etc/apt/sources.list.d/continuwuation.list
# Update remote package lists
sudo apt update
```
To install continuwuity:
```bash
sudo apt install continuwuity
```
The `continuwuity` package conflicts with the old `conduwuit` package and will remove it automatically when installed.
See the [generic deployment guide](../deploying/generic.md) for additional information about using the Debian package.
No `apt` repository is currently available. This feature is in development.
### Configuration
After installation, Continuwuity places the example configuration at `/etc/conduwuit/conduwuit.toml` as the default configuration file. The configuration file indicates which settings you must change before starting the service.
@@ -32,7 +16,7 @@ ### Configuration
### Running
The package uses the [`conduwuit.service`](../configuration/examples.md#example-systemd-unit-file) systemd unit file to start and stop Continuwuity. The binary installs at `/usr/bin/conduwuit`.
The package uses the [`conduwuit.service`](../configuration/examples.md#example-systemd-unit-file) systemd unit file to start and stop Continuwuity. The binary installs at `/usr/sbin/conduwuit`.
By default, this package assumes that Continuwuity runs behind a reverse proxy. The default configuration options apply (listening on `localhost` and TCP port `6167`). Matrix federation requires a valid domain name and TLS. To federate properly, you must set up TLS certificates and certificate renewal.

View File

@@ -1,5 +1,6 @@
# This should be run using rpkg: https://docs.pagure.org/rpkg
# 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 }}}

View File

@@ -64,8 +64,12 @@
"matchDatasources": ["docker"],
"matchPackageNames": ["ghcr.io/renovatebot/renovate"],
"automerge": true,
"automergeStrategy": "fast-forward",
"extends": ["schedule:earlyMondays"]
"automergeStrategy": "fast-forward"
},
{
"description": "Group lockfile updates into a single PR",
"matchUpdateTypes": ["lockFileMaintenance"],
"groupName": "lockfile-maintenance"
}
],
"customManagers": [
@@ -77,7 +81,7 @@
"/(^|/|\\.)([Dd]ocker|[Cc]ontainer)file$/"
],
"matchStrings": [
"# renovate: datasource=(?<datasource>[a-zA-Z0-9-._]+?) depName=(?<depName>[^\\s]+?)(?: (lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?(?: registryUrl=(?<registryUrl>[^\\s]+?))?\\s+(?:ENV\\s+|ARG\\s+)?[A-Za-z0-9_]+?_VERSION[ =][\"']?(?<currentValue>.+?)[\"']?\\s+(?:(?:ENV\\s+|ARG\\s+)?[A-Za-z0-9_]+?_CHECKSUM[ =][\"']?(?<currentDigest>.+?)[\"']?\\s)?"
"# renovate: datasource=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?(?: registryUrl=(?<registryUrl>[^\\s]+?))?\\s+(?:ENV|ARG)\\s+[A-Za-z0-9_]+?_VERSION[ =][\"']?(?<currentValue>.+?)[\"']?\\s"
]
}
]

View File

@@ -64,14 +64,10 @@ pub(crate) async fn create_content_route(
media_id: &utils::random_string(MXC_LENGTH),
};
if let Err(e) = services
services
.media
.create(mxc, Some(user), Some(&content_disposition), content_type, &body.file)
.await
{
err!("Failed to save uploaded media: {e}");
return Err!(Request(Unknown("Failed to save uploaded media")));
}
.await?;
let blurhash = body.generate_blurhash.then(|| {
services

View File

@@ -4,14 +4,11 @@
Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
};
use futures::FutureExt;
use futures::{FutureExt, join};
use ruma::{
OwnedServerName, RoomId, UserId,
api::{client::membership::invite_user, federation::membership::create_invite},
events::{
invite_permission_config::FilterLevel,
room::member::{MembershipState, RoomMemberEventContent},
},
events::room::member::{MembershipState, RoomMemberEventContent},
};
use service::Services;
@@ -50,21 +47,22 @@ pub(crate) async fn invite_user_route(
.await?;
match &body.recipient {
| invite_user::v3::InvitationRecipient::UserId { user_id: recipient_user } => {
let sender_filter_level = services
.users
.invite_filter_level(recipient_user, sender_user)
.await;
| invite_user::v3::InvitationRecipient::UserId { user_id } => {
let sender_ignored_recipient = services.users.user_is_ignored(sender_user, user_id);
let recipient_ignored_by_sender =
services.users.user_is_ignored(user_id, sender_user);
if !matches!(sender_filter_level, FilterLevel::Allow) {
// drop invites if the sender has the recipient filtered
let (sender_ignored_recipient, recipient_ignored_by_sender) =
join!(sender_ignored_recipient, recipient_ignored_by_sender);
if sender_ignored_recipient {
return Ok(invite_user::v3::Response {});
}
if let Ok(target_user_membership) = services
.rooms
.state_accessor
.get_member(&body.room_id, recipient_user)
.get_member(&body.room_id, user_id)
.await
{
if target_user_membership.membership == MembershipState::Ban {
@@ -72,27 +70,16 @@ pub(crate) async fn invite_user_route(
}
}
// check for blocked invites if the recipient is a local user.
if services.globals.user_is_local(recipient_user) {
let recipient_filter_level = services
.users
.invite_filter_level(sender_user, recipient_user)
.await;
// ignored invites aren't handled here
// since the recipient's membership should still be changed to `invite`.
// they're filtered out in the individual /sync handlers.
if matches!(recipient_filter_level, FilterLevel::Block) {
return Err!(Request(InviteBlocked(
"{recipient_user} has blocked invites from you."
)));
}
if recipient_ignored_by_sender {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
return Ok(invite_user::v3::Response {});
}
invite_helper(
&services,
sender_user,
recipient_user,
user_id,
&body.room_id,
body.reason.clone(),
false,
@@ -111,7 +98,7 @@ pub(crate) async fn invite_user_route(
pub(crate) async fn invite_helper(
services: &Services,
sender_user: &UserId,
recipient_user: &UserId,
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
is_direct: bool,
@@ -124,12 +111,12 @@ pub(crate) async fn invite_helper(
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
if !services.globals.user_is_local(recipient_user) {
if !services.globals.user_is_local(user_id) {
let (pdu, pdu_json, invite_room_state) = {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent {
avatar_url: services.users.avatar_url(recipient_user).await.ok(),
avatar_url: services.users.avatar_url(user_id).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
@@ -139,7 +126,7 @@ pub(crate) async fn invite_helper(
.rooms
.timeline
.create_hash_and_sign_event(
PduBuilder::state(recipient_user.to_string(), &content),
PduBuilder::state(user_id.to_string(), &content),
sender_user,
Some(room_id),
&state_lock,
@@ -157,7 +144,7 @@ pub(crate) async fn invite_helper(
let response = services
.sending
.send_federation_request(recipient_user.server_name(), create_invite::v2::Request {
.send_federation_request(user_id.server_name(), create_invite::v2::Request {
room_id: room_id.to_owned(),
event_id: (*pdu.event_id).to_owned(),
room_version: room_version_id.clone(),
@@ -186,7 +173,7 @@ pub(crate) async fn invite_helper(
return Err!(Request(BadJson(warn!(
%pdu.event_id, %event_id,
"Server {} sent event with wrong event ID",
recipient_user.server_name()
user_id.server_name()
))));
}
@@ -226,9 +213,9 @@ pub(crate) async fn invite_helper(
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent {
displayname: services.users.displayname(recipient_user).await.ok(),
avatar_url: services.users.avatar_url(recipient_user).await.ok(),
blurhash: services.users.blurhash(recipient_user).await.ok(),
displayname: services.users.displayname(user_id).await.ok(),
avatar_url: services.users.avatar_url(user_id).await.ok(),
blurhash: services.users.blurhash(user_id).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
@@ -238,7 +225,7 @@ pub(crate) async fn invite_helper(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(recipient_user.to_string(), &content),
PduBuilder::state(user_id.to_string(), &content),
sender_user,
Some(room_id),
&state_lock,

View File

@@ -313,14 +313,11 @@ pub async fn join_room_by_id_helper(
}
}
if !server_in_room && servers.is_empty() {
return Err!(Request(NotFound(
"No servers were provided to assist in joining the room remotely, and we are not \
already participating in the room."
)));
}
let local_join = server_in_room
|| servers.is_empty()
|| (servers.len() == 1 && services.globals.server_is_ours(&servers[0]));
if server_in_room {
if local_join {
join_room_by_id_helper_local(
services,
sender_user,
@@ -742,7 +739,6 @@ async fn join_room_by_id_helper_local(
.iter()
.stream()
.any(|restriction_room_id| {
trace!("Checking if {sender_user} is joined to {restriction_room_id}");
services
.rooms
.state_cache
@@ -755,7 +751,6 @@ async fn join_room_by_id_helper_local(
.state_cache
.local_users_in_room(room_id)
.filter(|user| {
trace!("Checking if {user} can invite {sender_user} to {room_id}");
services.rooms.state_accessor.user_can_invite(
room_id,
user,
@@ -768,7 +763,6 @@ async fn join_room_by_id_helper_local(
.await
.map(ToOwned::to_owned)
} else {
trace!("No restriction rooms are joined by {sender_user}");
None
}
};

View File

@@ -30,7 +30,6 @@
events::{
AnyStateEvent, StateEventType,
TimelineEventType::{self, *},
invite_permission_config::FilterLevel,
},
serde::Raw,
};
@@ -268,7 +267,7 @@ pub(crate) async fn ignored_filter(
pub(crate) async fn is_ignored_pdu<Pdu>(
services: &Services,
event: &Pdu,
recipient_user: &UserId,
user_id: &UserId,
) -> bool
where
Pdu: Event + Send + Sync,
@@ -279,29 +278,20 @@ pub(crate) async fn is_ignored_pdu<Pdu>(
return true;
}
let sender_user = event.sender();
let type_ignored = IGNORED_MESSAGE_TYPES.binary_search(event.kind()).is_ok();
let server_ignored = services
let ignored_type = IGNORED_MESSAGE_TYPES.binary_search(event.kind()).is_ok();
let ignored_server = services
.moderation
.is_remote_server_ignored(sender_user.server_name());
let user_ignored = services
.users
.user_is_ignored(sender_user, recipient_user)
.await;
.is_remote_server_ignored(event.sender().server_name());
if !type_ignored {
// We cannot safely ignore this type
return false;
}
if server_ignored {
// the sender's server is ignored, so ignore this event
return true;
}
if user_ignored && !services.config.send_messages_from_ignored_users_to_client {
// the recipient of this PDU has the sender ignored, and we're not
// configured to send ignored messages to clients
if ignored_type
&& (ignored_server
|| (!services.config.send_messages_from_ignored_users_to_client
&& services
.users
.user_is_ignored(event.sender(), user_id)
.await))
{
return true;
}
@@ -330,29 +320,6 @@ pub(crate) fn event_filter(item: PdusIterItem, filter: &RoomEventFilter) -> Opti
filter.matches(pdu).then_some(item)
}
#[inline]
pub(crate) async fn is_ignored_invite(
services: &Services,
recipient_user: &UserId,
room_id: &RoomId,
) -> bool {
let Ok(sender_user) = services
.rooms
.state_cache
.invite_sender(recipient_user, room_id)
.await
else {
// the invite may have been sent before the invite_sender table existed.
// assume it's not ignored
return false;
};
services
.users
.invite_filter_level(&sender_user, recipient_user)
.await == FilterLevel::Ignore
}
#[cfg_attr(debug_assertions, ctor::ctor)]
fn _is_sorted() {
debug_assert!(

View File

@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeMap;
use axum::extract::State;
use conduwuit::{
@@ -13,7 +13,6 @@
api::client::room::{self, create_room},
events::{
TimelineEventType,
invite_permission_config::FilterLevel,
room::{
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
@@ -122,40 +121,6 @@ pub(crate) async fn create_room_route(
return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")));
}
let mut invitees = BTreeSet::new();
for recipient_user in &body.invite {
if !matches!(
services
.users
.invite_filter_level(recipient_user, sender_user)
.await,
FilterLevel::Allow
) {
// drop invites if the creator has them blocked
continue;
}
// if the recipient of the invite is local and has the sender blocked, error
// out. if the recipient is remote we can't tell yet, and if they're local and
// have the sender _ignored_ their invite will be filtered out in
// the handlers for the individual /sync endpoints
if services.globals.user_is_local(recipient_user)
&& matches!(
services
.users
.invite_filter_level(sender_user, recipient_user)
.await,
FilterLevel::Block
) {
return Err!(Request(InviteBlocked(
"{recipient_user} has blocked invites from you."
)));
}
invitees.insert(recipient_user.clone());
}
let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() {
| Some(alias) =>
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?),
@@ -287,11 +252,19 @@ pub(crate) async fn create_room_route(
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
});
let mut power_levels_to_grant = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
let mut users = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
if preset == RoomPreset::TrustedPrivateChat {
for recipient_user in &invitees {
power_levels_to_grant.insert(recipient_user.clone(), int!(100));
for invite in &body.invite {
if services.users.user_is_ignored(sender_user, invite).await {
continue;
} else if services.users.user_is_ignored(invite, sender_user).await {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
continue;
}
users.insert(invite.clone(), int!(100));
}
}
@@ -316,7 +289,7 @@ pub(crate) async fn create_room_route(
}
}
} else {
power_levels_to_grant.insert(sender_user.to_owned(), int!(100));
users.insert(sender_user.to_owned(), int!(100));
creators.clear(); // If this vec is not empty, default_power_levels_content will
// treat this as a v12 room
}
@@ -324,7 +297,7 @@ pub(crate) async fn create_room_route(
let power_levels_content = default_power_levels_content(
body.power_level_content_override.as_ref(),
&body.visibility,
power_levels_to_grant,
users,
creators,
)?;
@@ -486,9 +459,17 @@ pub(crate) async fn create_room_route(
// 8. Events implied by invite (and TODO: invite_3pid)
drop(state_lock);
for recipient_user in &invitees {
for user_id in &body.invite {
if services.users.user_is_ignored(sender_user, user_id).await {
continue;
} else if services.users.user_is_ignored(user_id, sender_user).await {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
continue;
}
if let Err(e) =
invite_helper(&services, sender_user, recipient_user, &room_id, None, body.is_direct)
invite_helper(&services, sender_user, user_id, &room_id, None, body.is_direct)
.boxed()
.await
{

View File

@@ -2,7 +2,7 @@
use axum::extract::State;
use conduwuit::{
Err, Error, Event, Result, RoomVersion, debug, err, info,
Err, Error, Event, Result, debug, err, info,
matrix::{StateKey, pdu::PduBuilder},
};
use futures::{FutureExt, StreamExt};
@@ -68,77 +68,37 @@ pub(crate) async fn upgrade_room_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
// First, check if the user has permission to upgrade the room (send tombstone
// event)
let old_room_state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
// Check tombstone permission by attempting to create (but not send) the event
// Note that this does internally call the policy server with a fake room ID,
// which may not be good?
let tombstone_test_result = services
.rooms
.timeline
.create_hash_and_sign_event(
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: RoomId::new(services.globals.server_name()),
}),
sender_user,
Some(&body.room_id),
&old_room_state_lock,
)
.await;
if let Err(_e) = tombstone_test_result {
return Err!(Request(Forbidden("User does not have permission to upgrade this room.")));
}
drop(old_room_state_lock);
// Create a replacement room
let room_features = RoomVersion::new(&body.new_version)?;
let replacement_room_owned = if !room_features.room_ids_as_hashes {
Some(RoomId::new(services.globals.server_name()))
} else {
None
};
let replacement_room: Option<&RoomId> = replacement_room_owned.as_ref().map(AsRef::as_ref);
let replacement_room_tmp = match replacement_room {
| Some(v) => v,
| None => &RoomId::new(services.globals.server_name()),
};
let replacement_room = RoomId::new(services.globals.server_name());
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(replacement_room_tmp)
.get_or_create_shortroomid(&replacement_room)
.await;
// For pre-v12 rooms, send tombstone before creating replacement room
let tombstone_event_id = if !room_features.room_ids_as_hashes {
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
// Send a m.room.tombstone event to the old room to indicate that it is not
// intended to be used any further
let tombstone_event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: replacement_room.unwrap().to_owned(),
}),
sender_user,
Some(&body.room_id),
&state_lock,
)
.await?;
// Change lock to replacement room
drop(state_lock);
Some(tombstone_event_id)
} else {
None
};
let state_lock = services.rooms.state.mutex.lock(replacement_room_tmp).await;
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
// Send a m.room.tombstone event to the old room to indicate that it is not
// intended to be used any further Fail if the sender does not have the required
// permissions
let tombstone_event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: replacement_room.clone(),
}),
sender_user,
Some(&body.room_id),
&state_lock,
)
.await?;
// Change lock to replacement room
drop(state_lock);
let state_lock = services.rooms.state.mutex.lock(&replacement_room).await;
// Get the old room creation event
let mut create_event_content: CanonicalJsonObject = services
@@ -151,7 +111,7 @@ pub(crate) async fn upgrade_room_route(
// Use the m.room.tombstone event as the predecessor
let predecessor = Some(ruma::events::room::create::PreviousRoom::new(
body.room_id.clone(),
tombstone_event_id,
Some(tombstone_event_id),
));
// Send a m.room.create event containing a predecessor field and the applicable
@@ -172,7 +132,6 @@ pub(crate) async fn upgrade_room_route(
// "creator" key no longer exists in V11 rooms
create_event_content.remove("creator");
},
// TODO(hydra): additional_creators
}
}
@@ -200,7 +159,7 @@ pub(crate) async fn upgrade_room_route(
return Err(Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"));
}
let create_event_id = services
services
.rooms
.timeline
.build_and_append_pdu(
@@ -214,18 +173,11 @@ pub(crate) async fn upgrade_room_route(
timestamp: None,
},
sender_user,
replacement_room,
Some(&replacement_room),
&state_lock,
)
.boxed()
.await?;
let create_id = create_event_id.as_str().replace('$', "!");
let (replacement_room, state_lock) = if room_features.room_ids_as_hashes {
let parsed_room_id = RoomId::parse(&create_id)?;
(Some(parsed_room_id), services.rooms.state.mutex.lock(parsed_room_id).await)
} else {
(replacement_room, state_lock)
};
// Join the new room
services
@@ -252,7 +204,7 @@ pub(crate) async fn upgrade_room_route(
timestamp: None,
},
sender_user,
replacement_room,
Some(&replacement_room),
&state_lock,
)
.boxed()
@@ -291,7 +243,7 @@ pub(crate) async fn upgrade_room_route(
..Default::default()
},
sender_user,
replacement_room,
Some(&replacement_room),
&state_lock,
)
.boxed()
@@ -316,7 +268,7 @@ pub(crate) async fn upgrade_room_route(
services
.rooms
.alias
.set_alias(alias, replacement_room.unwrap(), sender_user)?;
.set_alias(alias, &replacement_room, sender_user)?;
}
// Get the old room power levels
@@ -358,27 +310,6 @@ pub(crate) async fn upgrade_room_route(
drop(state_lock);
// For v12 rooms, send tombstone AFTER creating replacement room
if room_features.room_ids_as_hashes {
let old_room_state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
// For v12 rooms, no event reference in predecessor due to cyclic dependency -
// could best effort one maybe?
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: replacement_room.unwrap().to_owned(),
}),
sender_user,
Some(&body.room_id),
&old_room_state_lock,
)
.await?;
drop(old_room_state_lock);
}
// Check if the old room has a space parent, and if so, whether we should update
// it (m.space.parent, room_id)
let parents = services
@@ -403,9 +334,8 @@ pub(crate) async fn upgrade_room_route(
continue;
};
debug!(
"Updating space {space_id} child event for room {} to {}",
&body.room_id,
replacement_room.unwrap()
"Updating space {space_id} child event for room {} to {replacement_room}",
&body.room_id
);
// First, drop the space's child event
let state_lock = services.rooms.state.mutex.lock(space_id).await;
@@ -429,10 +359,7 @@ pub(crate) async fn upgrade_room_route(
.await
.ok();
// Now, add a new child event for the replacement room
debug!(
"Adding space child event for room {} in space {space_id}",
replacement_room.unwrap()
);
debug!("Adding space child event for room {replacement_room} in space {space_id}");
services
.rooms
.timeline
@@ -445,7 +372,7 @@ pub(crate) async fn upgrade_room_route(
suggested: child.suggested,
})
.expect("event is valid, we just created it"),
state_key: Some(replacement_room.unwrap().as_str().into()),
state_key: Some(replacement_room.as_str().into()),
..Default::default()
},
sender_user,
@@ -456,15 +383,12 @@ pub(crate) async fn upgrade_room_route(
.await
.ok();
debug!(
"Finished updating space {space_id} child event for room {} to {}",
&body.room_id,
replacement_room.unwrap()
"Finished updating space {space_id} child event for room {} to {replacement_room}",
&body.room_id
);
drop(state_lock);
}
// Return the replacement room id
Ok(upgrade_room::v3::Response {
replacement_room: replacement_room.unwrap().to_owned(),
})
Ok(upgrade_room::v3::Response { replacement_room })
}

View File

@@ -60,10 +60,7 @@
use service::rooms::short::{ShortEventId, ShortStateKey};
use super::{load_timeline, share_encrypted_room};
use crate::{
Ruma, RumaResponse,
client::{ignored_filter, is_ignored_invite},
};
use crate::{Ruma, RumaResponse, client::ignored_filter};
#[derive(Default)]
struct StateChanges {
@@ -241,13 +238,6 @@ pub(crate) async fn build_sync_events(
.rooms
.state_cache
.rooms_invited(sender_user)
.wide_filter_map(async |(room_id, invite_state)| {
if is_ignored_invite(services, sender_user, &room_id).await {
None
} else {
Some((room_id, invite_state))
}
})
.fold_default(|mut invited_rooms: BTreeMap<_, _>, (room_id, invite_state)| async move {
let invite_count = services
.rooms

View File

@@ -11,7 +11,6 @@
utils::{
BoolExt, IterStream, ReadyExt, TryFutureExtExt,
math::{ruma_from_usize, usize_from_ruma, usize_from_u64_truncated},
stream::WidebandExt,
},
warn,
};
@@ -40,7 +39,7 @@
use super::{load_timeline, share_encrypted_room};
use crate::{
Ruma,
client::{DEFAULT_BUMP_TYPES, ignored_filter, is_ignored_invite},
client::{DEFAULT_BUMP_TYPES, ignored_filter},
};
type TodoRooms = BTreeMap<OwnedRoomId, (BTreeSet<TypeStateKey>, usize, u64)>;
@@ -103,13 +102,6 @@ pub(crate) async fn sync_events_v4_route(
.rooms
.state_cache
.rooms_invited(sender_user)
.wide_filter_map(async |(room_id, invite_state)| {
if is_ignored_invite(&services, sender_user, &room_id).await {
None
} else {
Some((room_id, invite_state))
}
})
.map(|r| r.0)
.collect()
.await;

View File

@@ -14,7 +14,6 @@
BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt,
future::ReadyEqExt,
math::{ruma_from_usize, usize_from_ruma},
stream::WidebandExt,
},
warn,
};
@@ -39,7 +38,7 @@
use super::share_encrypted_room;
use crate::{
Ruma,
client::{DEFAULT_BUMP_TYPES, ignored_filter, is_ignored_invite, sync::load_timeline},
client::{DEFAULT_BUMP_TYPES, ignored_filter, sync::load_timeline},
};
type SyncInfo<'a> = (&'a UserId, &'a DeviceId, u64, &'a sync_events::v5::Request);
@@ -107,13 +106,6 @@ pub(crate) async fn sync_events_v5_route(
.rooms
.state_cache
.rooms_invited(sender_user)
.wide_filter_map(async |(room_id, invite_state)| {
if is_ignored_invite(services, sender_user, &room_id).await {
None
} else {
Some((room_id, invite_state))
}
})
.map(|r| r.0)
.collect::<Vec<OwnedRoomId>>();
@@ -320,7 +312,6 @@ async fn handle_lists<'a, Rooms, AllRooms>(
for mut range in ranges {
range.0 = uint!(0);
range.1 = range.1.checked_add(uint!(1)).unwrap_or(range.1);
range.1 = range
.1
.clamp(range.0, UInt::try_from(active_rooms.len()).unwrap_or(UInt::MAX));

View File

@@ -59,7 +59,6 @@ pub(crate) async fn get_supported_versions_route(
("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) */
("org.matrix.msc4155".to_owned(), true), /* invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155) */
]),
};

View File

@@ -34,19 +34,6 @@ pub(super) async fn from(
let max_body_size = services.server.config.max_request_size;
// Check if the Content-Length header is present and valid, saves us streaming
// the response into memory
if let Some(content_length) = parts.headers.get(http::header::CONTENT_LENGTH) {
if let Ok(content_length) = content_length
.to_str()
.map(|s| s.parse::<usize>().unwrap_or_default())
{
if content_length > max_body_size {
return Err(err!(Request(TooLarge("Request body too large"))));
}
}
}
let body = axum::body::to_bytes(body, max_body_size)
.await
.map_err(|e| err!(Request(TooLarge("Request body too large: {e}"))))?;

View File

@@ -61,16 +61,13 @@ pub(crate) async fn create_invite_route(
let mut signed_event = utils::to_canonical_object(&body.event)
.map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?;
let recipient_user: OwnedUserId = signed_event
let invited_user: OwnedUserId = signed_event
.get("state_key")
.try_into()
.map(UserId::to_owned)
.map_err(|e| err!(Request(InvalidParam("Invalid state_key property: {e}"))))?;
if !services
.globals
.server_is_ours(recipient_user.server_name())
{
if !services.globals.server_is_ours(invited_user.server_name()) {
return Err!(Request(InvalidParam("User does not belong to this homeserver.")));
}
@@ -78,7 +75,7 @@ pub(crate) async fn create_invite_route(
services
.rooms
.event_handler
.acl_check(recipient_user.server_name(), &body.room_id)
.acl_check(invited_user.server_name(), &body.room_id)
.await?;
services
@@ -92,19 +89,18 @@ pub(crate) async fn create_invite_route(
// Add event_id back
signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string()));
let sender_user: &UserId = signed_event
let sender: &UserId = signed_event
.get("sender")
.try_into()
.map_err(|e| err!(Request(InvalidParam("Invalid sender property: {e}"))))?;
if services.rooms.metadata.is_banned(&body.room_id).await
&& !services.users.is_admin(&recipient_user).await
&& !services.users.is_admin(&invited_user).await
{
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
}
if services.config.block_non_admin_invites && !services.users.is_admin(&recipient_user).await
{
if services.config.block_non_admin_invites && !services.users.is_admin(&invited_user).await {
return Err!(Request(Forbidden("This server does not allow room invites.")));
}
@@ -135,9 +131,9 @@ pub(crate) async fn create_invite_route(
.state_cache
.update_membership(
&body.room_id,
&recipient_user,
&invited_user,
RoomMemberEventContent::new(MembershipState::Invite),
sender_user,
sender,
Some(invite_state),
body.via.clone(),
true,
@@ -145,7 +141,7 @@ pub(crate) async fn create_invite_route(
.await?;
for appservice in services.appservice.read().await.values() {
if appservice.is_user_match(&recipient_user) {
if appservice.is_user_match(&invited_user) {
services
.sending
.send_appservice_request(

View File

@@ -73,7 +73,6 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
| ThreepidAuthFailed
| UserDeactivated
| ThreepidDenied
| InviteBlocked
| WrongRoomKeysVersion { .. }
| Forbidden { .. } => StatusCode::FORBIDDEN,

View File

@@ -200,15 +200,11 @@ pub async fn auth_check<E, F, Fut>(
if incoming_event.room_id().is_some() {
let Some(room_id_server_name) = incoming_event.room_id().unwrap().server_name()
else {
warn!("legacy room ID has no server name");
warn!("room ID has no servername");
return Ok(false);
};
if room_id_server_name != sender.server_name() {
warn!(
expected = %sender.server_name(),
received = %room_id_server_name,
"server name of legacy room ID does not match server name of sender"
);
warn!("servername of room ID does not match servername of sender");
return Ok(false);
}
}
@@ -219,12 +215,12 @@ pub async fn auth_check<E, F, Fut>(
.room_version
.is_some_and(|v| v.deserialize().is_err())
{
warn!("unsupported room version found in m.room.create event");
warn!("invalid room version found in m.room.create event");
return Ok(false);
}
if room_version.room_ids_as_hashes && incoming_event.room_id().is_some() {
warn!("room create event incorrectly claims to have a room ID when it should not");
warn!("room create event incorrectly claims a room ID");
return Ok(false);
}
@@ -233,7 +229,7 @@ pub async fn auth_check<E, F, Fut>(
{
// If content has no creator field, reject
if content.creator.is_none() {
warn!("m.room.create event incorrectly omits 'creator' field");
warn!("no creator field found in m.room.create content");
return Ok(false);
}
}
@@ -286,19 +282,16 @@ pub async fn auth_check<E, F, Fut>(
.room_version
.is_some_and(|v| v.deserialize().is_err())
{
warn!(
create_event_id = %room_create_event.event_id(),
"unsupported room version found in m.room.create event"
);
warn!("invalid room version found in m.room.create event");
return Ok(false);
}
let expected_room_id = room_create_event.room_id_or_hash();
if incoming_event.room_id().expect("event must have a room ID") != expected_room_id {
if incoming_event.room_id().unwrap() != expected_room_id {
warn!(
expected = %expected_room_id,
received = %incoming_event.room_id().unwrap(),
"room_id of incoming event ({}) does not match that of the m.room.create event ({})",
"room_id of incoming event ({}) does not match room_id of m.room.create event ({})",
incoming_event.room_id().unwrap(),
expected_room_id,
);
@@ -311,15 +304,12 @@ pub async fn auth_check<E, F, Fut>(
.auth_events()
.any(|id| id == room_create_event.event_id());
if room_version.room_ids_as_hashes && claims_create_event {
warn!("event incorrectly references m.room.create event in auth events");
warn!("m.room.create event incorrectly found in auth events");
return Ok(false);
} else if !room_version.room_ids_as_hashes && !claims_create_event {
// If the create event is not referenced in the event's auth events, and this is
// a v11 room, reject
warn!(
missing = %room_create_event.event_id(),
"event incorrectly did not reference an m.room.create in its auth events"
);
warn!("no m.room.create event found in auth events");
return Ok(false);
}
@@ -328,7 +318,7 @@ pub async fn auth_check<E, F, Fut>(
warn!(
expected = %expected_room_id,
received = %pe.room_id().unwrap(),
"room_id of referenced power levels event does not match that of the m.room.create event"
"room_id of power levels event does not match room_id of m.room.create event"
);
return Ok(false);
}
@@ -342,9 +332,8 @@ pub async fn auth_check<E, F, Fut>(
&& room_create_event.sender().server_name() != incoming_event.sender().server_name()
{
warn!(
sender = %incoming_event.sender(),
create_sender = %room_create_event.sender(),
"room is not federated and event's sender domain does not match create event's sender domain"
"room is not federated and event's sender domain does not match create event's \
sender domain"
);
return Ok(false);
}
@@ -427,6 +416,7 @@ pub async fn auth_check<E, F, Fut>(
&user_for_join_auth_membership,
&room_create_event,
)? {
warn!("membership change not valid for some reason");
return Ok(false);
}
@@ -439,7 +429,7 @@ pub async fn auth_check<E, F, Fut>(
let sender_member_event = match sender_member_event {
| Some(mem) => mem,
| None => {
warn!("sender has no membership event");
warn!("sender not found in room");
return Ok(false);
},
};
@@ -450,7 +440,7 @@ pub async fn auth_check<E, F, Fut>(
!= expected_room_id
{
warn!(
"room_id of incoming event ({}) does not match that of the m.room.create event ({})",
"room_id of incoming event ({}) does not match room_id of m.room.create event ({})",
sender_member_event
.room_id()
.expect("event must have a room ID"),
@@ -463,7 +453,8 @@ pub async fn auth_check<E, F, Fut>(
from_json_str(sender_member_event.content().get())?;
let Some(membership_state) = sender_membership_event_content.membership else {
warn!(
?sender_membership_event_content,
sender_membership_event_content = format!("{sender_membership_event_content:?}"),
event_id = format!("{}", incoming_event.event_id()),
"Sender membership event content missing membership field"
);
return Err(Error::InvalidPdu("Missing membership field".to_owned()));
@@ -471,11 +462,7 @@ pub async fn auth_check<E, F, Fut>(
let membership_state = membership_state.deserialize()?;
if !matches!(membership_state, MembershipState::Join) {
warn!(
%sender,
?membership_state,
"sender cannot send events without being joined to the room"
);
warn!("sender's membership is not join");
return Ok(false);
}
@@ -535,12 +522,7 @@ pub async fn auth_check<E, F, Fut>(
};
if sender_power_level < invite_level {
warn!(
%sender,
has=?sender_power_level,
required=?invite_level,
"sender cannot send invites in this room"
);
warn!("sender's cannot send invites in this room");
return Ok(false);
}
@@ -552,11 +534,7 @@ pub async fn auth_check<E, F, Fut>(
// level, reject If the event has a state_key that starts with an @ and does
// not match the sender, reject.
if !can_send_event(incoming_event, power_levels_event.as_ref(), sender_power_level) {
warn!(
%sender,
event_type=?incoming_event.kind(),
"sender cannot send event"
);
warn!("user cannot send event");
return Ok(false);
}
@@ -601,12 +579,6 @@ pub async fn auth_check<E, F, Fut>(
};
if !check_redaction(room_version, incoming_event, sender_power_level, redact_level)? {
warn!(
%sender,
?sender_power_level,
?redact_level,
"redaction event was not allowed"
);
return Ok(false);
}
}
@@ -755,20 +727,12 @@ struct GetThirdPartyInvite {
let user_joined = user_for_join_auth_membership == &MembershipState::Join;
let okay_power = is_creator(room_version, &creators, create_room, user_for_join_auth)
|| auth_user_pl >= invite_level;
trace!(
auth_user_pl=?auth_user_pl,
invite_level=?invite_level,
user_joined=?user_joined,
okay_power=?okay_power,
passing=?(user_joined && okay_power),
"user for join auth is valid check details"
);
user_joined && okay_power
} else {
// No auth user was given
trace!("No auth user given for join auth");
false
};
let sender_creator = is_creator(room_version, &creators, create_room, sender);
let target_creator = is_creator(room_version, &creators, create_room, target_user);
@@ -787,7 +751,7 @@ struct GetThirdPartyInvite {
if prev_event_is_create_event && no_more_prev_events {
trace!(
%sender,
sender = %sender,
target_user = %target_user,
?sender_creator,
?target_creator,
@@ -807,33 +771,22 @@ struct GetThirdPartyInvite {
);
if sender != target_user {
// If the sender does not match state_key, reject.
warn!(
%sender,
target_user = %target_user,
"sender cannot join on behalf of another user"
);
warn!("Can't make other user join");
false
} else if target_user_current_membership == MembershipState::Ban {
// If the sender is banned, reject.
warn!(
%sender,
membership_event_id = ?target_user_membership_event_id,
"sender cannot join as they are banned from the room"
);
warn!(?target_user_membership_event_id, "Banned user can't join");
false
} else {
match join_rules {
| JoinRule::Invite =>
if !membership_allows_join {
warn!(
%sender,
membership_event_id = ?target_user_membership_event_id,
membership = ?target_user_current_membership,
"sender cannot join as they are not invited to the invite-only room"
membership=?target_user_current_membership,
"Join rule is invite but membership does not allow join"
);
false
} else {
trace!(sender=%sender, "sender is invited to room, allowing join");
true
},
| JoinRule::Knock if !room_version.allow_knocking => {
@@ -843,14 +796,11 @@ struct GetThirdPartyInvite {
| JoinRule::Knock =>
if !membership_allows_join {
warn!(
%sender,
membership_event_id = ?target_user_membership_event_id,
membership=?target_user_current_membership,
"sender cannot join a knock room without being invited or already joined"
"Join rule is knock but membership does not allow join"
);
false
} else {
trace!(sender=%sender, "sender is invited or already joined to room, allowing join");
true
},
| JoinRule::KnockRestricted(_) if !room_version.knock_restricted_join_rule =>
@@ -861,56 +811,38 @@ struct GetThirdPartyInvite {
false
},
| JoinRule::KnockRestricted(_) => {
if membership_allows_join || user_for_join_auth_is_valid {
trace!(
%sender,
%membership_allows_join,
%user_for_join_auth_is_valid,
"sender is invited, already joined to, or authorised to join the room, allowing join"
);
let valid_join = user_for_join_auth_is_valid
|| sender_membership == MembershipState::Join;
if membership_allows_join || valid_join {
true
} else {
warn!(
%sender,
membership_event_id = ?target_user_membership_event_id,
membership=?target_user_current_membership,
%user_for_join_auth_is_valid,
?user_for_join_auth,
"sender cannot join as they are not invited nor already joined to the room, nor was a \
valid authorising user given to permit the join"
"Join rule is a restricted one, but no valid authorising user \
was given and the sender's current membership does not permit \
a join transition"
);
false
}
},
| JoinRule::Restricted(_) =>
if membership_allows_join || user_for_join_auth_is_valid {
trace!(
%sender,
%membership_allows_join,
%user_for_join_auth_is_valid,
"sender is invited, already joined to, or authorised to join the room, allowing join"
);
true
} else {
if !user_for_join_auth_is_valid
&& sender_membership != MembershipState::Join
{
warn!(
%sender,
membership_event_id = ?target_user_membership_event_id,
membership=?target_user_current_membership,
%user_for_join_auth_is_valid,
?user_for_join_auth,
"sender cannot join as they are not invited nor already joined to the room, nor was a \
valid authorising user given to permit the join"
"Join rule is a restricted one but no valid authorising user \
was given"
);
false
} else {
true
},
| JoinRule::Public => {
trace!(%sender, "join rule is public, allowing join");
true
},
| JoinRule::Public => true,
| _ => {
warn!(
join_rule=?join_rules,
"Join rule is unknown, or the rule's conditions were not met"
membership=?target_user_current_membership,
"Unknown join rule doesn't allow joining, or the rule's conditions were not met"
);
false
},
@@ -937,23 +869,16 @@ struct GetThirdPartyInvite {
}
allow
},
| _ =>
if !sender_is_joined {
warn!(
%sender,
?sender_membership_event_id,
?sender_membership,
"sender cannot produce an invite without being joined to the room",
);
false
} else if matches!(
target_user_current_membership,
MembershipState::Join | MembershipState::Ban
) {
| _ => {
if !sender_is_joined
|| target_user_current_membership == MembershipState::Join
|| target_user_current_membership == MembershipState::Ban
{
warn!(
?target_user_membership_event_id,
?target_user_current_membership,
"cannot invite a user who is banned or already joined",
?sender_membership_event_id,
"Can't invite user if sender not joined or the user is currently \
joined or banned",
);
false
} else {
@@ -963,107 +888,56 @@ struct GetThirdPartyInvite {
.is_some();
if !allow {
warn!(
%sender,
has=?sender_power,
required=?power_levels.invite,
"sender does not have enough power to produce invites",
?target_user_membership_event_id,
?power_levels_event_id,
"User does not have enough power to invite",
);
}
trace!(
%sender,
?sender_membership_event_id,
?sender_membership,
?target_user_membership_event_id,
?target_user_current_membership,
sender_pl=?sender_power,
required_pl=?power_levels.invite,
"allowing invite"
);
allow
},
}
},
}
},
| MembershipState::Leave => {
let can_unban = if target_user_current_membership == MembershipState::Ban {
sender_creator || sender_power.filter(|&p| p < &power_levels.ban).is_some()
} else {
true
};
let can_kick = if !matches!(
target_user_current_membership,
MembershipState::Ban | MembershipState::Leave
) {
sender_creator || sender_power.filter(|&p| p < &power_levels.kick).is_some()
} else {
true
};
| MembershipState::Leave =>
if sender == target_user {
// self-leave
// let allow = target_user_current_membership == MembershipState::Join
// || target_user_current_membership == MembershipState::Invite
// || target_user_current_membership == MembershipState::Knock;
let allow = matches!(
target_user_current_membership,
MembershipState::Join | MembershipState::Invite | MembershipState::Knock
);
let allow = target_user_current_membership == MembershipState::Join
|| target_user_current_membership == MembershipState::Invite
|| target_user_current_membership == MembershipState::Knock;
if !allow {
warn!(
%sender,
current_membership_event_id=?target_user_membership_event_id,
current_membership=?target_user_current_membership,
"sender cannot leave as they are not already knocking on, invited to, or joined to the room"
?target_user_membership_event_id,
?target_user_current_membership,
"Can't leave if sender is not already invited, knocked, or joined"
);
}
trace!(sender=%sender, "allowing leave");
allow
} else if !sender_is_joined {
} else if !sender_is_joined
|| target_user_current_membership == MembershipState::Ban
&& (sender_creator
|| sender_power.filter(|&p| p < &power_levels.ban).is_some())
{
warn!(
%sender,
?target_user_membership_event_id,
?sender_membership_event_id,
"sender cannot kick another user as they are not joined to the room",
);
false
} else if !can_unban {
// If the target is banned, only a room creator or someone with ban power
// level can unban them
warn!(
%sender,
?target_user_membership_event_id,
?power_levels_event_id,
"sender lacks the power level required to unban users",
);
false
} else if !can_kick {
warn!(
%sender,
%target_user,
?target_user_membership_event_id,
?target_user_current_membership,
?power_levels_event_id,
"sender does not have enough power to kick the target",
"Can't kick if sender not joined or user is already banned",
);
false
} else {
trace!(
%sender,
%target_user,
?target_user_membership_event_id,
?target_user_current_membership,
sender_pl=?sender_power,
target_pl=?target_power,
required_pl=?power_levels.kick,
"allowing kick/unban",
);
true
}
},
let allow = sender_creator
|| (sender_power.filter(|&p| p >= &power_levels.kick).is_some()
&& target_power < sender_power);
if !allow {
warn!(
?target_user_membership_event_id,
?power_levels_event_id,
"User does not have enough power to kick",
);
}
allow
},
| MembershipState::Ban =>
if !sender_is_joined {
warn!(
%sender,
?sender_membership_event_id,
"sender cannot ban another user as they are not joined to the room",
);
warn!(?sender_membership_event_id, "Can't ban user if sender is not joined");
false
} else {
let allow = sender_creator
@@ -1071,11 +945,9 @@ struct GetThirdPartyInvite {
&& target_power < sender_power);
if !allow {
warn!(
%sender,
%target_user,
?target_user_membership_event_id,
?power_levels_event_id,
"sender does not have enough power to ban the target",
"User does not have enough power to ban",
);
}
allow
@@ -1101,9 +973,9 @@ struct GetThirdPartyInvite {
} else if sender != target_user {
// 3. If `sender` does not match `state_key`, reject.
warn!(
%sender,
%target_user,
"sender cannot knock on behalf of another user",
?sender,
?target_user,
"Can't make another user knock, sender did not match target"
);
false
} else if matches!(
@@ -1115,25 +987,15 @@ struct GetThirdPartyInvite {
// 5. Otherwise, reject.
warn!(
?target_user_membership_event_id,
?sender_membership,
"Knocking with a membership state of ban, invite or join is invalid",
);
false
} else {
trace!(%sender, "allowing knock");
true
}
},
| _ => {
warn!(
%sender,
?target_membership,
%target_user,
%target_user_current_membership,
"Unknown or invalid membership transition {} -> {}",
target_user_current_membership,
target_membership
);
warn!("Unknown membership transition");
false
},
})
@@ -1163,13 +1025,6 @@ fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int)
if event.state_key().is_some_and(|k| k.starts_with('@'))
&& event.state_key() != Some(event.sender().as_str())
{
warn!(
%user_level,
required=?event_type_power_level,
state_key=?event.state_key(),
sender=%event.sender(),
"state_key starts with @ but does not match sender",
);
return false; // permission required to post in this room
}
@@ -1254,14 +1109,7 @@ fn check_power_levels(
// If the current value is equal to the sender's current power level, reject
if user != power_event.sender() && old_level == Some(&user_level) {
warn!(
?old_level,
?new_level,
?user,
%user_level,
sender=%power_event.sender(),
"cannot alter the power level of a user with the same power level as sender's own"
);
warn!("m.room.power_level cannot remove ops == to own");
return Some(false); // cannot remove ops level == to own
}
@@ -1269,26 +1117,8 @@ fn check_power_levels(
// If the new value is higher than the sender's current power level, reject
let old_level_too_big = old_level > Some(&user_level);
let new_level_too_big = new_level > Some(&user_level);
if old_level_too_big {
warn!(
?old_level,
?new_level,
?user,
%user_level,
sender=%power_event.sender(),
"cannot alter the power level of a user with a higher power level than sender's own"
);
return Some(false); // cannot add ops greater than own
}
if new_level_too_big {
warn!(
?old_level,
?new_level,
?user,
%user_level,
sender=%power_event.sender(),
"cannot set the power level of a user to a level higher than sender's own"
);
if old_level_too_big || new_level_too_big {
warn!("m.room.power_level failed to add ops > than own");
return Some(false); // cannot add ops greater than own
}
}
@@ -1305,26 +1135,8 @@ fn check_power_levels(
// If the new value is higher than the sender's current power level, reject
let old_level_too_big = old_level > Some(&user_level);
let new_level_too_big = new_level > Some(&user_level);
if old_level_too_big {
warn!(
?old_level,
?new_level,
?ev_type,
%user_level,
sender=%power_event.sender(),
"cannot alter the power level of an event with a higher power level than sender's own"
);
return Some(false); // cannot add ops greater than own
}
if new_level_too_big {
warn!(
?old_level,
?new_level,
?ev_type,
%user_level,
sender=%power_event.sender(),
"cannot set the power level of an event to a level higher than sender's own"
);
if old_level_too_big || new_level_too_big {
warn!("m.room.power_level failed to add ops > than own");
return Some(false); // cannot add ops greater than own
}
}
@@ -1339,13 +1151,7 @@ fn check_power_levels(
let old_level_too_big = old_level > user_level;
let new_level_too_big = new_level > user_level;
if old_level_too_big || new_level_too_big {
warn!(
?old_level,
?new_level,
%user_level,
sender=%power_event.sender(),
"cannot alter the power level of notifications greater than sender's own"
);
warn!("m.room.power_level failed to add ops > than own");
return Some(false); // cannot add ops greater than own
}
}
@@ -1369,14 +1175,7 @@ fn check_power_levels(
let new_level_too_big = new_lvl > user_level;
if old_level_too_big || new_level_too_big {
warn!(
?old_lvl,
?new_lvl,
%user_level,
sender=%power_event.sender(),
action=%lvl_name,
"cannot alter the power level of action greater than sender's own",
);
warn!("cannot add ops > than own");
return Some(false);
}
}

View File

@@ -36,7 +36,7 @@
room_version::RoomVersion,
};
use crate::{
debug, debug_error, err,
debug, debug_error,
matrix::{Event, StateKey},
state_res::room_version::StateResolutionVersion,
trace,
@@ -319,19 +319,8 @@ async fn calculate_conflicted_subgraph<F, Fut, E>(
path.pop();
continue;
}
trace!(event_id = event_id.as_str(), "fetching event for its auth events");
let evt = fetch_event(event_id.clone()).await;
if evt.is_none() {
err!("could not fetch event {} to calculate conflicted subgraph", event_id);
path.pop();
continue;
}
stack.push(
evt.expect("checked")
.auth_events()
.map(ToOwned::to_owned)
.collect(),
);
let evt = fetch_event(event_id.clone()).await?;
stack.push(evt.auth_events().map(ToOwned::to_owned).collect());
seen.insert(event_id);
}
Some(subgraph)
@@ -1078,8 +1067,7 @@ async fn test_event_sort() {
);
}
// NOTE(2025-09-17): Disabled due to unknown "create event must exist" bug
// #[tokio::test]
#[tokio::test]
async fn test_sort() {
for _ in 0..20 {
// since we shuffle the eventIds before we sort them introducing randomness
@@ -1088,8 +1076,7 @@ async fn test_sort() {
}
}
// NOTE(2025-09-17): Disabled due to unknown "create event must exist" bug
//#[tokio::test]
#[tokio::test]
async fn ban_vs_power_level() {
let _ = tracing::subscriber::set_default(
tracing_subscriber::fmt().with_test_writer().finish(),

View File

@@ -417,7 +417,7 @@ fn deserialize_ignored_any<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Valu
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
debug_assert_eq!(
conduwuit::debug::type_name::<V>(),
"serde_json::value::de::<impl serde_core::de::Deserialize for \
"serde_json::value::de::<impl serde::de::Deserialize for \
serde_json::value::Value>::deserialize::ValueVisitor",
"deserialize_any: type not expected"
);

View File

@@ -434,8 +434,4 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
name: "userroomid_notificationcount",
..descriptor::RANDOM
},
Descriptor {
name: "userroomid_invitesender",
..descriptor::RANDOM_SMALL
},
];

View File

@@ -22,13 +22,11 @@ crate-type = [
]
[package.metadata.deb]
name = "continuwuity"
maintainer = "continuwuity developers <contact@continuwuity.org>"
copyright = "2024, continuwuity developers"
name = "conduwuit"
maintainer = "strawberry <strawberry@puppygock.gay>"
copyright = "2024, strawberry <strawberry@puppygock.gay>"
license-file = ["../../LICENSE", "3"]
depends = "$auto, ca-certificates"
breaks = ["conduwuit (<<0.5.0)"]
replaces = ["conduwuit (<<0.5.0)"]
extended-description = """\
a cool hard fork of Conduit, a Matrix homeserver written in Rust"""
section = "net"
@@ -156,7 +154,6 @@ sentry_telemetry = [
]
systemd = [
"conduwuit-router/systemd",
"conduwuit-service/systemd"
]
journald = [ # This is a stub on non-unix platforms
"dep:tracing-journald",

View File

@@ -40,6 +40,7 @@ io_uring = [
"conduwuit-admin/io_uring",
"conduwuit-api/io_uring",
"conduwuit-service/io_uring",
"conduwuit-api/io_uring",
]
jemalloc = [
"conduwuit-admin/jemalloc",

View File

@@ -65,7 +65,7 @@ pub(crate) async fn start(server: Arc<Server>) -> Result<Arc<Services>> {
let services = Services::build(server).await?.start().await?;
#[cfg(all(feature = "systemd", target_os = "linux"))]
sd_notify::notify(false, &[sd_notify::NotifyState::Ready])
sd_notify::notify(true, &[sd_notify::NotifyState::Ready])
.expect("failed to notify systemd of ready state");
debug!("Started");
@@ -78,7 +78,7 @@ pub(crate) async fn stop(services: Arc<Services>) -> Result<()> {
debug!("Shutting down...");
#[cfg(all(feature = "systemd", target_os = "linux"))]
sd_notify::notify(false, &[sd_notify::NotifyState::Stopping])
sd_notify::notify(true, &[sd_notify::NotifyState::Stopping])
.expect("failed to notify systemd of stopping state");
// Wait for all completions before dropping or we'll lose them to the module

View File

@@ -67,9 +67,6 @@ release_max_log_level = [
"tracing/max_level_trace",
"tracing/release_max_level_info",
]
systemd = [
"dep:sd-notify",
]
url_preview = [
"dep:image",
"dep:webpage",
@@ -122,9 +119,5 @@ blurhash.optional = true
recaptcha-verify = { version = "0.1.5", default-features = false }
ctor.workspace = true
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
sd-notify.workspace = true
sd-notify.optional = true
[lints]
workspace = true

View File

@@ -45,16 +45,13 @@ fn deref(&self) -> &Self::Target { &self.server.config }
fn handle_reload(&self) -> Result {
if self.server.config.config_reload_signal {
#[cfg(all(feature = "systemd", target_os = "linux"))]
sd_notify::notify(false, &[
sd_notify::NotifyState::Reloading,
sd_notify::NotifyState::monotonic_usec_now().expect("Failed to read monotonic time"),
])
.expect("failed to notify systemd of reloading state");
sd_notify::notify(true, &[sd_notify::NotifyState::Reloading])
.expect("failed to notify systemd of reloading state");
self.reload(iter::empty())?;
#[cfg(all(feature = "systemd", target_os = "linux"))]
sd_notify::notify(false, &[sd_notify::NotifyState::Ready])
sd_notify::notify(true, &[sd_notify::NotifyState::Ready])
.expect("failed to notify systemd of ready state");
}

View File

@@ -90,22 +90,17 @@ pub async fn create(
file: &[u8],
) -> Result<()> {
// Width, Height = 0 if it's not a thumbnail
let key = self
.db
.create_file_metadata(mxc, user, &Dim::default(), content_disposition, content_type)
.map_err(|e| {
err!(Database(error!("Failed to create media metadata for MXC {mxc}: {e}")))
})?;
let key = self.db.create_file_metadata(
mxc,
user,
&Dim::default(),
content_disposition,
content_type,
)?;
//TODO: Dangling metadata in database if creation fails
let mut f = self.create_media_file(&key).await.map_err(|e| {
err!(Database(error!(
"Failed to create media file for MXC {mxc} at key {key:?}: {e}"
)))
})?;
f.write_all(file).await.map_err(|e| {
err!(Database(error!("Failed to write media file for MXC {mxc} at key {key:?}: {e}")))
})?;
let mut f = self.create_media_file(&key).await?;
f.write_all(file).await?;
Ok(())
}

View File

@@ -9,8 +9,7 @@
},
warn,
};
use database::Json;
use futures::{FutureExt, StreamExt, TryStreamExt};
use futures::{FutureExt, StreamExt};
use itertools::Itertools;
use ruma::{
OwnedUserId, RoomId, UserId,
@@ -139,14 +138,6 @@ async fn migrate(services: &Services) -> Result<()> {
info!("Migration: Bumped database version to 17");
}
if db["global"]
.get(FIXED_CORRUPT_MSC4133_FIELDS_MARKER)
.await
.is_not_found()
{
fix_corrupt_msc4133_fields(services).await?;
}
if services.globals.db.database_version().await < 18 {
services.globals.db.bump_database_version(18);
info!("Migration: Bumped database version to 18");
@@ -573,54 +564,3 @@ async fn fix_readreceiptid_readreceipt_duplicates(services: &Services) -> Result
db["global"].insert(b"fix_readreceiptid_readreceipt_duplicates", []);
db.db.sort()
}
const FIXED_CORRUPT_MSC4133_FIELDS_MARKER: &[u8] = b"fix_corrupt_msc4133_fields";
async fn fix_corrupt_msc4133_fields(services: &Services) -> Result {
use serde_json::{Value, from_slice};
type KeyVal<'a> = ((OwnedUserId, String), &'a [u8]);
warn!("Fixing corrupted `us.cloke.msc4175.tz` fields...");
let db = &services.db;
let cork = db.cork_and_sync();
let useridprofilekey_value = db["useridprofilekey_value"].clone();
let (total, fixed) = useridprofilekey_value
.stream()
.try_fold(
(0_usize, 0_usize),
async |(mut total, mut fixed),
((user, key), value): KeyVal<'_>|
-> Result<(usize, usize)> {
if let Err(error) = from_slice::<Value>(value) {
// Due to an old bug, some conduwuit databases have `us.cloke.msc4175.tz` user
// profile fields with raw strings instead of quoted JSON ones.
// This migration fixes that.
let new_value = if key == "us.cloke.msc4175.tz" {
Value::String(String::from_utf8(value.to_vec())?)
} else {
return Err!(
"failed to deserialize msc4133 key {} of user {}: {}",
key,
user,
error
);
};
useridprofilekey_value.put((user, key), Json(new_value));
fixed = fixed.saturating_add(1);
}
total = total.saturating_add(1);
Ok((total, fixed))
},
)
.await?;
drop(cork);
info!(?total, ?fixed, "Fixed corrupted `us.cloke.msc4175.tz` fields.");
db["global"].insert(FIXED_CORRUPT_MSC4133_FIELDS_MARKER, []);
db.db.sort()?;
Ok(())
}

View File

@@ -4,8 +4,9 @@
};
use conduwuit::{
Event, PduEvent, debug, debug_warn, implement, matrix::event::gen_event_id_canonical_json,
trace, utils::continue_exponential_backoff_secs, warn,
Event, PduEvent, debug, debug_error, debug_warn, implement,
matrix::event::gen_event_id_canonical_json, trace, utils::continue_exponential_backoff_secs,
warn,
};
use ruma::{
CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName,
@@ -51,14 +52,12 @@ pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
};
let mut events_with_auth_events = Vec::with_capacity(events.clone().count());
trace!("Fetching {} outlier pdus", events.clone().count());
for id in events {
// a. Look in the main timeline (pduid_pdu tree)
// b. Look at outlier pdu tree
// (get_pdu_json checks both)
if let Ok(local_pdu) = self.services.timeline.get_pdu(id).await {
trace!("Found {id} in main timeline or outlier tree");
events_with_auth_events.push((id.to_owned(), Some(local_pdu), vec![]));
continue;
}
@@ -105,7 +104,7 @@ pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
continue;
}
debug!("Fetching {next_id} over federation from {origin}.");
debug!("Fetching {next_id} over federation.");
match self
.services
.sending
@@ -116,7 +115,7 @@ pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
.await
{
| Ok(res) => {
debug!("Got {next_id} over federation from {origin}");
debug!("Got {next_id} over federation");
let Ok(room_version_id) = get_room_version_id(create_event) else {
back_off((*next_id).to_owned());
continue;
@@ -146,9 +145,6 @@ pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
auth_event.clone().into(),
) {
| Ok(auth_event) => {
trace!(
"Found auth event id {auth_event} for event {next_id}"
);
todo_auth_events.push_back(auth_event);
},
| _ => {
@@ -164,7 +160,7 @@ pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
events_all.insert(next_id);
},
| Err(e) => {
warn!("Failed to fetch auth event {next_id} from {origin}: {e}");
debug_error!("Failed to fetch event {next_id}: {e}");
back_off((*next_id).to_owned());
},
}
@@ -179,7 +175,7 @@ pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
// b. Look at outlier pdu tree
// (get_pdu_json checks both)
if let Some(local_pdu) = local_pdu {
trace!("Found {id} in main timeline or outlier tree");
trace!("Found {id} in db");
pdus.push((local_pdu.clone(), None));
}
@@ -205,7 +201,6 @@ pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
}
}
trace!("Handling outlier {next_id}");
match Box::pin(self.handle_outlier_pdu(
origin,
create_event,
@@ -218,7 +213,6 @@ pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
{
| Ok((pdu, json)) =>
if next_id == *id {
trace!("Handled outlier {next_id} (original request)");
pdus.push((pdu, Some(json)));
},
| Err(e) => {
@@ -228,6 +222,6 @@ pub(super) async fn fetch_and_handle_outliers<'a, Pdu, Events>(
}
}
}
trace!("Fetched and handled {} outlier pdus", pdus.len());
pdus
}

View File

@@ -1,12 +1,11 @@
use std::collections::{BTreeMap, HashMap, hash_map};
use conduwuit::{
Err, Event, PduEvent, Result, debug, debug_info, debug_warn, err, implement, state_res, trace,
Err, Event, PduEvent, Result, debug, debug_info, err, implement, state_res, trace, warn,
};
use futures::future::ready;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName,
events::StateEventType,
CanonicalJsonObject, CanonicalJsonValue, EventId, RoomId, ServerName, events::StateEventType,
};
use super::{check_room_id, get_room_version_id, to_room_version};
@@ -75,73 +74,36 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
check_room_id(room_id, &pdu_event)?;
// Fetch all auth events
let mut auth_events: HashMap<OwnedEventId, PduEvent> = HashMap::new();
for aid in pdu_event.auth_events() {
if let Ok(auth_event) = self.services.timeline.get_pdu(aid).await {
check_room_id(room_id, &auth_event)?;
trace!("Found auth event {aid} for outlier event {event_id} locally");
auth_events.insert(aid.to_owned(), auth_event);
} else {
debug_warn!("Could not find auth event {aid} for outlier event {event_id} locally");
}
}
// Fetch any missing ones & reject invalid ones
let missing_auth_events = if auth_events_known {
pdu_event
.auth_events()
.filter(|id| !auth_events.contains_key(*id))
.collect::<Vec<_>>()
} else {
pdu_event.auth_events().collect::<Vec<_>>()
};
if !missing_auth_events.is_empty() || !auth_events_known {
debug_info!(
"Fetching {} missing auth events for outlier event {event_id}",
missing_auth_events.len()
);
for (pdu, _) in self
.fetch_and_handle_outliers(
origin,
missing_auth_events.iter().copied(),
create_event,
room_id,
)
.await
{
auth_events.insert(pdu.event_id().to_owned(), pdu);
}
} else {
debug!("No missing auth events for outlier event {event_id}");
}
// reject if we are still missing some
let still_missing = pdu_event
.auth_events()
.filter(|id| !auth_events.contains_key(*id))
.collect::<Vec<_>>();
if !still_missing.is_empty() {
return Err!(Request(InvalidParam(
"Could not fetch all auth events for outlier event {event_id}, still missing: \
{still_missing:?}"
)));
if !auth_events_known {
// 4. fetch any missing auth events doing all checks listed here starting at 1.
// These are not timeline events
// 5. Reject "due to auth events" if can't get all the auth events or some of
// the auth events are also rejected "due to auth events"
// NOTE: Step 5 is not applied anymore because it failed too often
debug!("Fetching auth events");
Box::pin(self.fetch_and_handle_outliers(
origin,
pdu_event.auth_events(),
create_event,
room_id,
))
.await;
}
// 6. Reject "due to auth events" if the event doesn't pass auth based on the
// auth events
debug!("Checking based on auth events");
let mut auth_events_by_key: HashMap<_, _> = HashMap::with_capacity(auth_events.len());
// Build map of auth events
let mut auth_events = HashMap::with_capacity(pdu_event.auth_events().count());
for id in pdu_event.auth_events() {
let auth_event = auth_events
.get(id)
.expect("we just checked that we have all auth events")
.to_owned();
let Ok(auth_event) = self.services.timeline.get_pdu(id).await else {
warn!("Could not find auth event {id}");
continue;
};
check_room_id(room_id, &auth_event)?;
match auth_events_by_key.entry((
match auth_events.entry((
auth_event.kind.to_string().into(),
auth_event
.state_key
@@ -161,7 +123,7 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
// The original create event must be in the auth events
if !matches!(
auth_events_by_key.get(&(StateEventType::RoomCreate, String::new().into())),
auth_events.get(&(StateEventType::RoomCreate, String::new().into())),
Some(_) | None
) {
return Err!(Request(InvalidParam("Incoming event refers to wrong create event.")));
@@ -169,7 +131,7 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
let state_fetch = |ty: &StateEventType, sk: &str| {
let key = (ty.to_owned(), sk.into());
ready(auth_events_by_key.get(&key).map(ToOwned::to_owned))
ready(auth_events.get(&key).map(ToOwned::to_owned))
};
let auth_check = state_res::event_auth::auth_check(

View File

@@ -12,7 +12,7 @@
use database::{Deserialized, Ignore, Interfix, Map};
use futures::{Stream, StreamExt, future::join5, pin_mut};
use ruma::{
OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId,
OwnedRoomId, RoomId, ServerName, UserId,
events::{AnyStrippedStateEvent, AnySyncStateEvent, room::member::MembershipState},
serde::Raw,
};
@@ -49,7 +49,6 @@ struct Data {
userroomid_joined: Arc<Map>,
userroomid_leftstate: Arc<Map>,
userroomid_knockedstate: Arc<Map>,
userroomid_invitesender: Arc<Map>,
}
type AppServiceInRoomCache = SyncRwLock<HashMap<OwnedRoomId, HashMap<String, bool>>>;
@@ -84,7 +83,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
userroomid_joined: args.db["userroomid_joined"].clone(),
userroomid_leftstate: args.db["userroomid_leftstate"].clone(),
userroomid_knockedstate: args.db["userroomid_knockedstate"].clone(),
userroomid_invitesender: args.db["userroomid_invitesender"].clone(),
},
}))
}
@@ -525,14 +523,3 @@ pub async fn is_left(&self, user_id: &UserId, room_id: &RoomId) -> bool {
let key = (user_id, room_id);
self.db.userroomid_leftstate.qry(&key).await.is_ok()
}
#[implement(Service)]
#[tracing::instrument(skip(self), level = "trace")]
pub async fn invite_sender(&self, user_id: &UserId, room_id: &RoomId) -> Result<OwnedUserId> {
let key = (user_id, room_id);
self.db
.userroomid_invitesender
.qry(&key)
.await
.deserialized()
}

View File

@@ -1,6 +1,6 @@
use std::collections::HashSet;
use conduwuit::{Err, Result, implement, is_not_empty, utils::ReadyExt, warn};
use conduwuit::{Result, implement, is_not_empty, utils::ReadyExt, warn};
use database::{Json, serialize_key};
use futures::StreamExt;
use ruma::{
@@ -9,7 +9,6 @@
AnyStrippedStateEvent, AnySyncStateEvent, GlobalAccountDataEventType,
RoomAccountDataEventType, StateEventType,
direct::DirectEvent,
invite_permission_config::FilterLevel,
room::{
create::RoomCreateEventContent,
member::{MembershipState, RoomMemberEventContent},
@@ -122,21 +121,12 @@ pub async fn update_membership(
self.mark_as_joined(user_id, room_id);
},
| MembershipState::Invite => {
// return an error for blocked invites. ignored invites aren't handled here
// since the recipient's membership should still be changed to `invite`.
// they're filtered out in the individual /sync handlers
if matches!(
self.services
.users
.invite_filter_level(sender, user_id)
.await,
FilterLevel::Block
) {
return Err!(Request(InviteBlocked(
"{user_id} has blocked invites from {sender}."
)));
// We want to know if the sender is ignored by the receiver
if self.services.users.user_is_ignored(sender, user_id).await {
return Ok(());
}
self.mark_as_invited(user_id, room_id, sender, last_state, invite_via)
self.mark_as_invited(user_id, room_id, last_state, invite_via)
.await;
},
| MembershipState::Leave | MembershipState::Ban => {
@@ -241,7 +231,6 @@ pub fn mark_as_joined(&self, user_id: &UserId, room_id: &RoomId) {
self.db.userroomid_invitestate.remove(&userroom_id);
self.db.roomuserid_invitecount.remove(&roomuser_id);
self.db.userroomid_invitesender.remove(&userroom_id);
self.db.userroomid_leftstate.remove(&userroom_id);
self.db.roomuserid_leftcount.remove(&roomuser_id);
@@ -279,7 +268,6 @@ pub fn mark_as_left(&self, user_id: &UserId, room_id: &RoomId) {
self.db.userroomid_invitestate.remove(&userroom_id);
self.db.roomuserid_invitecount.remove(&roomuser_id);
self.db.userroomid_invitesender.remove(&userroom_id);
self.db.userroomid_knockedstate.remove(&userroom_id);
self.db.roomuserid_knockedcount.remove(&roomuser_id);
@@ -316,7 +304,6 @@ pub fn mark_as_knocked(
self.db.userroomid_invitestate.remove(&userroom_id);
self.db.roomuserid_invitecount.remove(&roomuser_id);
self.db.userroomid_invitesender.remove(&userroom_id);
self.db.userroomid_leftstate.remove(&userroom_id);
self.db.roomuserid_leftcount.remove(&roomuser_id);
@@ -348,7 +335,6 @@ pub async fn mark_as_invited(
&self,
user_id: &UserId,
room_id: &RoomId,
sender_user: &UserId,
last_state: Option<Vec<Raw<AnyStrippedStateEvent>>>,
invite_via: Option<Vec<OwnedServerName>>,
) {
@@ -364,9 +350,6 @@ pub async fn mark_as_invited(
self.db
.roomuserid_invitecount
.raw_aput::<8, _, _>(&roomuser_id, self.services.globals.next_count().unwrap());
self.db
.userroomid_invitesender
.raw_put(&userroom_id, sender_user);
self.db.userroomid_joined.remove(&userroom_id);
self.db.roomuserid_joined.remove(&roomuser_id);

View File

@@ -274,6 +274,8 @@ fn from_evt(
pdu_json.insert("event_id".into(), CanonicalJsonValue::String(pdu.event_id.clone().into()));
// Check with the policy server
// TODO(hydra): Skip this check for create events (why didnt we do this
// already?)
if room_id.is_some() {
trace!(
"Checking event {} in room {} with policy server",

View File

@@ -20,9 +20,7 @@
api::client::{device::Device, error::ErrorKind, filter::FilterDefinition},
encryption::{CrossSigningKey, DeviceKeys, OneTimeKey},
events::{
AnyToDeviceEvent, GlobalAccountDataEventType,
ignored_user_list::IgnoredUserListEvent,
invite_permission_config::{FilterLevel, InvitePermissionConfigEvent},
AnyToDeviceEvent, GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent,
},
serde::Raw,
};
@@ -141,26 +139,6 @@ pub async fn user_is_ignored(&self, sender_user: &UserId, recipient_user: &UserI
})
}
/// Returns the recipient's filter level for an invite from the sender.
pub async fn invite_filter_level(
&self,
sender_user: &UserId,
recipient_user: &UserId,
) -> FilterLevel {
if self.user_is_ignored(sender_user, recipient_user).await {
FilterLevel::Ignore
} else {
self.services
.account_data
.get_global(recipient_user, GlobalAccountDataEventType::InvitePermissionConfig)
.await
.map(|config: InvitePermissionConfigEvent| {
config.content.user_filter_level(sender_user)
})
.unwrap_or(FilterLevel::Allow)
}
}
/// Check if a user is an admin
#[inline]
pub async fn is_admin(&self, user_id: &UserId) -> bool {
@@ -1124,6 +1102,34 @@ pub async fn find_from_login_token(&self, token: &str) -> Result<OwnedUserId> {
Ok(user_id)
}
#[inline]
fn parse_profile_kv(
&self,
user_id: &UserId,
key: &str,
value: Vec<u8>,
) -> Result<serde_json::Value> {
match serde_json::from_slice(&value) {
| Ok(value) => Ok(value),
| Err(error) => {
// Due to an old bug, some conduwuit databases have `us.cloke.msc4175.tz` user
// profile fields with raw strings instead of quoted JSON ones.
if key == "us.cloke.msc4175.tz" {
// TODO insert a hint about this being a cold path
debug_warn!(
"Fixing corrupt `us.cloke.msc4175.tz` field in the profile of {}",
user_id
);
let raw_tz = serde_json::Value::String(String::from_utf8(value)?);
self.set_profile_key(user_id, "us.cloke.msc4175.tz", Some(raw_tz.clone()));
Ok(raw_tz)
} else {
Err(error.into())
}
},
}
}
/// Gets a specific user profile key
pub async fn profile_key(
&self,
@@ -1135,7 +1141,7 @@ pub async fn profile_key(
.useridprofilekey_value
.qry(&key)
.await
.and_then(|handle| serde_json::from_slice(&handle).map_err(Into::into))
.and_then(|handle| self.parse_profile_kv(user_id, profile_key, handle.to_vec()))
}
/// Gets all the user's profile keys and values in an iterator
@@ -1150,7 +1156,10 @@ pub fn all_profile_keys<'a>(
.useridprofilekey_value
.stream_prefix(&prefix)
.ignore_err()
.map(|((_, key), value): KeyVal<'_>| Ok((key, serde_json::from_slice(value)?)))
.map(|((_, key), value): KeyVal<'_>| {
let value = self.parse_profile_kv(user_id, &key, value.to_vec())?;
Ok((key, value))
})
.ignore_err()
}