Compare commits

..

59 Commits

Author SHA1 Message Date
Renovate Bot 13adea6498 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.118.1 2025-09-19 10:31:58 +00:00
Renovate Bot 17d0bb6cf6 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.117.0 2025-09-18 21:06:35 +00:00
Renovate Bot 6dc5051fa6 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.116.10 2025-09-18 19:26:39 +00:00
Renovate Bot 3034c03ad1 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.116.8 2025-09-18 13:36:19 +00:00
Renovate Bot fa6f549d39 chore(deps): lock file maintenance 2025-09-18 13:32:26 +00:00
Renovate Bot 999217b0f6 chore(deps): update dependency cargo-bins/cargo-binstall to v1.15.5 2025-09-18 13:31:48 +00:00
Renovate Bot 74fccff2cc chore(deps): update github-actions-non-major 2025-09-18 13:31:19 +00:00
Shuroii 7a56a2462c fix(ci): Use github env namespace as forgejo is still unsupported 2025-09-18 13:30:50 +00:00
Ginger 458811f241 fix: Fix nexy's very accurate and not-at-all busted fix to my fix 2025-09-17 20:04:50 -04:00
nexy7574 0672ce5b88 style: Fix clippy lint errors 2025-09-17 23:54:09 +01:00
Ginger 7f287c7880 fix: Use a database migration to fix corrupted us.cloke.msc4175.tz fields
(cherry picked from commit 4a893ce4cc81487bcf324dccefd8184ddef5b215)
2025-09-17 23:14:07 +01:00
Shuroii 9142978a15 fix: Fully qualify action
This fixes an issue where Forgejo tries to look for code.forgejo.org for the action despite it not being available.
2025-09-17 21:37:50 +00:00
Shuroii a8eb9c47f8 feat(ci): Add a workflow to update flake hashes
This workflow is intended to be ran as dispatch whenever the rocksdb fork changes!
Other than that, it'll run on any toolchain changes (rust-toolchain.toml, Cargo.lock, Cargo.toml) and update the relevant hash accordingly.
2025-09-17 21:37:50 +00:00
nexy7574 9f18cf667a chore: Temporarily disable bad tests 2025-09-17 22:25:04 +01:00
nexy7574 7e4071c117 Implement room v12 (#943)
**Does not yet work!** Currently, state resolution does not correctly resolve conflicting states. Everything else appears to work as expected, so stateres will be fixed soon, then we should be clear for takeoff.

Also: a lot of things currently accept a nullable room ID that really just don't need to. This will need tidying up before merge. Some authentication checks have also been disabled temporarily but nothing important.

A lot of things are tagged with `TODO(hydra)`, those need resolving before merge. External contributors should PR to the `hydra/public` branch, *not* ` main`.

---

This PR should be squash merged.

Reviewed-on: https://forgejo.ellis.link/continuwuation/continuwuity/pulls/943
Co-authored-by: nexy7574 <git@nexy7574.co.uk>
Co-committed-by: nexy7574 <git@nexy7574.co.uk>
2025-09-17 20:46:03 +00:00
Renovate Bot 51423c9d7d chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.115.6 2025-09-17 05:03:46 +00:00
Ginger a0b0ff9d5c fix: Remove legacy check for u. prefix 2025-09-16 11:30:39 +00:00
Ginger 8e27d74c4a fix: Slightly more parallelism 2025-09-16 11:30:39 +00:00
Ginger d6b1055683 fix: Remove needless async marker 2025-09-16 11:30:39 +00:00
Ginger c9117e6ee4 fix: Fix incorrect deserialization of MSC4133 profile fields 2025-09-16 11:30:39 +00:00
Ginger e3415a500d chore: Code cleanup 2025-09-16 11:30:39 +00:00
Ginger e6fd3c970b fix: Nuke explicit references to the MSC4175 tz profile field 2025-09-16 11:30:39 +00:00
Renovate Bot 6b7f35a8b8 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.115.0 2025-09-16 05:01:56 +00:00
Tom Foster a120a4fa95 fix: Handle runner cargo bin path migration in timelord action
Runner images have migrated from /usr/share/rust/.cargo/bin to standard
~/.cargo/bin location. Action now checks old location first and migrates
binaries if found, maintaining compatibility with both paths.

Bump cache key to v3 to ensure fresh binary cache after path changes.
2025-09-15 16:17:32 +01:00
Renovate Bot f872210b20 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.113.4 2025-09-15 05:01:40 +00:00
Renovate Bot 3dd04bd9df chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.113.2 2025-09-14 05:03:21 +00:00
Ginger af45c348a4 fix: Properly deserialize changes to legacy fields made with MSC4133 endpoints 2025-09-14 01:28:08 +00:00
nexy7574 36dabecb82 chore(1014): Include MSC4155 in build features to resolve build errors 2025-09-14 00:53:43 +00:00
nexy7574 50cd1081ba chore(1014): Bump ruwuma 2025-09-14 00:53:43 +00:00
nexy7574 14df55e5c5 style(1014): Remove unnecessary commented code 2025-09-14 00:53:43 +00:00
nexy7574 d9d0d1a465 fix(!1014): Don't prematurely return during registration 2025-09-14 00:53:43 +00:00
Tom Foster 81b6b3547c fix: Resolve Forgejo runner v11 matrix job execution failure
Matrix jobs stopped starting after upgrading from runner v9 to v11 due to
changes in job dependency resolution. Remove redundant define-variables job
that computed static image paths and replace with IMAGE_PATH environment
variable.

Also fix timelord action binary caching for compatibility between different
runner images that install cargo binaries in different locations.
2025-09-13 17:12:09 +01:00
Renovate Bot 0bbc3c4e05 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.112.0 2025-09-12 21:11:13 +00:00
Jade 0f09fa3d31 chore(renovate): Specify automerge strategy 2025-09-12 21:02:25 +00:00
Tom Foster 3d5355dfc3 chore(renovate): Add auto-merge for renovatebot and reorganise package rules
Enable automatic merging of ghcr.io/renovatebot/renovate docker image updates
to reduce manual maintenance overhead.

Reorganise package rules by manager type (cargo, github-actions, docker) and
add missing description for cargo concurrency limit rule to improve config
maintainability.
2025-09-12 17:50:08 +01:00
Renovate Bot 2547eb3a90 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.109.0 2025-09-12 13:29:47 +00:00
Renovate Bot 51ba41823f chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.106.0 2025-09-12 13:23:28 +00:00
Tom Foster 542dff50bd ci: Split Docker builds into sequential release and max-perf stages
Separate fast release builds from slow max-perf builds to optimise runner
utilisation and provide quicker feedback. Release builds complete first with
standard optimisations, followed by Haswell-optimised dragrace builds once
the safe builds pass successfully.

Extract build logic into focused composite actions for better log visibility
in Forgejo UI. Split monolithic build action into prepare-docker-build,
inline docker build step, and upload-docker-artifacts to ensure each phase
completes independently and shows logs immediately.

Creates separate manifests at each stage to avoid waiting for all builds
before publishing.
2025-09-12 12:43:19 +01:00
Tom Foster 9c147b182f ci: Fix BuildKit cache invalidation and add Haswell-optimised builds
The workflow was rebuilding dependencies unnecessarily despite timelord
restoring timestamps because TARGET_CPU and RUST_PROFILE weren't passed
to Docker, creating inconsistent cache keys. Now passes both arguments
for proper cache reuse.

Adds Haswell-optimised builds alongside baseline builds using -march=haswell
for PCLMUL instruction support. Recent build improvements reducing compile
times from 15-20 minutes to ~5 minutes make this additional CPU variant
feasible. Users can pull optimised images with -haswell suffix.
2025-09-11 13:59:43 +01:00
Renovate Bot 7e76ca45c1 chore(deps): lock file maintenance 2025-09-11 12:28:11 +00:00
Tom Foster 5126cb4554 fix: Use forgejo/upload-artifact@v4 for artifact consistency
Follow-on to correct #1009. The previous fix downgraded upload-artifact
to v3 but kept download-artifact@v4, creating incompatible storage
formats that prevented artifact pattern filtering from working.

Update all upload-artifact actions to v4 and adjust renovate
configuration to disable automatic updates for forgejo artifact
actions to maintain version consistency.
2025-09-11 11:57:04 +01:00
Renovate Bot 4d05d0f677 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.99.9 2025-09-11 09:56:48 +00:00
Tom Foster 0673ac1a6c fix: Fix artifact action compatibility and add digest debugging
Resolve upload-artifact v4 GHES compatibility errors by downgrading to v3.
Switch to standard forgejo/download-artifact@v4 for pattern filtering support.
Update renovate configuration to prevent future incompatible upgrades.

Add diagnostic output to digest export step to troubleshoot zero-byte
artifact uploads preventing manifest creation. Include CI triggers for
Element workflow to test changes in pull requests.
2025-09-11 10:44:11 +01:00
Jade Ellis ad11417145 chore(deps): Replace serde_yaml with serde_yml 2025-09-10 20:20:45 +01:00
Renovate Bot 0de904ffe4 chore(deps): update rust crate const-str to 0.7.0 2025-09-10 18:05:00 +00:00
Renovate Bot d74b9de221 chore(deps): update dependency cargo-bins/cargo-binstall to v1.15.4 2025-09-10 17:44:44 +00:00
Renovate Bot e7ac5988cb chore(deps): update https://github.com/actions/setup-node action to v5 2025-09-10 17:06:45 +00:00
Jade Ellis 571f05017c chore: Update resolv git hash 2025-09-10 17:50:37 +01:00
Jade Ellis a339e73eb5 chore: Unify actions versions 2025-09-10 17:39:25 +01:00
Jade Ellis 72b78ed6d4 chore: Fix nightly-only clippy lints 2025-09-10 17:35:17 +01:00
nexy7574 baa89586e2 fix(MSC4277): Undo refuted response changes 2025-09-10 16:25:06 +00:00
nexy7574 7ad8ff2e45 style(MSC4277): Run lints to satisfy checks 2025-09-10 16:25:06 +00:00
nexy7574 2046b1e2f6 feat(MSC4277): Unify reporting endpoint behaviours
* reporting rooms now always returns 200 OK
* reporting an event returns OK if we don't know about the reported event
* removed the score parameter (needs a followup ruwuma update)
2025-09-10 16:25:06 +00:00
Renovate Bot 2cb980cd4c chore(deps): update ghcr.io/renovatebot/renovate docker tag to v41.99.7 2025-09-10 16:16:34 +00:00
Jade Ellis 27e0ef7b2e chore: Update renovate CI
- Fixes some issues with the action - Enables OSV vuln scanning -
Enables updating the dockerfile tool versions
2025-09-10 16:53:59 +01:00
Jade Ellis 7091882887 chore: Update cargo lockfile 2025-09-10 16:47:20 +01:00
Jade Ellis a81546374d ci: Make timelord docker work locally 2025-09-10 16:40:55 +01:00
Tom Foster 7950e2cc7f ci: Refactor timelord action to use git-warp-time fallback
Updates the timelord action to fall back to git-warp-time when the cache
is completely empty, enabling timestamp restoration even on fresh builds.
When git-warp-time is used, performs an unshallow fetch to get full history,
while subsequent runs use normal fetches. Simplifies the interface by making
inputs optional with sensible defaults.

Adds binary caching for timelord-cli and git-warp-time tools to avoid
repeated installations, and updates paths to use /usr/share/rust/.cargo/bin/
for the catthehacker runner image used by the dind profile (may need updating
if/when switching to standard image).

The main timelord restore now happens inside the Dockerfile itself, as Docker
intentionally wipes all file mtimes on COPY/ADD operations.
2025-09-08 08:34:29 +00:00
Renovate Bot 8f186cd770 chore(deps): update https://github.com/renovatebot/github-action action to v43.0.11 2025-09-08 05:02:33 +00:00
96 changed files with 2527 additions and 1522 deletions
@@ -0,0 +1,104 @@
name: create-manifest
description: |
Create and push a multi-platform Docker manifest from individual platform digests.
Handles downloading digests, creating manifest lists, and pushing to registry.
inputs:
digest_pattern:
description: Glob pattern to match digest artifacts (e.g. "digests-linux-{amd64,arm64}")
required: true
tag_suffix:
description: Suffix to add to all Docker tags (e.g. "-maxperf")
required: false
default: ""
images:
description: Container registry images (newline-separated)
required: true
registry_user:
description: Registry username for authentication
required: false
registry_password:
description: Registry password for authentication
required: false
outputs:
version:
description: The version tag created for the manifest
value: ${{ steps.meta.outputs.version }}
tags:
description: All tags created for the manifest
value: ${{ steps.meta.outputs.tags }}
runs:
using: composite
steps:
- name: Download digests
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: forgejo/download-artifact@v4
with:
path: /tmp/digests
pattern: ${{ inputs.digest_pattern }}
merge-multiple: true
- name: Login to builtin registry
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: docker/login-action@v3
with:
registry: ${{ env.BUILTIN_REGISTRY }}
username: ${{ inputs.registry_user }}
password: ${{ inputs.registry_password }}
- name: Set up Docker Buildx
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: docker/setup-buildx-action@v3
with:
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
endpoint: ${{ env.BUILDKIT_ENDPOINT || '' }}
- name: Extract metadata (tags) for Docker
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
id: meta
uses: docker/metadata-action@v5
with:
tags: |
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:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
- name: Create manifest list and push
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
working-directory: /tmp/digests
shell: bash
env:
IMAGES: ${{ inputs.images }}
run: |
IFS=$'\n'
IMAGES_LIST=($IMAGES)
ANNOTATIONS_LIST=($DOCKER_METADATA_OUTPUT_ANNOTATIONS)
TAGS_LIST=($DOCKER_METADATA_OUTPUT_TAGS)
for REPO in "${IMAGES_LIST[@]}"; do
docker buildx imagetools create \
$(for tag in "${TAGS_LIST[@]}"; do echo "--tag"; echo "$tag"; done) \
$(for annotation in "${ANNOTATIONS_LIST[@]}"; do echo "--annotation"; echo "$annotation"; done) \
$(for reference in *; do printf "$REPO@sha256:%s\n" $reference; done)
done
- name: Inspect image
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
shell: bash
env:
IMAGES: ${{ inputs.images }}
run: |
IMAGES_LIST=($IMAGES)
for REPO in "${IMAGES_LIST[@]}"; do
docker buildx imagetools inspect $REPO:${{ steps.meta.outputs.version }}
done
@@ -0,0 +1,169 @@
name: prepare-docker-build
description: |
Prepare the Docker build environment for Continuwuity builds.
Sets up Rust toolchain, Docker Buildx, caching, and extracts metadata for Docker builds.
inputs:
platform:
description: Target platform (e.g. linux/amd64, linux/arm64)
required: true
slug:
description: Platform slug for artifact naming (e.g. linux-amd64, linux-arm64)
required: true
target_cpu:
description: Target CPU architecture (e.g. haswell, empty for base)
required: false
default: ""
profile:
description: Cargo build profile (release or release-max-perf)
required: true
images:
description: Container registry images (newline-separated)
required: true
registry_user:
description: Registry username for authentication
required: false
registry_password:
description: Registry password for authentication
required: false
outputs:
cpu_suffix:
description: CPU suffix for artifact naming
value: ${{ steps.cpu-suffix.outputs.suffix }}
metadata_labels:
description: Docker labels for the image
value: ${{ steps.meta.outputs.labels }}
metadata_annotations:
description: Docker annotations for the image
value: ${{ steps.meta.outputs.annotations }}
runs:
using: composite
steps:
- name: Set CPU suffix variable
id: cpu-suffix
shell: bash
run: |
if [[ -n "${{ inputs.target_cpu }}" ]]; then
echo "suffix=-${{ inputs.target_cpu }}" >> $GITHUB_OUTPUT
echo "CPU_SUFFIX=-${{ inputs.target_cpu }}" >> $GITHUB_ENV
else
echo "suffix=" >> $GITHUB_OUTPUT
echo "CPU_SUFFIX=" >> $GITHUB_ENV
fi
- name: Echo matrix configuration
shell: bash
run: |
echo "Platform: ${{ inputs.platform }}"
echo "Slug: ${{ inputs.slug }}"
echo "Target CPU: ${{ inputs.target_cpu }}"
echo "Profile: ${{ inputs.profile }}"
- name: Install rust
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
id: rust-toolchain
uses: ./.forgejo/actions/rust-toolchain
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
endpoint: ${{ env.BUILDKIT_ENDPOINT || '' }}
- name: Set up QEMU
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
uses: docker/setup-qemu-action@v3
- name: Login to builtin registry
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: docker/login-action@v3
with:
registry: ${{ env.BUILTIN_REGISTRY }}
username: ${{ inputs.registry_user }}
password: ${{ inputs.registry_password }}
- name: Extract metadata (labels, annotations) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ inputs.images }}
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
- name: Get short git commit SHA
id: sha
shell: bash
run: |
calculatedSha=$(git rev-parse --short ${{ github.sha }})
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
echo "Short SHA: $calculatedSha"
- name: Get Git commit timestamps
shell: bash
run: |
timestamp=$(git log -1 --pretty=%ct)
echo "TIMESTAMP=$timestamp" >> $GITHUB_ENV
echo "Commit timestamp: $timestamp"
- uses: ./.forgejo/actions/timelord
id: timelord
- name: Cache Rust registry
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
uses: actions/cache@v3
with:
path: |
.cargo/git
.cargo/git/checkouts
.cargo/registry
.cargo/registry/src
key: rust-registry-image-${{hashFiles('**/Cargo.lock') }}
- name: Cache cargo target
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
id: cache-cargo-target
uses: actions/cache@v3
with:
path: |
cargo-target${{ env.CPU_SUFFIX }}-${{ inputs.slug }}-${{ inputs.profile }}
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 == '' }}
id: cache-apt
uses: actions/cache@v3
with:
path: |
var-cache-apt-${{ inputs.slug }}
key: var-cache-apt-${{ inputs.slug }}
- name: Cache apt lib
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
id: cache-apt-lib
uses: actions/cache@v3
with:
path: |
var-lib-apt-${{ inputs.slug }}
key: var-lib-apt-${{ inputs.slug }}
- name: inject cache into docker
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
uses: https://github.com/reproducible-containers/buildkit-cache-dance@v3.3.0
with:
cache-map: |
{
".cargo/registry": "/usr/local/cargo/registry",
".cargo/git/db": "/usr/local/cargo/git/db",
"cargo-target${{ env.CPU_SUFFIX }}-${{ inputs.slug }}-${{ inputs.profile }}": {
"target": "/app/target",
"id": "cargo-target${{ env.CPU_SUFFIX }}-${{ inputs.slug }}-${{ inputs.profile }}"
},
"var-cache-apt-${{ inputs.slug }}": "/var/cache/apt",
"var-lib-apt-${{ inputs.slug }}": "/var/lib/apt",
"${{ steps.timelord.outputs.database-path }}":"/timelord"
}
skip-extraction: ${{ steps.cache.outputs.cache-hit }}
+1 -1
View File
@@ -9,7 +9,7 @@ runs:
- name: Install sccache
uses: https://git.tomfos.tr/tom/sccache-action@v1
- name: Configure sccache
uses: https://github.com/actions/github-script@v7
uses: https://github.com/actions/github-script@v8
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
@@ -57,7 +57,7 @@ runs:
- name: Check for LLVM cache
id: cache
uses: https://github.com/actions/cache@v4
uses: actions/cache@v4
with:
path: |
/usr/bin/clang-*
+3 -3
View File
@@ -65,7 +65,7 @@ runs:
- name: Cache Cargo registry and git
id: registry-cache
uses: https://github.com/actions/cache@v4
uses: actions/cache@v4
with:
path: |
.cargo/registry/index
@@ -79,7 +79,7 @@ runs:
- name: Cache toolchain binaries
id: toolchain-cache
uses: https://github.com/actions/cache@v4
uses: actions/cache@v4
with:
path: |
.cargo/bin
@@ -94,7 +94,7 @@ runs:
- name: Cache build artifacts
id: build-cache
uses: https://github.com/actions/cache@v4
uses: actions/cache@v4
with:
path: |
target/**/deps
+102 -28
View File
@@ -1,46 +1,120 @@
name: timelord
description: |
Use timelord to set file timestamps
Use timelord to set file timestamps with git-warp-time fallback for cache misses
inputs:
key:
description: |
The key to use for caching the timelord data.
This should be unique to the repository and the runner.
required: true
default: timelord-v0
required: false
default: ''
path:
description: |
The path to the directory to be timestamped.
This should be the root of the repository.
required: true
default: .
required: false
default: ''
outputs:
database-path:
description: Path to timelord database
value: '${{ env.TIMELORD_CACHE_PATH }}'
runs:
using: composite
steps:
- name: Cache timelord-cli installation
id: cache-timelord-bin
uses: actions/cache@v3
with:
path: ~/.cargo/bin/timelord
key: timelord-cli-v3.0.1
- name: Install timelord-cli
uses: https://github.com/cargo-bins/cargo-binstall@main
if: steps.cache-timelord-bin.outputs.cache-hit != 'true'
- run: cargo binstall timelord-cli@3.0.1
- name: Set defaults
shell: bash
if: steps.cache-timelord-bin.outputs.cache-hit != 'true'
run: |
echo "TIMELORD_KEY=${{ inputs.key || format('timelord-v1-{0}-{1}', github.repository, hashFiles('**/*.rs', '**/Cargo.toml', '**/Cargo.lock')) }}" >> $GITHUB_ENV
echo "TIMELORD_PATH=${{ inputs.path || '.' }}" >> $GITHUB_ENV
echo "TIMELORD_CACHE_PATH=$HOME/.cache/timelord" >> $GITHUB_ENV
echo "PATH=$HOME/.cargo/bin:/usr/share/rust/.cargo/bin:$PATH" >> $GITHUB_ENV
- name: Load timelord files
uses: actions/cache/restore@v3
- name: Restore binary cache
id: binary-cache
uses: actions/cache/restore@v4
with:
path: /timelord/
key: ${{ inputs.key }}
- name: Run timelord to set timestamps
path: |
/usr/share/rust/.cargo/bin
~/.cargo/bin
key: timelord-binaries-v3
- name: Check if binaries need installation
shell: bash
run: timelord sync --source-dir ${{ inputs.path }} --cache-dir /timelord/
- name: Save timelord
uses: actions/cache/save@v3
id: check-binaries
run: |
NEED_INSTALL=false
# Ensure ~/.cargo/bin exists
mkdir -p ~/.cargo/bin
# Check and move timelord if needed
if [ -f /usr/share/rust/.cargo/bin/timelord ] && [ ! -f ~/.cargo/bin/timelord ]; then
echo "Moving timelord from /usr/share/rust/.cargo/bin to ~/.cargo/bin"
mv /usr/share/rust/.cargo/bin/timelord ~/.cargo/bin/
fi
if [ ! -f ~/.cargo/bin/timelord ]; then
echo "timelord-cli not found, needs installation"
NEED_INSTALL=true
fi
# Check and move git-warp-time if needed
if [ -f /usr/share/rust/.cargo/bin/git-warp-time ] && [ ! -f ~/.cargo/bin/git-warp-time ]; then
echo "Moving git-warp-time from /usr/share/rust/.cargo/bin to ~/.cargo/bin"
mv /usr/share/rust/.cargo/bin/git-warp-time ~/.cargo/bin/
fi
if [ ! -f ~/.cargo/bin/git-warp-time ]; then
echo "git-warp-time not found, needs installation"
NEED_INSTALL=true
fi
echo "need-install=$NEED_INSTALL" >> $GITHUB_OUTPUT
- name: Install timelord-cli and git-warp-time
if: steps.check-binaries.outputs.need-install == 'true'
uses: https://github.com/taiki-e/install-action@v2
with:
path: /timelord/
key: ${{ inputs.key }}
tool: git-warp-time,timelord-cli@3.0.1
- name: Save binary cache
if: steps.check-binaries.outputs.need-install == 'true'
uses: actions/cache/save@v4
with:
path: |
/usr/share/rust/.cargo/bin
~/.cargo/bin
key: timelord-binaries-v3
- name: Restore timelord cache with fallbacks
id: timelord-restore
uses: actions/cache/restore@v4
with:
path: ${{ env.TIMELORD_CACHE_PATH }}
key: ${{ env.TIMELORD_KEY }}
restore-keys: |
timelord-v1-${{ github.repository }}-
- name: Initialize timestamps on complete cache miss
if: steps.timelord-restore.outputs.cache-hit != 'true'
shell: bash
run: |
echo "Complete timelord cache miss - running git-warp-time"
git fetch --unshallow
if [ "${{ env.TIMELORD_PATH }}" = "." ]; then
git-warp-time --quiet
else
git-warp-time --quiet ${{ env.TIMELORD_PATH }}
fi
echo "Git timestamps restored"
- name: Run timelord sync
shell: bash
run: |
mkdir -p ${{ env.TIMELORD_CACHE_PATH }}
timelord sync --source-dir ${{ env.TIMELORD_PATH }} --cache-dir ${{ env.TIMELORD_CACHE_PATH }}
- name: Save updated timelord cache immediately
uses: actions/cache/save@v4
with:
path: ${{ env.TIMELORD_CACHE_PATH }}
key: ${{ env.TIMELORD_KEY }}
@@ -0,0 +1,70 @@
name: upload-docker-artifacts
description: |
Upload Docker build artifacts including binary and digest files.
Handles artifact naming and conditional digest uploads for registry publishing.
inputs:
slug:
description: Platform slug for artifact naming (e.g. linux-amd64, linux-arm64)
required: true
cpu_suffix:
description: CPU suffix for artifact naming (e.g. -haswell)
required: false
default: ""
artifact_suffix:
description: Suffix for binary artifacts (e.g. -maxperf)
required: false
default: ""
digest_suffix:
description: Suffix for digest artifacts (e.g. -maxperf)
required: false
default: ""
digest:
description: The digest of the built Docker image
required: true
outputs:
binary_artifact_name:
description: The name of the uploaded binary artifact
value: conduwuit${{ inputs.cpu_suffix }}-${{ inputs.slug }}${{ inputs.artifact_suffix }}
runs:
using: composite
steps:
- name: Export digest
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
shell: bash
run: |
mkdir -p /tmp/digests
digest="${{ inputs.digest }}"
echo "🔍 Build step digest output: '$digest'"
if [[ -z "$digest" ]]; then
echo "❌ ERROR: No digest found from build step"
exit 1
fi
digest_file="/tmp/digests/${digest#sha256:}"
echo "📁 Creating digest file: $digest_file"
touch "$digest_file"
echo "✅ Digest file created successfully"
echo "📋 Contents of /tmp/digests:"
ls -la /tmp/digests/
- name: Rename extracted binary
shell: bash
run: mv /tmp/binaries/sbin/conduwuit /tmp/binaries/conduwuit${{ inputs.cpu_suffix }}-${{ inputs.slug }}${{ inputs.artifact_suffix }}
- name: Upload binary artifact
uses: forgejo/upload-artifact@v4
with:
name: conduwuit${{ inputs.cpu_suffix }}-${{ inputs.slug }}${{ inputs.artifact_suffix }}
path: /tmp/binaries/conduwuit${{ inputs.cpu_suffix }}-${{ inputs.slug }}${{ inputs.artifact_suffix }}
if-no-files-found: error
- name: Upload digest
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: forgejo/upload-artifact@v4
with:
name: digests${{ inputs.digest_suffix }}-${{ inputs.slug }}${{ inputs.cpu_suffix }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 5
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Sync repository
uses: https://github.com/actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
fetch-depth: 0
@@ -55,7 +55,7 @@ jobs:
- name: Setup Node.js
if: steps.runner-env.outputs.node_major == '' || steps.runner-env.outputs.node_major < '20'
uses: https://github.com/actions/setup-node@v4
uses: https://github.com/actions/setup-node@v5
with:
node-version: 22
+10 -2
View File
@@ -4,6 +4,14 @@ on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
pull_request:
paths:
- ".forgejo/workflows/element.yml"
push:
branches:
- main
paths:
- ".forgejo/workflows/element.yml"
concurrency:
group: "element-${{ github.ref }}"
@@ -16,7 +24,7 @@ jobs:
steps:
- name: 📦 Setup Node.js
uses: https://github.com/actions/setup-node@v4
uses: https://github.com/actions/setup-node@v5
with:
node-version: "22"
@@ -101,7 +109,7 @@ jobs:
cat ./element-web/webapp/config.json
- name: 📤 Upload Artifact
uses: https://code.forgejo.org/actions/upload-artifact@v3
uses: forgejo/upload-artifact@v4
with:
name: element-web
path: ./element-web/webapp/
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
@@ -47,7 +47,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
+127 -256
View File
@@ -29,49 +29,12 @@ on:
env:
BUILTIN_REGISTRY: forgejo.ellis.link
BUILTIN_REGISTRY_ENABLED: "${{ ((vars.BUILTIN_REGISTRY_USER && secrets.BUILTIN_REGISTRY_PASSWORD) || (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)) && 'true' || 'false' }}"
IMAGE_PATH: forgejo.ellis.link/continuwuation/continuwuity
jobs:
define-variables:
runs-on: ubuntu-latest
outputs:
images: ${{ steps.var.outputs.images }}
images_list: ${{ steps.var.outputs.images_list }}
build_matrix: ${{ steps.var.outputs.build_matrix }}
steps:
- name: Setting variables
uses: https://github.com/actions/github-script@v7
id: var
with:
script: |
const githubRepo = '${{ github.repository }}'.toLowerCase()
const repoId = githubRepo.split('/')[1]
core.setOutput('github_repository', githubRepo)
const builtinImage = '${{ env.BUILTIN_REGISTRY }}/' + githubRepo
let images = []
if (process.env.BUILTIN_REGISTRY_ENABLED === "true") {
images.push(builtinImage)
} else {
// Fallback to official registry for forks/PRs without credentials
images.push('forgejo.ellis.link/continuwuation/continuwuity')
}
core.setOutput('images', images.join("\n"))
core.setOutput('images_list', images.join(","))
const platforms = ['linux/amd64', 'linux/arm64']
core.setOutput('build_matrix', JSON.stringify({
platform: platforms,
target_cpu: ['base'],
include: platforms.map(platform => { return {
platform,
slug: platform.replace('/', '-')
}})
}))
build-image:
build-release:
name: "Build ${{ matrix.slug }} (release)"
runs-on: dind
needs: define-variables
permissions:
contents: read
packages: write
@@ -79,133 +42,28 @@ jobs:
id-token: write
strategy:
matrix:
{
"target_cpu": ["base"],
"profile": ["release"],
"include":
[
{ "platform": "linux/amd64", "slug": "linux-amd64" },
{ "platform": "linux/arm64", "slug": "linux-arm64" },
],
"platform": ["linux/amd64", "linux/arm64"],
}
include:
- platform: "linux/amd64"
slug: "linux-amd64"
- platform: "linux/arm64"
slug: "linux-arm64"
steps:
- name: Echo strategy
run: echo '${{ toJSON(fromJSON(needs.define-variables.outputs.build_matrix)) }}'
- name: Echo matrix
run: echo '${{ toJSON(matrix) }}'
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
- name: Install rust
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
id: rust-toolchain
uses: ./.forgejo/actions/rust-toolchain
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Prepare Docker build environment
id: prepare
uses: ./.forgejo/actions/prepare-docker-build
with:
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
endpoint: ${{ env.BUILDKIT_ENDPOINT || '' }}
- name: Set up QEMU
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
uses: docker/setup-qemu-action@v3
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Login to builtin registry
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: docker/login-action@v3
with:
registry: ${{ env.BUILTIN_REGISTRY }}
username: ${{ vars.BUILTIN_REGISTRY_USER || github.actor }}
password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (labels, annotations) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{needs.define-variables.outputs.images}}
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
# It will not push images generated from a pull request
- name: Get short git commit SHA
id: sha
run: |
calculatedSha=$(git rev-parse --short ${{ github.sha }})
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
echo "Short SHA: $calculatedSha"
- name: Get Git commit timestamps
run: |
timestamp=$(git log -1 --pretty=%ct)
echo "TIMESTAMP=$timestamp" >> $GITHUB_ENV
echo "Commit timestamp: $timestamp"
- uses: ./.forgejo/actions/timelord
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
with:
key: timelord-v0
path: .
- name: Cache Rust registry
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
uses: actions/cache@v3
with:
path: |
.cargo/git
.cargo/git/checkouts
.cargo/registry
.cargo/registry/src
key: rust-registry-image-${{hashFiles('**/Cargo.lock') }}
- name: Cache cargo target
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
id: cache-cargo-target
uses: actions/cache@v3
with:
path: |
cargo-target-${{ matrix.target_cpu }}-${{ matrix.slug }}-${{ matrix.profile }}
key: cargo-target-${{ matrix.target_cpu }}-${{ matrix.slug }}-${{ matrix.profile }}-${{hashFiles('**/Cargo.lock') }}-${{steps.rust-toolchain.outputs.rustc_version}}
- name: Cache apt cache
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
id: cache-apt
uses: actions/cache@v3
with:
path: |
var-cache-apt-${{ matrix.slug }}
key: var-cache-apt-${{ matrix.slug }}
- name: Cache apt lib
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
id: cache-apt-lib
uses: actions/cache@v3
with:
path: |
var-lib-apt-${{ matrix.slug }}
key: var-lib-apt-${{ matrix.slug }}
- name: inject cache into docker
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
uses: https://github.com/reproducible-containers/buildkit-cache-dance@v3.3.0
with:
cache-map: |
{
".cargo/registry": "/usr/local/cargo/registry",
".cargo/git/db": "/usr/local/cargo/git/db",
"cargo-target-${{ matrix.target_cpu }}-${{ matrix.slug }}-${{ matrix.profile }}": {
"target": "/app/target",
"id": "cargo-target-${{ matrix.target_cpu }}-${{ matrix.slug }}-${{ matrix.profile }}"
},
"var-cache-apt-${{ matrix.slug }}": "/var/cache/apt",
"var-lib-apt-${{ matrix.slug }}": "/var/lib/apt"
}
skip-extraction: ${{ steps.cache.outputs.cache-hit }}
platform: ${{ matrix.platform }}
slug: ${{ matrix.slug }}
target_cpu: ""
profile: "release"
images: ${{ env.IMAGE_PATH }}
registry_user: ${{ vars.BUILTIN_REGISTRY_USER || github.actor }}
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
- name: Build and push Docker image by digest
id: build
uses: docker/build-push-action@v6
@@ -217,117 +75,130 @@ jobs:
GIT_COMMIT_HASH_SHORT=${{ env.COMMIT_SHORT_SHA }}
GIT_REMOTE_URL=${{github.event.repository.html_url }}
GIT_REMOTE_COMMIT_URL=${{github.event.head_commit.url }}
CARGO_INCREMENTAL=${{ env.BUILDKIT_ENDPOINT != '' && '1' || '0' }}
TARGET_CPU=
RUST_PROFILE=release
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
labels: ${{ steps.prepare.outputs.metadata_labels }}
annotations: ${{ steps.prepare.outputs.metadata_annotations }}
cache-from: type=gha
# cache-to: type=gha,mode=max
sbom: true
outputs: |
${{ env.BUILTIN_REGISTRY_ENABLED == 'true' && format('type=image,"name={0}",push-by-digest=true,name-canonical=true,push=true', needs.define-variables.outputs.images_list) || format('type=image,"name={0}",push=false', needs.define-variables.outputs.images_list) }}
${{ env.BUILTIN_REGISTRY_ENABLED == 'true' && format('type=image,"name={0}",push-by-digest=true,name-canonical=true,push=true', env.IMAGE_PATH) || format('type=image,"name={0}",push=false', env.IMAGE_PATH) }}
type=local,dest=/tmp/binaries
env:
SOURCE_DATE_EPOCH: ${{ env.TIMESTAMP }}
# For publishing multi-platform manifests
- name: Export digest
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
# Binary extracted via local output for all builds
- name: Rename extracted binary
run: mv /tmp/binaries/sbin/conduwuit /tmp/binaries/conduwuit-${{ matrix.target_cpu }}-${{ matrix.slug }}-${{ matrix.profile }}
- name: Upload binary artifact
uses: forgejo/upload-artifact@v4
- name: Upload Docker artifacts
uses: ./.forgejo/actions/upload-docker-artifacts
with:
name: conduwuit-${{ matrix.target_cpu }}-${{ matrix.slug }}-${{ matrix.profile }}
path: /tmp/binaries/conduwuit-${{ matrix.target_cpu }}-${{ matrix.slug }}-${{ matrix.profile }}
if-no-files-found: error
slug: ${{ matrix.slug }}
cpu_suffix: ${{ steps.prepare.outputs.cpu_suffix }}
artifact_suffix: ""
digest_suffix: ""
digest: ${{ steps.build.outputs.digest }}
- name: Upload digest
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: forgejo/upload-artifact@v4
with:
name: digests-${{ matrix.slug }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 5
merge:
merge-release:
name: "Create Multi-arch Release Manifest"
runs-on: dind
needs: [define-variables, build-image]
needs: build-release
steps:
- name: Download digests
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: forgejo/download-artifact@v4
- name: Checkout repository
uses: actions/checkout@v5
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Login to builtin registry
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: docker/login-action@v3
persist-credentials: false
- name: Create multi-platform manifest
uses: ./.forgejo/actions/create-docker-manifest
with:
registry: ${{ env.BUILTIN_REGISTRY }}
username: ${{ vars.BUILTIN_REGISTRY_USER || github.actor }}
password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
digest_pattern: "digests-linux-{amd64,arm64}"
tag_suffix: ""
images: ${{ env.IMAGE_PATH }}
registry_user: ${{ vars.BUILTIN_REGISTRY_USER || github.actor }}
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: docker/setup-buildx-action@v3
with:
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
endpoint: ${{ env.BUILDKIT_ENDPOINT || '' }}
build-maxperf:
name: "Build ${{ matrix.slug }} (max-perf)"
runs-on: dind
needs: build-release
permissions:
contents: read
packages: write
attestations: write
id-token: write
strategy:
matrix:
include:
- platform: "linux/amd64"
slug: "linux-amd64"
target_cpu: "haswell"
- platform: "linux/arm64"
slug: "linux-arm64"
target_cpu: ""
- name: Extract metadata (tags) for Docker
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
id: meta
uses: docker/metadata-action@v5
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
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=long
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
images: ${{needs.define-variables.outputs.images}}
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
persist-credentials: false
- name: Prepare max-perf Docker build environment
id: prepare
uses: ./.forgejo/actions/prepare-docker-build
with:
platform: ${{ matrix.platform }}
slug: ${{ matrix.slug }}
target_cpu: ${{ matrix.target_cpu }}
profile: "release-max-perf"
images: ${{ env.IMAGE_PATH }}
registry_user: ${{ vars.BUILTIN_REGISTRY_USER || github.actor }}
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
- name: Build and push max-perf Docker image by digest
id: build
uses: docker/build-push-action@v6
with:
context: .
file: "docker/Dockerfile"
build-args: |
GIT_COMMIT_HASH=${{ github.sha }}
GIT_COMMIT_HASH_SHORT=${{ env.COMMIT_SHORT_SHA }}
GIT_REMOTE_URL=${{github.event.repository.html_url }}
GIT_REMOTE_COMMIT_URL=${{github.event.head_commit.url }}
CARGO_INCREMENTAL=${{ env.BUILDKIT_ENDPOINT != '' && '1' || '0' }}
TARGET_CPU=${{ matrix.target_cpu }}
RUST_PROFILE=release-max-perf
platforms: ${{ matrix.platform }}
labels: ${{ steps.prepare.outputs.metadata_labels }}
annotations: ${{ steps.prepare.outputs.metadata_annotations }}
cache-from: type=gha
# cache-to: type=gha,mode=max
sbom: true
outputs: |
${{ env.BUILTIN_REGISTRY_ENABLED == 'true' && format('type=image,"name={0}",push-by-digest=true,name-canonical=true,push=true', env.IMAGE_PATH) || format('type=image,"name={0}",push=false', env.IMAGE_PATH) }}
type=local,dest=/tmp/binaries
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
SOURCE_DATE_EPOCH: ${{ env.TIMESTAMP }}
- name: Upload max-perf Docker artifacts
uses: ./.forgejo/actions/upload-docker-artifacts
with:
slug: ${{ matrix.slug }}
cpu_suffix: ${{ steps.prepare.outputs.cpu_suffix }}
artifact_suffix: "-maxperf"
digest_suffix: "-maxperf"
digest: ${{ steps.build.outputs.digest }}
- name: Create manifest list and push
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
working-directory: /tmp/digests
env:
IMAGES: ${{needs.define-variables.outputs.images}}
shell: bash
run: |
IFS=$'\n'
IMAGES_LIST=($IMAGES)
ANNOTATIONS_LIST=($DOCKER_METADATA_OUTPUT_ANNOTATIONS)
TAGS_LIST=($DOCKER_METADATA_OUTPUT_TAGS)
for REPO in "${IMAGES_LIST[@]}"; do
docker buildx imagetools create \
$(for tag in "${TAGS_LIST[@]}"; do echo "--tag"; echo "$tag"; done) \
$(for annotation in "${ANNOTATIONS_LIST[@]}"; do echo "--annotation"; echo "$annotation"; done) \
$(for reference in *; do printf "$REPO@sha256:%s\n" $reference; done)
done
- name: Inspect image
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
env:
IMAGES: ${{needs.define-variables.outputs.images}}
shell: bash
run: |
IMAGES_LIST=($IMAGES)
for REPO in "${IMAGES_LIST[@]}"; do
docker buildx imagetools inspect $REPO:${{ steps.meta.outputs.version }}
done
merge-maxperf:
name: "Create Max-Perf Manifest"
runs-on: dind
needs: build-maxperf
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
persist-credentials: false
- name: Create max-perf manifest
uses: ./.forgejo/actions/create-docker-manifest
with:
digest_pattern: "digests-maxperf-linux-{amd64-haswell,arm64}"
tag_suffix: "-maxperf"
images: ${{ env.IMAGE_PATH }}
registry_user: ${{ vars.BUILTIN_REGISTRY_USER || github.actor }}
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
+36 -15
View File
@@ -1,5 +1,7 @@
name: Maintenance / Renovate
enable-email-notifications: true
on:
schedule:
# Run at 5am UTC daily to avoid late-night dev
@@ -10,10 +12,10 @@ on:
dryRun:
description: 'Dry run mode'
required: false
default: null
default: ''
type: choice
options:
- null
- ''
- 'extract'
- 'lookup'
- 'full'
@@ -23,6 +25,7 @@ on:
default: 'info'
type: choice
options:
- 'debug'
- 'info'
- 'warning'
- 'critical'
@@ -40,11 +43,11 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:41
image: ghcr.io/renovatebot/renovate:41.118.1@sha256:ad040bc6755d4bea6eeca40e6c6bb42963687b189cc7c391302a639ecec5c1e9
options: --tmpfs /tmp:exec
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
show-progress: false
@@ -52,7 +55,7 @@ jobs:
run: /usr/local/renovate/node -e 'console.log(`node heap limit = ${require("v8").getHeapStatistics().heap_size_limit / (1024 * 1024)} Mb`)'
- name: Restore renovate repo cache
uses: https://github.com/actions/cache@v4
uses: actions/cache/restore@v4
with:
path: |
/tmp/renovate/cache/renovate/repository
@@ -61,7 +64,7 @@ jobs:
repo-cache-
- name: Restore renovate package cache
uses: https://github.com/actions/cache@v4
uses: actions/cache/restore@v4
with:
path: |
/tmp/renovate/cache/renovate/renovate-cache-sqlite
@@ -69,8 +72,17 @@ jobs:
restore-keys: |
package-cache-
- name: Restore renovate OSV cache
uses: actions/cache/restore@v4
with:
path: |
/tmp/osv
key: osv-cache-${{ github.run_id }}
restore-keys: |
osv-cache-
- name: Self-hosted Renovate
uses: https://github.com/renovatebot/github-action@v43.0.9
run: renovate
env:
LOG_LEVEL: ${{ inputs.logLevel || 'info' }}
RENOVATE_DRY_RUN: ${{ inputs.dryRun || 'false' }}
@@ -84,28 +96,37 @@ jobs:
RENOVATE_REQUIRE_CONFIG: 'required'
RENOVATE_ONBOARDING: 'false'
RENOVATE_PR_COMMITS_PER_RUN_LIMIT: 3
RENOVATE_INHERIT_CONFIG: 'true'
RENOVATE_GITHUB_TOKEN_WARN: 'false'
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
GITHUB_COM_TOKEN: ${{ secrets.GH_PUBLIC_RO }}
GITHUB_COM_TOKEN: ${{ secrets.GH_PUBLIC_RO || secrets.GH_TOKEN }}
RENOVATE_REPOSITORY_CACHE: 'enabled'
RENOVATE_X_SQLITE_PACKAGE_CACHE: true
RENOVATE_X_SQLITE_PACKAGE_CACHE: 'true'
OSV_OFFLINE_ROOT_DIR: /tmp/osv
- name: Save renovate repo cache
if: always() && env.RENOVATE_DRY_RUN != 'full'
uses: https://github.com/actions/cache@v4
if: always()
uses:
actions/cache/save@v4
with:
path: |
/tmp/renovate/cache/renovate/repository
key: repo-cache-${{ github.run_id }}
- name: Save renovate package cache
if: always() && env.RENOVATE_DRY_RUN != 'full'
uses: https://github.com/actions/cache@v4
if: always()
uses: actions/cache/save@v4
with:
path: |
/tmp/renovate/cache/renovate/renovate-cache-sqlite
key: package-cache-${{ github.run_id }}
- name: Save renovate OSV cache
if: always()
uses: actions/cache/save@v4
with:
path: |
/tmp/osv
key: osv-cache-${{ github.run_id }}
+107
View File
@@ -0,0 +1,107 @@
name: Update flake hashes
on:
workflow_dispatch:
pull_request:
paths:
- "Cargo.lock"
- "Cargo.toml"
- "rust-toolchain.toml"
jobs:
update-flake-hashes:
runs-on: ubuntu-latest
steps:
- uses: https://code.forgejo.org/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
with:
fetch-depth: 1
fetch-tags: false
fetch-single-branch: true
submodules: false
persist-credentials: false
- uses: https://github.com/cachix/install-nix-action@7be5dee1421f63d07e71ce6e0a9f8a4b07c2a487 # v31.6.1
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"
+3
View File
@@ -13,6 +13,9 @@ extend-ignore-re = [
"[0-9+][A-Za-z0-9+]{30,}[a-z0-9+]",
"\\$[A-Z0-9+][A-Za-z0-9+]{6,}[a-z0-9+]",
"\\b[a-z0-9+/=][A-Za-z0-9+/=]{7,}[a-z0-9+/=][A-Z]\\b",
# In the renovate config
".ontainer"
]
[default.extend-words]
Generated
+325 -311
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -45,7 +45,7 @@ version = "0.3"
features = ["ffi", "std", "union"]
[workspace.dependencies.const-str]
version = "0.6.2"
version = "0.7.0"
[workspace.dependencies.ctor]
version = "0.5.0"
@@ -166,8 +166,8 @@ default-features = false
features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde_yaml]
version = "0.9.34"
[workspace.dependencies.serde_yml]
version = "0.0.12"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
@@ -351,8 +351,7 @@ version = "0.1.2"
# Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
#branch = "conduwuit-changes"
rev = "8fb268fa2771dfc3a1c8075ef1246e7c9a0a53fd"
rev = "d18823471ab3c09e77ff03eea346d4c07e572654"
features = [
"compat",
"rand",
@@ -387,11 +386,12 @@ features = [
"unstable-msc4210", # remove legacy mentions
"unstable-extensible-events",
"unstable-pdu",
"unstable-msc4155"
]
[workspace.dependencies.rust-rocksdb]
git = "https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1"
rev = "99b0319416b64830dd6f8943e1f65e15aeef18bc"
rev = "61d9d23872197e9ace4a477f2617d5c9f50ecb23"
default-features = false
features = [
"multi-threaded-cf",
@@ -602,7 +602,7 @@ rev = "e4ae7628fe4fcdacef9788c4c8415317a4489941"
# Use 1-indexed line numbers when displaying parse error messages
[patch.crates-io.resolv-conf]
git = "https://forgejo.ellis.link/continuwuation/resolv-conf"
rev = "56251316cc4127bcbf36e68ce5e2093f4d33e227"
rev = "ebbbec1cb965b487a0150f5d007e96c05e3d72af"
#
# Our crates
+14 -5
View File
@@ -48,11 +48,13 @@ EOF
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.13.0
ENV BINSTALL_VERSION=1.15.5
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
ENV LDDTREE_VERSION=0.3.7
# renovate: datasource=crate depName=timelord-cli
ENV TIMELORD_VERSION=3.0.1
# Install unpackaged tools
RUN <<EOF
@@ -60,6 +62,7 @@ RUN <<EOF
curl --retry 5 -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
cargo binstall --no-confirm cargo-sbom --version $CARGO_SBOM_VERSION
cargo binstall --no-confirm lddtree --version $LDDTREE_VERSION
cargo binstall --no-confirm timelord-cli --version $TIMELORD_VERSION
EOF
# Set up xx (cross-compilation scripts)
@@ -81,8 +84,9 @@ RUN rustc --version \
&& xx-cargo --setup-target-triple
# Build binary
# We disable incremental compilation to save disk space, as it only produces a minimal speedup for this case.
RUN echo "CARGO_INCREMENTAL=0" >> /etc/environment
# Configure incremental compilation based on build context
ARG CARGO_INCREMENTAL=0
RUN echo "CARGO_INCREMENTAL=${CARGO_INCREMENTAL}" >> /etc/environment
# Configure pkg-config
RUN <<EOF
@@ -133,6 +137,11 @@ FROM toolchain AS builder
# Get source
COPY . .
# Restore timestamps from timelord cache if available
RUN --mount=type=cache,target=/timelord/ \
echo "Restoring timestamps from timelord cache"; \
timelord sync --source-dir /app --cache-dir /timelord;
ARG TARGETPLATFORM
# Verify environment configuration
@@ -172,8 +181,8 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name"))
for BINARY in "${BINARIES[@]}"; do
echo $BINARY
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY
cp $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY /out/sbin/$BINARY
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE}/$BINARY
cp $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE}/$BINARY /out/sbin/$BINARY
done
EOF
+1 -1
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.13.0
ENV BINSTALL_VERSION=1.15.5
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
Generated
+82 -126
View File
@@ -10,11 +10,11 @@
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1751403276,
"narHash": "sha256-V0EPQNsQko1a8OqIWc2lLviLnMpR1m08Ej00z5RVTfs=",
"lastModified": 1757683818,
"narHash": "sha256-q7q0pWT+wu5AUU1Qlbwq8Mqb+AzHKhaMCVUq/HNZfo8=",
"owner": "zhaofengli",
"repo": "attic",
"rev": "896ad88fa57ad5dbcd267c0ac51f1b71ccfcb4dd",
"rev": "7c5d79ad62cda340cb8c80c99b921b7b7ffacf69",
"type": "github"
},
"original": {
@@ -29,14 +29,14 @@
"devenv": "devenv",
"flake-compat": "flake-compat_2",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs_4"
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1748883665,
"narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=",
"lastModified": 1756385612,
"narHash": "sha256-+NU5MMhuPHHRyvZZWNFG7zt+leRSPsJu1MwhOUzkPUk=",
"owner": "cachix",
"repo": "cachix",
"rev": "f707778d902af4d62d8dd92c269f8e70de09acbe",
"rev": "dc24688cd67518c3711d511fa369c0f5a131063a",
"type": "github"
},
"original": {
@@ -58,16 +58,21 @@
],
"git-hooks": [
"cachix",
"devenv"
"devenv",
"git-hooks"
],
"nixpkgs": "nixpkgs_2"
"nixpkgs": [
"cachix",
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1744206633,
"narHash": "sha256-pb5aYkE8FOoa4n123slgHiOf1UbNSnKe5pEZC+xXD5g=",
"lastModified": 1748883665,
"narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=",
"owner": "cachix",
"repo": "cachix",
"rev": "8a60090640b96f9df95d1ab99e5763a586be1404",
"rev": "f707778d902af4d62d8dd92c269f8e70de09acbe",
"type": "github"
},
"original": {
@@ -78,18 +83,12 @@
}
},
"crane": {
"inputs": {
"nixpkgs": [
"attic",
"nixpkgs"
]
},
"locked": {
"lastModified": 1722960479,
"narHash": "sha256-NhCkJJQhD5GUib8zN9JrmYGMwt4lCRp6ZVNzIiYCl0Y=",
"lastModified": 1751562746,
"narHash": "sha256-smpugNIkmDeicNz301Ll1bD7nFOty97T79m4GUMUczA=",
"owner": "ipetkov",
"repo": "crane",
"rev": "4c6c77920b8d44cd6660c1621dea6b3fc4b4c4f4",
"rev": "aed2020fd3dc26e1e857d4107a5a67a33ab6c1fd",
"type": "github"
},
"original": {
@@ -100,11 +99,11 @@
},
"crane_2": {
"locked": {
"lastModified": 1750266157,
"narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
"lastModified": 1757183466,
"narHash": "sha256-kTdCCMuRE+/HNHES5JYsbRHmgtr+l9mOtf5dpcMppVc=",
"owner": "ipetkov",
"repo": "crane",
"rev": "e37c943371b73ed87faf33f7583860f81f1d5a48",
"rev": "d599ae4847e7f87603e7082d73ca673aa93c916d",
"type": "github"
},
"original": {
@@ -132,11 +131,11 @@
]
},
"locked": {
"lastModified": 1748273445,
"narHash": "sha256-5V0dzpNgQM0CHDsMzh+ludYeu1S+Y+IMjbaskSSdFh0=",
"lastModified": 1754404745,
"narHash": "sha256-BdbW/iTImczgcuATgQIa9sPGuYIBxVq2xqcvICsa2AQ=",
"owner": "cachix",
"repo": "devenv",
"rev": "668a50d8b7bdb19a0131f53c9f6c25c9071e1ffb",
"rev": "6563b21105168f90394dfaf58284b078af2d7275",
"type": "github"
},
"original": {
@@ -153,11 +152,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1755585599,
"narHash": "sha256-tl/0cnsqB/Yt7DbaGMel2RLa7QG5elA8lkaOXli6VdY=",
"lastModified": 1758004879,
"narHash": "sha256-kV7tQzcNbmo58wg2uE2MQ/etaTx+PxBMHeNrLP8vOgk=",
"owner": "nix-community",
"repo": "fenix",
"rev": "6ed03ef4c8ec36d193c18e06b9ecddde78fb7e42",
"rev": "07e5ce53dd020e6b337fdddc934561bee0698fa2",
"type": "github"
},
"original": {
@@ -170,11 +169,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
@@ -224,11 +223,11 @@
]
},
"locked": {
"lastModified": 1722555600,
"narHash": "sha256-XOQkdLafnb/p9ij77byFQjDf5m5QYl9b2REiVClC+x4=",
"lastModified": 1751413152,
"narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "8471fe90ad337a8074e957b69ca4d0089218391d",
"rev": "77826244401ea9de6e3bac47c2db46005e1f30b5",
"type": "github"
},
"original": {
@@ -247,11 +246,11 @@
]
},
"locked": {
"lastModified": 1712014858,
"narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=",
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9126214d0a59633752a136528f5f3b9aa8565b7d",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"type": "github"
},
"original": {
@@ -292,11 +291,11 @@
]
},
"locked": {
"lastModified": 1747372754,
"narHash": "sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=",
"lastModified": 1750779888,
"narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46",
"rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
"type": "github"
},
"original": {
@@ -327,31 +326,24 @@
"type": "github"
}
},
"libgit2": {
"flake": false,
"locked": {
"lastModified": 1697646580,
"narHash": "sha256-oX4Z3S9WtJlwvj0uH9HlYcWv+x1hqp8mhXl7HsLu2f0=",
"owner": "libgit2",
"repo": "libgit2",
"rev": "45fd9ed7ae1a9b74b957ef4f337bc3c8b3df01b5",
"type": "github"
},
"original": {
"owner": "libgit2",
"repo": "libgit2",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": [
"cachix",
"devenv"
"devenv",
"flake-compat"
],
"flake-parts": "flake-parts_2",
"libgit2": "libgit2",
"nixpkgs": "nixpkgs_3",
"git-hooks-nix": [
"cachix",
"devenv",
"git-hooks"
],
"nixpkgs": [
"cachix",
"devenv",
"nixpkgs"
],
"nixpkgs-23-11": [
"cachix",
"devenv"
@@ -359,34 +351,30 @@
"nixpkgs-regression": [
"cachix",
"devenv"
],
"pre-commit-hooks": [
"cachix",
"devenv"
]
},
"locked": {
"lastModified": 1745930071,
"narHash": "sha256-bYyjarS3qSNqxfgc89IoVz8cAFDkF9yPE63EJr+h50s=",
"owner": "domenkozar",
"lastModified": 1752773918,
"narHash": "sha256-dOi/M6yNeuJlj88exI+7k154z+hAhFcuB8tZktiW7rg=",
"owner": "cachix",
"repo": "nix",
"rev": "b455edf3505f1bf0172b39a735caef94687d0d9c",
"rev": "031c3cf42d2e9391eee373507d8c12e0f9606779",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.24",
"owner": "cachix",
"ref": "devenv-2.30",
"repo": "nix",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1731533336,
"narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=",
"lastModified": 1757882181,
"narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "f7653272fd234696ae94229839a99b73c9ab7de0",
"rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
"type": "github"
},
"original": {
@@ -404,11 +392,11 @@
]
},
"locked": {
"lastModified": 1729742964,
"narHash": "sha256-B4mzTcQ0FZHdpeWcpDYPERtyjJd/NIuaQ9+BV1h+MpA=",
"lastModified": 1737420293,
"narHash": "sha256-F1G5ifvqTpJq7fdkT34e/Jy9VCyzd5XfJ9TO8fHhJWE=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "e04df33f62cdcf93d73e9a04142464753a16db67",
"rev": "f4158fa080ef4503c8f4c820967d946c2af31ec9",
"type": "github"
},
"original": {
@@ -419,11 +407,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1726042813,
"narHash": "sha256-LnNKCCxnwgF+575y0pxUdlGZBO/ru1CtGHIqQVfvjlA=",
"lastModified": 1751949589,
"narHash": "sha256-mgFxAPLWw0Kq+C8P3dRrZrOYEQXOtKuYVlo9xvPntt8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "159be5db480d1df880a0135ca0bfed84c2f88353",
"rev": "9b008d60392981ad674e04016d25619281550a9d",
"type": "github"
},
"original": {
@@ -435,27 +423,27 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1724316499,
"narHash": "sha256-Qb9MhKBUTCfWg/wqqaxt89Xfi6qTD3XpTzQ9eXi3JmE=",
"lastModified": 1751741127,
"narHash": "sha256-t75Shs76NgxjZSgvvZZ9qOmz5zuBE8buUaYD28BMTxg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "797f7dc49e0bc7fab4b57c021cdf68f595e47841",
"rev": "29e290002bfff26af1db6f64d070698019460302",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1733212471,
"narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=",
"lastModified": 1754214453,
"narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "55d15ad12a74eb7d4646254e13638ad0c4128776",
"rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376",
"type": "github"
},
"original": {
@@ -467,43 +455,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1717432640,
"narHash": "sha256-+f9c4/ZX5MWDOuB1rKoWj+lBNm0z0rs4CK47HBLxy1o=",
"lastModified": 1758029226,
"narHash": "sha256-TjqVmbpoCqWywY9xIZLTf6ANFvDCXdctCjoYuYPYdMI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "88269ab3044128b7c2f4c7d68448b2fb50456870",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1748190013,
"narHash": "sha256-R5HJFflOfsP5FBtk+zE8FpL8uqE7n62jqOsADvVshhE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "62b852f6c6742134ade1abdd2a21685fd617a291",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1751498133,
"narHash": "sha256-QWJ+NQbMU+NcU2xiyo7SNox1fAuwksGlQhpzBl76g1I=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d55716bb59b91ae9d1ced4b1ccdea7a442ecbfdb",
"rev": "08b8f92ac6354983f5382124fef6006cade4a1c1",
"type": "github"
},
"original": {
@@ -522,17 +478,17 @@
"flake-compat": "flake-compat_3",
"flake-utils": "flake-utils",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_5"
"nixpkgs": "nixpkgs_3"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1755504847,
"narHash": "sha256-VX0B9hwhJypCGqncVVLC+SmeMVd/GAYbJZ0MiiUn2Pk=",
"lastModified": 1757362324,
"narHash": "sha256-/PAhxheUq4WBrW5i/JHzcCqK5fGWwLKdH6/Lu1tyS18=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "a905e3b21b144d77e1b304e49f3264f6f8d4db75",
"rev": "9edc9cbe5d8e832b5864e09854fa94861697d2fd",
"type": "github"
},
"original": {
+44 -15
View File
@@ -1,10 +1,12 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"extends": ["config:recommended", "replacements:all"],
"osvVulnerabilityAlerts": true,
"lockFileMaintenance": {
"enabled": true,
"schedule": ["at any time"]
},
"platformAutomerge": true,
"nix": {
"enabled": true
},
@@ -29,10 +31,15 @@
},
"packageRules": [
{
"description": "Batch minor and patch GitHub Actions updates",
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "github-actions-non-major"
"description": "Batch patch-level Rust dependency updates",
"matchManagers": ["cargo"],
"matchUpdateTypes": ["patch"],
"groupName": "rust-patch-updates"
},
{
"description": "Limit concurrent Cargo PRs",
"matchManagers": ["cargo"],
"prConcurrentLimit": 5
},
{
"description": "Group Rust toolchain updates into a single PR",
@@ -40,20 +47,42 @@
"matchPackageNames": ["rust", "rustc", "cargo"],
"groupName": "rust-toolchain"
},
{
"description": "Batch minor and patch GitHub Actions updates",
"matchManagers": ["github-actions"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "github-actions-non-major"
},
{
"description": "Pin forgejo artifact actions to prevent breaking changes",
"matchManagers": ["github-actions"],
"matchPackageNames": ["forgejo/upload-artifact", "forgejo/download-artifact"],
"enabled": false
},
{
"description": "Auto-merge renovatebot docker image updates",
"matchDatasources": ["docker"],
"matchPackageNames": ["ghcr.io/renovatebot/renovate"],
"automerge": true,
"automergeStrategy": "fast-forward"
},
{
"description": "Group lockfile updates into a single PR",
"matchUpdateTypes": ["lockFileMaintenance"],
"groupName": "lockfile-maintenance"
},
{
"description": "Batch patch-level Rust dependency updates",
"matchManagers": ["cargo"],
"matchUpdateTypes": ["patch"],
"groupName": "rust-patch-updates"
},
{
"matchManagers": ["cargo"],
"prConcurrentLimit": 5
}
],
"customManagers": [
{
"customType": "regex",
"description": "Update _VERSION variables in Dockerfiles",
"managerFilePatterns": [
"/(^|/)([Dd]ocker|[Cc]ontainer)file[^/]*$/",
"/(^|/|\\.)([Dd]ocker|[Cc]ontainer)file$/"
],
"matchStrings": [
"# 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"
]
}
]
}
+1 -1
View File
@@ -85,7 +85,7 @@ futures.workspace = true
log.workspace = true
ruma.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
serde_yml.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
+2 -2
View File
@@ -16,7 +16,7 @@ pub(super) async fn register(&self) -> Result {
let range = 1..checked!(body_len - 1)?;
let appservice_config_body = body[range].join("\n");
let parsed_config = serde_yaml::from_str(&appservice_config_body);
let parsed_config = serde_yml::from_str(&appservice_config_body);
match parsed_config {
| Err(e) => return Err!("Could not parse appservice config as YAML: {e}"),
| Ok(registration) => match self
@@ -57,7 +57,7 @@ pub(super) async fn show_appservice_config(&self, appservice_identifier: String)
{
| None => return Err!("Appservice does not exist."),
| Some(config) => {
let config_str = serde_yaml::to_string(&config)?;
let config_str = serde_yml::to_string(&config)?;
write!(self, "Config for {appservice_identifier}:\n\n```yaml\n{config_str}\n```")
},
}
+3 -1
View File
@@ -632,6 +632,7 @@ pub(super) async fn force_set_room_state_from_server(
.add_pdu_outlier(&event_id, &value);
}
info!("Resolving new room state");
let new_room_state = self
.services
.rooms
@@ -639,7 +640,7 @@ pub(super) async fn force_set_room_state_from_server(
.resolve_state(&room_id, &room_version, state)
.await?;
info!("Forcing new room state");
info!("Compressing new room state");
let HashSetCompressStateEvent {
shortstatehash: short_state_hash,
added,
@@ -653,6 +654,7 @@ pub(super) async fn force_set_room_state_from_server(
let state_lock = self.services.rooms.state.mutex.lock(&*room_id).await;
info!("Forcing new room state");
self.services
.rooms
.state
+25 -7
View File
@@ -179,7 +179,11 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
.await
.is_ok_and(is_equal_to!(1))
{
self.services.admin.make_user_admin(&user_id).await?;
self.services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
warn!("Granting {user_id} admin privileges as the first user");
}
} else {
@@ -217,7 +221,9 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
.collect()
.await;
full_user_deactivate(self.services, &user_id, &all_joined_rooms).await?;
full_user_deactivate(self.services, &user_id, &all_joined_rooms)
.boxed()
.await?;
update_displayname(self.services, &user_id, None, &all_joined_rooms).await;
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms).await;
leave_all_rooms(self.services, &user_id).await;
@@ -376,7 +382,9 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
.collect()
.await;
full_user_deactivate(self.services, &user_id, &all_joined_rooms).await?;
full_user_deactivate(self.services, &user_id, &all_joined_rooms)
.boxed()
.await?;
update_displayname(self.services, &user_id, None, &all_joined_rooms).await;
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms)
.await;
@@ -756,7 +764,7 @@ pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAli
.build_and_append_pdu(
PduBuilder::state(String::new(), &power_levels_content),
&user_id,
&room_id,
Some(&room_id),
&state_lock,
)
.await?;
@@ -776,7 +784,11 @@ pub(super) async fn make_user_admin(&self, user_id: String) -> Result {
"Parsed user_id must be a local user"
);
self.services.admin.make_user_admin(&user_id).await?;
self.services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
self.write_str(&format!("{user_id} has been granted admin privileges.",))
.await
@@ -901,7 +913,13 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
);
let redaction_event_id = {
let state_lock = self.services.rooms.state.mutex.lock(event.room_id()).await;
let state_lock = self
.services
.rooms
.state
.mutex
.lock(&event.room_id_or_hash())
.await;
self.services
.rooms
@@ -915,7 +933,7 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
})
},
event.sender(),
event.room_id(),
Some(&event.room_id_or_hash()),
&state_lock,
)
.await?
+29 -34
View File
@@ -405,41 +405,36 @@ pub(crate) async fn register_route(
)
.await?;
if (!is_guest && body.inhibit_login)
// Generate new device id if the user didn't specify one
let no_device = body.inhibit_login
|| body
.appservice_info
.as_ref()
.is_some_and(|appservice| appservice.registration.device_management)
{
return Ok(register::v3::Response {
access_token: None,
user_id,
device_id: None,
refresh_token: None,
expires_in: None,
});
}
.is_some_and(|aps| aps.registration.device_management);
let (token, device) = if !no_device {
// Don't create a device for inhibited logins
let device_id = if is_guest { None } else { body.device_id.clone() }
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
// Generate new device id if the user didn't specify one
let device_id = if is_guest { None } else { body.device_id.clone() }
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
// Generate new token for the device
let new_token = utils::random_string(TOKEN_LENGTH);
// Generate new token for the device
let token = utils::random_string(TOKEN_LENGTH);
// Create device for this account
services
.users
.create_device(
&user_id,
&device_id,
&token,
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
.await?;
debug_info!(%user_id, %device_id, "User account was created");
// Create device for this account
services
.users
.create_device(
&user_id,
&device_id,
&new_token,
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
.await?;
debug_info!(%user_id, %device_id, "User account was created");
(Some(new_token), Some(device_id))
} else {
(None, None)
};
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
@@ -505,7 +500,7 @@ pub(crate) async fn register_route(
.await
.is_ok_and(is_equal_to!(1))
{
services.admin.make_user_admin(&user_id).await?;
services.admin.make_user_admin(&user_id).boxed().await?;
warn!("Granting {user_id} admin privileges as the first user");
} else if services.config.suspend_on_register {
// This is not an admin, suspend them.
@@ -583,9 +578,9 @@ pub(crate) async fn register_route(
}
Ok(register::v3::Response {
access_token: Some(token),
access_token: token,
user_id,
device_id: Some(device_id),
device_id: device,
refresh_token: None,
expires_in: None,
})
@@ -929,7 +924,7 @@ pub async fn full_user_deactivate(
.build_and_append_pdu(
PduBuilder::state(String::new(), &power_levels_content),
user_id,
room_id,
Some(room_id),
&state_lock,
)
.await
+2 -2
View File
@@ -69,7 +69,7 @@ pub(crate) async fn get_context_route(
let (base_id, base_pdu, visible) = try_join3(base_id, base_pdu, visible).await?;
if base_pdu.room_id != *room_id || base_pdu.event_id != *event_id {
if base_pdu.room_id_or_hash() != *room_id || base_pdu.event_id != *event_id {
return Err!(Request(NotFound("Base event not found.")));
}
@@ -130,7 +130,7 @@ pub(crate) async fn get_context_route(
let state_at = events_after
.last()
.map(ref_at!(1))
.map_or(body.event_id.as_ref(), |pdu| pdu.event_id.as_ref());
.map_or_else(|| body.event_id.as_ref(), |pdu| pdu.event_id.as_ref());
let state_ids = services
.rooms
+1 -1
View File
@@ -49,7 +49,7 @@ pub(crate) async fn ban_user_route(
..current_member_content
}),
sender_user,
&body.room_id,
Some(&body.room_id),
&state_lock,
)
.await?;
+3 -3
View File
@@ -128,12 +128,12 @@ pub(crate) async fn invite_helper(
.create_hash_and_sign_event(
PduBuilder::state(user_id.to_string(), &content),
sender_user,
room_id,
Some(room_id),
&state_lock,
)
.await?;
let invite_room_state = services.rooms.state.summary_stripped(&pdu).await;
let invite_room_state = services.rooms.state.summary_stripped(&pdu, room_id).await;
drop(state_lock);
@@ -227,7 +227,7 @@ pub(crate) async fn invite_helper(
.build_and_append_pdu(
PduBuilder::state(user_id.to_string(), &content),
sender_user,
room_id,
Some(room_id),
&state_lock,
)
.await?;
+13 -6
View File
@@ -18,7 +18,7 @@
},
warn,
};
use futures::{FutureExt, StreamExt};
use futures::{FutureExt, StreamExt, TryFutureExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
RoomVersionId, UserId,
@@ -556,6 +556,10 @@ async fn join_room_by_id_helper_remote(
services
.server_keys
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
.inspect_err(|e| {
debug_warn!("Could not validate send_join response room_state event: {e:?}");
})
.inspect(|_| debug!("Completed validating send_join response room_state event"))
})
.ready_filter_map(Result::ok)
.fold(HashMap::new(), |mut state, (event_id, value)| async move {
@@ -566,7 +570,6 @@ async fn join_room_by_id_helper_remote(
return state;
},
};
services.rooms.outlier.add_pdu_outlier(&event_id, &value);
if let Some(state_key) = &pdu.state_key {
let shortstatekey = services
@@ -577,7 +580,6 @@ async fn join_room_by_id_helper_remote(
state.insert(shortstatekey, pdu.event_id.clone());
}
state
})
.await;
@@ -598,6 +600,7 @@ async fn join_room_by_id_helper_remote(
})
.ready_filter_map(Result::ok)
.ready_for_each(|(event_id, value)| {
trace!(%event_id, "Adding PDU as an outlier from send_join auth_chain");
services.rooms.outlier.add_pdu_outlier(&event_id, &value);
})
.await;
@@ -618,6 +621,9 @@ async fn join_room_by_id_helper_remote(
&parsed_join_pdu,
None, // TODO: third party invite
|k, s| state_fetch(k.clone(), s.into()),
&state_fetch(StateEventType::RoomCreate, "".into())
.await
.expect("create event is missing from send_join auth"),
)
.await
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
@@ -652,7 +658,7 @@ async fn join_room_by_id_helper_remote(
.force_state(room_id, statehash_before_join, added, removed, &state_lock)
.await?;
info!("Updating joined counts for new room");
debug!("Updating joined counts for new room");
services
.rooms
.state_cache
@@ -665,7 +671,7 @@ async fn join_room_by_id_helper_remote(
let statehash_after_join = services
.rooms
.state
.append_to_state(&parsed_join_pdu)
.append_to_state(&parsed_join_pdu, room_id)
.await?;
info!("Appending new room join event");
@@ -677,6 +683,7 @@ async fn join_room_by_id_helper_remote(
join_event,
once(parsed_join_pdu.event_id.borrow()),
&state_lock,
room_id,
)
.await?;
@@ -776,7 +783,7 @@ async fn join_room_by_id_helper_local(
.build_and_append_pdu(
PduBuilder::state(sender_user.to_string(), &content),
sender_user,
room_id,
Some(room_id),
&state_lock,
)
.await
+1 -1
View File
@@ -54,7 +54,7 @@ pub(crate) async fn kick_user_route(
..event
}),
sender_user,
&body.room_id,
Some(&body.room_id),
&state_lock,
)
.await?;
+4 -2
View File
@@ -373,7 +373,7 @@ async fn knock_room_helper_local(
.build_and_append_pdu(
PduBuilder::state(sender_user.to_string(), &content),
sender_user,
room_id,
Some(room_id),
&state_lock,
)
.await
@@ -502,6 +502,7 @@ async fn knock_room_helper_local(
knock_event,
once(parsed_knock_pdu.event_id.borrow()),
&state_lock,
room_id,
)
.await?;
@@ -672,7 +673,7 @@ async fn knock_room_helper_remote(
let statehash_after_knock = services
.rooms
.state
.append_to_state(&parsed_knock_pdu)
.append_to_state(&parsed_knock_pdu, room_id)
.await?;
info!("Updating membership locally to knock state with provided stripped state events");
@@ -701,6 +702,7 @@ async fn knock_room_helper_remote(
knock_event,
once(parsed_knock_pdu.event_id.borrow()),
&state_lock,
room_id,
)
.await?;
+1 -1
View File
@@ -206,7 +206,7 @@ pub async fn leave_room(
..event
}),
user_id,
room_id,
Some(room_id),
&state_lock,
)
.await?;
+5 -6
View File
@@ -69,11 +69,11 @@ pub(crate) async fn banned_room_check(
}
if let Some(room_id) = room_id {
if services.rooms.metadata.is_banned(room_id).await
|| services
.moderation
.is_remote_server_forbidden(room_id.server_name().expect("legacy room mxid"))
{
let room_banned = services.rooms.metadata.is_banned(room_id).await;
let server_banned = room_id.server_name().is_some_and(|server_name| {
services.moderation.is_remote_server_forbidden(server_name)
});
if room_banned || server_banned {
warn!(
"User {user_id} who is not an admin attempted to send an invite for or \
attempted to join a banned room or banned room server name: {room_id}"
@@ -106,7 +106,6 @@ pub(crate) async fn banned_room_check(
.boxed()
.await?;
}
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
}
} else if let Some(server_name) = server_name {
+1 -1
View File
@@ -47,7 +47,7 @@ pub(crate) async fn unban_user_route(
..current_member_content
}),
sender_user,
&body.room_id,
Some(&body.room_id),
&state_lock,
)
.await?;
+1 -1
View File
@@ -309,7 +309,7 @@ pub(crate) async fn visibility_filter(
services
.rooms
.state_accessor
.user_can_see_event(user_id, pdu.room_id(), pdu.event_id())
.user_can_see_event(user_id, &pdu.room_id_or_hash(), pdu.event_id())
.await
.then_some(item)
}
+5 -21
View File
@@ -1,5 +1,3 @@
use std::collections::BTreeMap;
use axum::extract::State;
use conduwuit::{
Err, Result,
@@ -226,7 +224,8 @@ pub(crate) async fn get_avatar_url_route(
/// # `GET /_matrix/client/v3/profile/{userId}`
///
/// Returns the displayname, avatar_url, blurhash, and tz of the user.
/// Returns the displayname, avatar_url, blurhash, and custom profile fields of
/// the user.
///
/// - If user is on another server and we do not have a local copy already,
/// fetch profile over federation.
@@ -260,9 +259,6 @@ pub(crate) async fn get_profile_route(
services
.users
.set_blurhash(&body.user_id, response.blurhash.clone());
services
.users
.set_timezone(&body.user_id, response.tz.clone());
for (profile_key, profile_key_value) in &response.custom_profile_fields {
services.users.set_profile_key(
@@ -276,7 +272,6 @@ pub(crate) async fn get_profile_route(
displayname: response.displayname,
avatar_url: response.avatar_url,
blurhash: response.blurhash,
tz: response.tz,
custom_profile_fields: response.custom_profile_fields,
});
}
@@ -288,21 +283,11 @@ pub(crate) async fn get_profile_route(
return Err!(Request(NotFound("Profile was not found.")));
}
let mut custom_profile_fields: BTreeMap<String, serde_json::Value> = services
.users
.all_profile_keys(&body.user_id)
.collect()
.await;
// services.users.timezone will collect the MSC4175 timezone key if it exists
custom_profile_fields.remove("us.cloke.msc4175.tz");
custom_profile_fields.remove("m.tz");
let (avatar_url, blurhash, displayname, tz) = join4(
let (avatar_url, blurhash, displayname, custom_profile_fields) = join4(
services.users.avatar_url(&body.user_id).ok(),
services.users.blurhash(&body.user_id).ok(),
services.users.displayname(&body.user_id).ok(),
services.users.timezone(&body.user_id).ok(),
services.users.all_profile_keys(&body.user_id).collect(),
)
.await;
@@ -310,7 +295,6 @@ pub(crate) async fn get_profile_route(
avatar_url,
blurhash,
displayname,
tz,
custom_profile_fields,
})
}
@@ -423,7 +407,7 @@ pub async fn update_all_rooms(
if let Err(e) = services
.rooms
.timeline
.build_and_append_pdu(pdu_builder, user_id, room_id, &state_lock)
.build_and_append_pdu(pdu_builder, user_id, Some(room_id), &state_lock)
.await
{
warn!(%user_id, %room_id, "Failed to update/send new profile join membership update in room: {e}");
+1 -1
View File
@@ -36,7 +36,7 @@ pub(crate) async fn redact_event_route(
})
},
sender_user,
&body.room_id,
Some(&body.room_id),
&state_lock,
)
.await?;
+1 -1
View File
@@ -222,7 +222,7 @@ async fn visibility_filter<Pdu: Event + Send + Sync>(
services
.rooms
.state_accessor
.user_can_see_event(sender_user, pdu.room_id(), pdu.event_id())
.user_can_see_event(sender_user, &pdu.room_id_or_hash(), pdu.event_id())
.await
.then_some(item)
}
+12 -22
View File
@@ -1,8 +1,8 @@
use std::{fmt::Write as _, ops::Mul, time::Duration};
use std::{fmt::Write as _, time::Duration};
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
use conduwuit::{Err, Event, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
use conduwuit_service::Services;
use rand::Rng;
use ruma::{
@@ -12,7 +12,6 @@
room::{report_content, report_room},
},
events::{Mentions, room::message::RoomMessageEventContent},
int,
};
use tokio::time::sleep;
@@ -25,7 +24,6 @@ struct Report {
user_id: Option<OwnedUserId>,
report_type: String,
reason: Option<String>,
score: Option<ruma::Int>,
}
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
@@ -50,6 +48,15 @@ pub(crate) async fn report_room_route(
delay_response().await;
// We log this early in case the room ID does actually exist, in which case
// admins who scan their logs can see the report and choose to investigate at
// their discretion.
info!(
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
body.room_id,
body.reason.as_deref().unwrap_or("")
);
if !services
.rooms
.state_cache
@@ -60,11 +67,6 @@ pub(crate) async fn report_room_route(
"Room does not exist to us, no local users have joined at all"
)));
}
info!(
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
body.room_id,
body.reason.as_deref().unwrap_or("")
);
let report = Report {
sender: sender_user.to_owned(),
@@ -73,7 +75,6 @@ pub(crate) async fn report_room_route(
user_id: None,
report_type: "room".to_owned(),
reason: body.reason.clone(),
score: None,
};
services.admin.send_message(build_report(report)).await.ok();
@@ -109,7 +110,6 @@ pub(crate) async fn report_event_route(
&body.room_id,
sender_user,
body.reason.as_ref(),
body.score,
&pdu,
)
.await?;
@@ -127,7 +127,6 @@ pub(crate) async fn report_event_route(
user_id: None,
report_type: "event".to_owned(),
reason: body.reason.clone(),
score: body.score,
};
services.admin.send_message(build_report(report)).await.ok();
@@ -166,7 +165,6 @@ pub(crate) async fn report_user_route(
user_id: Some(body.user_id.clone()),
report_type: "user".to_owned(),
reason: body.reason.clone(),
score: None,
};
info!(
@@ -192,7 +190,6 @@ async fn is_event_report_valid(
room_id: &RoomId,
sender_user: &UserId,
reason: Option<&String>,
score: Option<ruma::Int>,
pdu: &PduEvent,
) -> Result<()> {
debug_info!(
@@ -200,14 +197,10 @@ async fn is_event_report_valid(
valid"
);
if room_id != pdu.room_id {
if room_id != pdu.room_id_or_hash() {
return Err!(Request(NotFound("Event ID does not belong to the reported room",)));
}
if score.is_some_and(|s| s > int!(0) || s < int!(-100)) {
return Err!(Request(InvalidParam("Invalid score, must be within 0 to -100",)));
}
if reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err!(Request(
InvalidParam("Reason too long, should be 750 characters or fewer",)
@@ -240,9 +233,6 @@ fn build_report(report: Report) -> RoomMessageEventContent {
if report.event_id.is_some() {
let _ = writeln!(text, "- Reported Event ID: `{}`", report.event_id.unwrap());
}
if let Some(score) = report.score {
let _ = writeln!(text, "- User-supplied offensiveness score: {}%", score.mul(int!(-1)));
}
if let Some(reason) = report.reason {
let _ = writeln!(text, "- Report Reason: {reason}");
}
+135 -61
View File
@@ -2,9 +2,9 @@
use axum::extract::State;
use conduwuit::{
Err, Result, debug_info, debug_warn, err, info,
Err, Result, RoomVersion, debug, debug_info, debug_warn, err, info,
matrix::{StateKey, pdu::PduBuilder},
warn,
trace, warn,
};
use conduwuit_service::{Services, appservice::RegistrationInfo};
use futures::FutureExt;
@@ -49,6 +49,7 @@
/// - Send events implied by `name` and `topic`
/// - Send invite events
#[allow(clippy::large_stack_frames)]
#[allow(clippy::cognitive_complexity)]
pub(crate) async fn create_room_route(
State(services): State<crate::State>,
body: Ruma<create_room::v3::Request>,
@@ -68,51 +69,6 @@ pub(crate) async fn create_room_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let room_id: OwnedRoomId = match &body.room_id {
| Some(custom_room_id) => custom_room_id_check(&services, custom_room_id)?,
| _ => RoomId::new(&services.server.name),
};
// check if room ID doesn't already exist instead of erroring on auth check
if services.rooms.short.get_shortroomid(&room_id).await.is_ok() {
return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
}
if body.visibility == room::Visibility::Public
&& services.server.config.lockdown_public_room_directory
&& !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none()
{
warn!(
"Non-admin user {sender_user} tried to publish {room_id} to the room directory \
while \"lockdown_public_room_directory\" is enabled"
);
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
"Non-admin user {sender_user} tried to publish {room_id} to the room \
directory while \"lockdown_public_room_directory\" is enabled"
))
.await;
}
return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")));
}
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&room_id)
.await;
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
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?),
| _ => None,
};
let room_version = match body.room_version.clone() {
| Some(room_version) =>
if services.server.supported_room_version(&room_version) {
@@ -124,6 +80,52 @@ pub(crate) async fn create_room_route(
},
| None => services.server.config.default_room_version.clone(),
};
let room_features = RoomVersion::new(&room_version)?;
let room_id: Option<OwnedRoomId> = if !room_features.room_ids_as_hashes {
match &body.room_id {
| Some(custom_room_id) => Some(custom_room_id_check(&services, custom_room_id)?),
| None => Some(RoomId::new(services.globals.server_name())),
}
} else {
None
};
// check if room ID doesn't already exist instead of erroring on auth check
if let Some(ref room_id) = room_id {
if services.rooms.short.get_shortroomid(room_id).await.is_ok() {
return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
}
}
if body.visibility == room::Visibility::Public
&& services.server.config.lockdown_public_room_directory
&& !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none()
{
warn!(
"Non-admin user {sender_user} tried to publish {room_id:?} to the room directory \
while \"lockdown_public_room_directory\" is enabled"
);
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
"Non-admin user {sender_user} tried to publish {room_id:?} to the room \
directory while \"lockdown_public_room_directory\" is enabled"
))
.await;
}
return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")));
}
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?),
| _ => None,
};
let create_content = match &body.creation_content {
| Some(content) => {
@@ -164,18 +166,36 @@ pub(crate) async fn create_room_route(
let content = match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(sender_user.to_owned()),
| _ => RoomCreateEventContent::new_v11(),
| V11 => RoomCreateEventContent::new_v11(),
| _ => RoomCreateEventContent::new_v12(),
};
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())
.unwrap();
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
};
let state_lock = match room_id.clone() {
| Some(room_id) => {
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&room_id)
.await;
services.rooms.state.mutex.lock(&room_id).await
},
| None => {
let temp_room_id = RoomId::new(services.globals.server_name());
trace!("Locking temporary room state mutex for {temp_room_id}");
services.rooms.state.mutex.lock(&temp_room_id).await
},
};
// 1. The room create event
services
debug!("Creating room create event for {sender_user} in room {room_id:?}");
let tmp_id = room_id.as_deref();
let create_event_id = services
.rooms
.timeline
.build_and_append_pdu(
@@ -186,13 +206,26 @@ pub(crate) async fn create_room_route(
..Default::default()
},
sender_user,
&room_id,
tmp_id,
&state_lock,
)
.boxed()
.await?;
trace!("Created room create event with ID {}", &create_event_id);
let room_id = match room_id.clone() {
| Some(room_id) => room_id,
| None => {
let as_room_id = create_event_id.as_str().replace('$', "!");
trace!("Creating room with v12 room ID {as_room_id}");
RoomId::parse(&as_room_id)?.to_owned()
},
};
drop(state_lock);
debug!("Room created with ID {room_id}");
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
// 2. Let the room creator join
debug_info!("Joining {sender_user} to room {room_id}");
services
.rooms
.timeline
@@ -205,7 +238,7 @@ pub(crate) async fn create_room_route(
..RoomMemberEventContent::new(MembershipState::Join)
}),
sender_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -235,10 +268,37 @@ pub(crate) async fn create_room_route(
}
}
let mut creators: Vec<OwnedUserId> = vec![sender_user.to_owned()];
// Do we care about additional_creators?
if room_features.explicitly_privilege_room_creators {
// Have they been specified?
if let Some(additional_creators) = create_content.get("additional_creators") {
// Are they a real array?
if let Some(additional_creators) = additional_creators.as_array() {
// Iterate through them
for creator in additional_creators {
// Are they a string?
if let Some(creator) = creator.as_str() {
// Do they parse into a real user ID?
if let Ok(creator) = OwnedUserId::parse(creator) {
// Add them to the power levels and creators
creators.push(creator.clone());
}
}
}
}
}
} else {
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
}
let power_levels_content = default_power_levels_content(
body.power_level_content_override.as_ref(),
&body.visibility,
users,
creators,
)?;
services
@@ -252,7 +312,7 @@ pub(crate) async fn create_room_route(
..Default::default()
},
sender_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -269,7 +329,7 @@ pub(crate) async fn create_room_route(
alt_aliases: vec![],
}),
sender_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -292,7 +352,7 @@ pub(crate) async fn create_room_route(
}),
),
sender_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -308,7 +368,7 @@ pub(crate) async fn create_room_route(
&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared),
),
sender_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -327,7 +387,7 @@ pub(crate) async fn create_room_route(
}),
),
sender_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -363,7 +423,7 @@ pub(crate) async fn create_room_route(
services
.rooms
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
.build_and_append_pdu(pdu_builder, sender_user, Some(&room_id), &state_lock)
.boxed()
.await?;
}
@@ -376,7 +436,7 @@ pub(crate) async fn create_room_route(
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomNameEventContent::new(name.clone())),
sender_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -390,7 +450,7 @@ pub(crate) async fn create_room_route(
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }),
sender_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -450,6 +510,7 @@ fn default_power_levels_content(
power_level_content_override: Option<&Raw<RoomPowerLevelsEventContent>>,
visibility: &room::Visibility,
users: BTreeMap<OwnedUserId, Int>,
creators: Vec<OwnedUserId>,
) -> Result<serde_json::Value> {
let mut power_levels_content =
serde_json::to_value(RoomPowerLevelsEventContent { users, ..Default::default() })
@@ -499,6 +560,19 @@ fn default_power_levels_content(
}
}
if !creators.is_empty() {
// Raise the default power level of tombstone to 150
power_levels_content["events"]["m.room.tombstone"] =
serde_json::to_value(150).expect("150 is valid Value");
for creator in creators {
// Omit creators from the power level list altogether
power_levels_content["users"]
.as_object_mut()
.expect("users is an object")
.remove(creator.as_str());
}
}
Ok(power_levels_content)
}
+7 -7
View File
@@ -91,7 +91,7 @@ pub(crate) async fn upgrade_room_route(
replacement_room: replacement_room.clone(),
}),
sender_user,
&body.room_id,
Some(&body.room_id),
&state_lock,
)
.await?;
@@ -173,7 +173,7 @@ pub(crate) async fn upgrade_room_route(
timestamp: None,
},
sender_user,
&replacement_room,
Some(&replacement_room),
&state_lock,
)
.boxed()
@@ -204,7 +204,7 @@ pub(crate) async fn upgrade_room_route(
timestamp: None,
},
sender_user,
&replacement_room,
Some(&replacement_room),
&state_lock,
)
.boxed()
@@ -243,7 +243,7 @@ pub(crate) async fn upgrade_room_route(
..Default::default()
},
sender_user,
&replacement_room,
Some(&replacement_room),
&state_lock,
)
.boxed()
@@ -302,7 +302,7 @@ pub(crate) async fn upgrade_room_route(
..power_levels_event_content
}),
sender_user,
&body.room_id,
Some(&body.room_id),
&state_lock,
)
.boxed()
@@ -352,7 +352,7 @@ pub(crate) async fn upgrade_room_route(
..Default::default()
},
sender_user,
space_id,
Some(space_id),
&state_lock,
)
.boxed()
@@ -376,7 +376,7 @@ pub(crate) async fn upgrade_room_route(
..Default::default()
},
sender_user,
space_id,
Some(space_id),
&state_lock,
)
.boxed()
+1 -1
View File
@@ -80,7 +80,7 @@ pub(crate) async fn send_message_event_route(
..Default::default()
},
sender_user,
&body.room_id,
Some(&body.room_id),
&state_lock,
)
.await?;
+2 -2
View File
@@ -145,9 +145,9 @@ pub(super) async fn ldap_login(
let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await;
if is_ldap_admin && !is_conduwuit_admin {
services.admin.make_user_admin(lowercased_user_id).await?;
Box::pin(services.admin.make_user_admin(lowercased_user_id)).await?;
} else if !is_ldap_admin && is_conduwuit_admin {
services.admin.revoke_admin(lowercased_user_id).await?;
Box::pin(services.admin.revoke_admin(lowercased_user_id)).await?;
}
Ok(user_id)
+1 -1
View File
@@ -201,7 +201,7 @@ async fn send_state_event_for_key_helper(
..Default::default()
},
sender,
room_id,
Some(room_id),
&state_lock,
)
.await?;
+1 -1
View File
@@ -457,7 +457,7 @@ async fn handle_left_room(
state_key: Some(sender_user.as_str().into()),
unsigned: None,
// The following keys are dropped on conversion
room_id: room_id.clone(),
room_id: Some(room_id.clone()),
prev_events: vec![],
depth: uint!(1),
auth_events: vec![],
+11 -135
View File
@@ -2,18 +2,14 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Error, Result};
use conduwuit::{Err, Result};
use futures::StreamExt;
use ruma::{
OwnedRoomId,
api::{
client::{
error::ErrorKind,
membership::mutual_rooms,
profile::{
delete_profile_key, delete_timezone_key, get_profile_key, get_timezone_key,
set_profile_key, set_timezone_key,
},
profile::{delete_profile_key, get_profile_key, set_profile_key},
},
federation,
},
@@ -60,62 +56,6 @@ pub(crate) async fn get_mutual_rooms_route(
})
}
/// # `DELETE /_matrix/client/unstable/uk.tcpip.msc4133/profile/:user_id/us.cloke.msc4175.tz`
///
/// Deletes the `tz` (timezone) of a user, as per MSC4133 and MSC4175.
///
/// - Also makes sure other users receive the update using presence EDUs
pub(crate) async fn delete_timezone_key_route(
State(services): State<crate::State>,
body: Ruma<delete_timezone_key::unstable::Request>,
) -> Result<delete_timezone_key::unstable::Response> {
let sender_user = body.sender_user();
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));
}
services.users.set_timezone(&body.user_id, None);
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await?;
}
Ok(delete_timezone_key::unstable::Response {})
}
/// # `PUT /_matrix/client/unstable/uk.tcpip.msc4133/profile/:user_id/us.cloke.msc4175.tz`
///
/// Updates the `tz` (timezone) of a user, as per MSC4133 and MSC4175.
///
/// - Also makes sure other users receive the update using presence EDUs
pub(crate) async fn set_timezone_key_route(
State(services): State<crate::State>,
body: Ruma<set_timezone_key::unstable::Request>,
) -> Result<set_timezone_key::unstable::Response> {
let sender_user = body.sender_user();
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));
}
services.users.set_timezone(&body.user_id, body.tz.clone());
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await?;
}
Ok(set_timezone_key::unstable::Response {})
}
/// # `PUT /_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}`
///
/// Updates the profile key-value field of a user, as per MSC4133.
@@ -150,19 +90,14 @@ pub(crate) async fn set_profile_key_route(
)));
};
if body
.kv_pair
.keys()
.any(|key| key.starts_with("u.") && !profile_key_value.is_string())
{
return Err!(Request(BadJson("u.* profile key fields must be strings")));
}
if body.kv_pair.keys().any(|key| key.len() > 128) {
return Err!(Request(BadJson("Key names cannot be longer than 128 bytes")));
}
if body.key_name == "displayname" {
let Some(display_name) = profile_key_value.as_str() else {
return Err!(Request(BadJson("displayname must be a string")));
};
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
@@ -174,12 +109,15 @@ pub(crate) async fn set_profile_key_route(
update_displayname(
&services,
&body.user_id,
Some(profile_key_value.to_string()),
Some(display_name.to_owned()),
&all_joined_rooms,
)
.await;
} else if body.key_name == "avatar_url" {
let mxc = ruma::OwnedMxcUri::from(profile_key_value.to_string());
let Some(avatar_url) = profile_key_value.as_str() else {
return Err!(Request(BadJson("avatar_url must be a string")));
};
let mxc = ruma::OwnedMxcUri::from(avatar_url);
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
@@ -268,70 +206,12 @@ pub(crate) async fn delete_profile_key_route(
Ok(delete_profile_key::unstable::Response {})
}
/// # `GET /_matrix/client/unstable/uk.tcpip.msc4133/profile/:user_id/us.cloke.msc4175.tz`
///
/// Returns the `timezone` of the user as per MSC4133 and MSC4175.
///
/// - If user is on another server and we do not have a local copy already fetch
/// `timezone` over federation
pub(crate) async fn get_timezone_key_route(
State(services): State<crate::State>,
body: Ruma<get_timezone_key::unstable::Request>,
) -> Result<get_timezone_key::unstable::Response> {
if !services.globals.user_is_local(&body.user_id) {
// Create and update our local copy of the user
if let Ok(response) = services
.sending
.send_federation_request(
body.user_id.server_name(),
federation::query::get_profile_information::v1::Request {
user_id: body.user_id.clone(),
field: None, // we want the full user's profile to update locally as well
},
)
.await
{
if !services.users.exists(&body.user_id).await {
services.users.create(&body.user_id, None, None).await?;
}
services
.users
.set_displayname(&body.user_id, response.displayname.clone());
services
.users
.set_avatar_url(&body.user_id, response.avatar_url.clone());
services
.users
.set_blurhash(&body.user_id, response.blurhash.clone());
services
.users
.set_timezone(&body.user_id, response.tz.clone());
return Ok(get_timezone_key::unstable::Response { tz: response.tz });
}
}
if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over
// federation
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
}
Ok(get_timezone_key::unstable::Response {
tz: services.users.timezone(&body.user_id).await.ok(),
})
}
/// # `GET /_matrix/client/unstable/uk.tcpip.msc4133/profile/{userId}/{field}}`
///
/// Gets the profile key-value field of a user, as per MSC4133.
///
/// - If user is on another server and we do not have a local copy already fetch
/// `timezone` over federation
/// the value over federation
pub(crate) async fn get_profile_key_route(
State(services): State<crate::State>,
body: Ruma<get_profile_key::unstable::Request>,
@@ -367,10 +247,6 @@ pub(crate) async fn get_profile_key_route(
.users
.set_blurhash(&body.user_id, response.blurhash.clone());
services
.users
.set_timezone(&body.user_id, response.tz.clone());
match response.custom_profile_fields.get(&body.key_name) {
| Some(value) => {
profile_key_value.insert(body.key_name.clone(), value.clone());
-3
View File
@@ -22,12 +22,9 @@
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
let config = &server.config;
let mut router = router
.ruma_route(&client::get_timezone_key_route)
.ruma_route(&client::get_profile_key_route)
.ruma_route(&client::set_profile_key_route)
.ruma_route(&client::delete_profile_key_route)
.ruma_route(&client::set_timezone_key_route)
.ruma_route(&client::delete_timezone_key_route)
.ruma_route(&client::appservice_ping)
.ruma_route(&client::get_supported_versions_route)
.ruma_route(&client::get_register_available_route)
+2 -5
View File
@@ -20,9 +20,7 @@
client::{
directory::get_public_rooms,
error::ErrorKind,
profile::{
get_avatar_url, get_display_name, get_profile, get_profile_key, get_timezone_key,
},
profile::{get_avatar_url, get_display_name, get_profile, get_profile_key},
voip::get_turn_server_info,
},
federation::{authentication::XMatrix, openid::get_openid_userinfo},
@@ -89,8 +87,7 @@ pub(super) async fn auth(
| &get_profile::v3::Request::METADATA
| &get_profile_key::unstable::Request::METADATA
| &get_display_name::v3::Request::METADATA
| &get_avatar_url::v3::Request::METADATA
| &get_timezone_key::unstable::Request::METADATA => {
| &get_avatar_url::v3::Request::METADATA => {
if services.server.config.require_auth_for_profile_requests {
match token {
| Token::Appservice(_) | Token::User(_) => {
+2 -2
View File
@@ -2,7 +2,7 @@
use axum::extract::State;
use conduwuit::{
PduCount, Result,
Event, PduCount, Result,
utils::{IterStream, ReadyExt, stream::TryTools},
};
use futures::{FutureExt, StreamExt, TryStreamExt};
@@ -68,7 +68,7 @@ pub(crate) async fn get_backfill_route(
Ok(services
.rooms
.state_accessor
.server_can_see_event(body.origin(), &pdu.room_id, &pdu.event_id)
.server_can_see_event(body.origin(), &pdu.room_id_or_hash(), &pdu.event_id)
.await
.then_some(pdu))
})
+1 -1
View File
@@ -122,7 +122,7 @@ pub(crate) async fn create_join_event_template_route(
..RoomMemberEventContent::new(MembershipState::Join)
}),
&body.user_id,
&body.room_id,
Some(&body.room_id),
&state_lock,
)
.await?;
+1 -1
View File
@@ -95,7 +95,7 @@ pub(crate) async fn create_knock_event_template_route(
&RoomMemberEventContent::new(MembershipState::Knock),
),
&body.user_id,
&body.room_id,
Some(&body.room_id),
&state_lock,
)
.await?;
+1 -1
View File
@@ -45,7 +45,7 @@ pub(crate) async fn create_leave_event_template_route(
&RoomMemberEventContent::new(MembershipState::Leave),
),
&body.user_id,
&body.room_id,
Some(&body.room_id),
&state_lock,
)
.await?;
-7
View File
@@ -83,7 +83,6 @@ pub(crate) async fn get_profile_information_route(
let mut displayname = None;
let mut avatar_url = None;
let mut blurhash = None;
let mut tz = None;
let mut custom_profile_fields = BTreeMap::new();
match &body.field {
@@ -107,7 +106,6 @@ pub(crate) async fn get_profile_information_route(
displayname = services.users.displayname(&body.user_id).await.ok();
avatar_url = services.users.avatar_url(&body.user_id).await.ok();
blurhash = services.users.blurhash(&body.user_id).await.ok();
tz = services.users.timezone(&body.user_id).await.ok();
custom_profile_fields = services
.users
.all_profile_keys(&body.user_id)
@@ -116,15 +114,10 @@ pub(crate) async fn get_profile_information_route(
},
}
// services.users.timezone will collect the MSC4175 timezone key if it exists
custom_profile_fields.remove("us.cloke.msc4175.tz");
custom_profile_fields.remove("m.tz");
Ok(get_profile_information::v1::Response {
displayname,
avatar_url,
blurhash,
tz,
custom_profile_fields,
})
}
+5 -1
View File
@@ -175,7 +175,11 @@ pub(crate) async fn create_knock_event_v1_route(
.send_pdu_room(&body.room_id, &pdu_id)
.await?;
let knock_room_state = services.rooms.state.summary_stripped(&pdu).await;
let knock_room_state = services
.rooms
.state
.summary_stripped(&pdu, &body.room_id)
.await;
Ok(send_knock::v1::Response { knock_room_state })
}
+1 -1
View File
@@ -92,7 +92,7 @@ ruma.workspace = true
sanitize-filename.workspace = true
serde_json.workspace = true
serde_regex.workspace = true
serde_yaml.workspace = true
serde_yml.workspace = true
serde.workspace = true
smallvec.workspace = true
smallstr.workspace = true
+1 -1
View File
@@ -83,7 +83,7 @@ pub enum Error {
#[error(transparent)]
TypedHeader(#[from] axum_extra::typed_header::TypedHeaderRejection),
#[error(transparent)]
Yaml(#[from] serde_yaml::Error),
Yaml(#[from] serde_yml::Error),
// ruma/conduwuit
#[error("Arithmetic operation failed: {0}")]
+1 -1
View File
@@ -18,7 +18,7 @@
/// Experimental, partially supported room versions
pub const UNSTABLE_ROOM_VERSIONS: &[RoomVersionId] =
&[RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5];
&[RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5, RoomVersionId::V12];
type RoomVersion = (RoomVersionId, RoomVersionStability);
+1 -1
View File
@@ -27,5 +27,5 @@ fn init_user_agent() -> String { format!("{}/{}", name(), version()) }
fn init_version() -> String {
conduwuit_build_metadata::version_tag()
.map_or(SEMANTIC.to_owned(), |extra| format!("{SEMANTIC} ({extra})"))
.map_or_else(|| SEMANTIC.to_owned(), |extra| format!("{SEMANTIC} ({extra})"))
}
+7 -2
View File
@@ -10,7 +10,7 @@
use std::fmt::Debug;
use ruma::{
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId,
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, RoomId,
RoomVersionId, UserId, events::TimelineEventType,
};
use serde::Deserialize;
@@ -168,7 +168,12 @@ fn as_mut_pdu(&mut self) -> &mut Pdu { unimplemented!("not a mutable Pdu") }
fn redacts(&self) -> Option<&EventId>;
/// The `RoomId` of this event.
fn room_id(&self) -> &RoomId;
fn room_id(&self) -> Option<&RoomId>;
/// The `RoomId` or hash of this event.
/// This should only be preferred over room_id() if the event is a v12
/// create event.
fn room_id_or_hash(&self) -> OwnedRoomId;
/// The `UserId` of this event.
fn sender(&self) -> &UserId;
+9 -2
View File
@@ -32,12 +32,19 @@ fn matches(&self, event: &E) -> bool {
}
fn matches_room<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
if filter.not_rooms.iter().any(is_equal_to!(event.room_id())) {
if filter
.not_rooms
.iter()
.any(is_equal_to!(event.room_id().expect("event has a room ID")))
{
return false;
}
if let Some(rooms) = filter.rooms.as_ref() {
if !rooms.iter().any(is_equal_to!(event.room_id())) {
if !rooms
.iter()
.any(is_equal_to!(event.room_id().expect("event has a room ID")))
{
return false;
}
}
+44 -3
View File
@@ -31,7 +31,8 @@
pub struct Pdu {
pub event_id: OwnedEventId,
pub room_id: OwnedRoomId,
#[serde(skip_serializing_if = "Option::is_none")]
pub room_id: Option<OwnedRoomId>,
pub sender: OwnedUserId,
@@ -110,7 +111,27 @@ fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Sen
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
#[inline]
fn room_id(&self) -> &RoomId { &self.room_id }
fn room_id(&self) -> Option<&RoomId> { self.room_id.as_deref() }
#[inline]
fn room_id_or_hash(&self) -> OwnedRoomId {
if *self.event_type() != TimelineEventType::RoomCreate {
return self
.room_id()
.expect("Event must have a room ID")
.to_owned();
}
if let Some(room_id) = &self.room_id {
// v1-v11
room_id.clone()
} else {
// v12+
let constructed_hash = self.event_id.as_str().replace('$', "!");
RoomId::parse(&constructed_hash)
.expect("event ID can be parsed")
.to_owned()
}
}
#[inline]
fn sender(&self) -> &UserId { &self.sender }
@@ -163,7 +184,27 @@ fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Sen
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
#[inline]
fn room_id(&self) -> &RoomId { &self.room_id }
fn room_id(&self) -> Option<&RoomId> { self.room_id.as_ref().map(AsRef::as_ref) }
#[inline]
fn room_id_or_hash(&self) -> OwnedRoomId {
if *self.event_type() != TimelineEventType::RoomCreate {
return self
.room_id()
.expect("Event must have a room ID")
.to_owned();
}
if let Some(room_id) = &self.room_id {
// v1-v11
room_id.clone()
} else {
// v12+
let constructed_hash = self.event_id.as_str().replace('$', "!");
RoomId::parse(&constructed_hash)
.expect("event ID can be parsed")
.to_owned()
}
}
#[inline]
fn sender(&self) -> &UserId { &self.sender }
+1 -1
View File
@@ -406,7 +406,7 @@ fn to_pdu_event<S>(
Pdu {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
room_id: Some(room_id().to_owned()),
sender: sender.to_owned(),
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
+276 -109
View File
@@ -2,7 +2,7 @@
use futures::{
Future,
future::{OptionFuture, join3},
future::{OptionFuture, join, join3},
};
use ruma::{
Int, OwnedUserId, RoomVersionId, UserId,
@@ -44,6 +44,15 @@ struct RoomMemberContentFields {
join_authorised_via_users_server: Option<Raw<OwnedUserId>>,
}
#[derive(Deserialize)]
struct RoomCreateContentFields {
room_version: Option<Raw<RoomVersionId>>,
creator: Option<Raw<IgnoredAny>>,
additional_creators: Option<Vec<Raw<OwnedUserId>>>,
#[serde(rename = "m.federate", default = "ruma::serde::default_true")]
federate: bool,
}
/// For the given event `kind` what are the relevant auth events that are needed
/// to authenticate this `content`.
///
@@ -56,16 +65,24 @@ pub fn auth_types_for_event(
sender: &UserId,
state_key: Option<&str>,
content: &RawJsonValue,
room_version: &RoomVersion,
) -> serde_json::Result<Vec<(StateEventType, StateKey)>> {
if kind == &TimelineEventType::RoomCreate {
return Ok(vec![]);
}
let mut auth_types = vec![
(StateEventType::RoomPowerLevels, StateKey::new()),
(StateEventType::RoomMember, sender.as_str().into()),
(StateEventType::RoomCreate, StateKey::new()),
];
let mut auth_types = if room_version.room_ids_as_hashes {
vec![
(StateEventType::RoomPowerLevels, StateKey::new()),
(StateEventType::RoomMember, sender.as_str().into()),
]
} else {
vec![
(StateEventType::RoomPowerLevels, StateKey::new()),
(StateEventType::RoomMember, sender.as_str().into()),
(StateEventType::RoomCreate, StateKey::new()),
]
};
if kind == &TimelineEventType::RoomMember {
#[derive(Deserialize)]
@@ -136,11 +153,13 @@ struct RoomMemberContentFields {
event_id = incoming_event.event_id().as_str(),
)
)]
#[allow(clippy::suspicious_operation_groupings)]
pub async fn auth_check<E, F, Fut>(
room_version: &RoomVersion,
incoming_event: &E,
current_third_party_invite: Option<&E>,
fetch_state: F,
create_event: &E,
) -> Result<bool, Error>
where
F: Fn(&StateEventType, &str) -> Fut + Send,
@@ -169,12 +188,6 @@ pub async fn auth_check<E, F, Fut>(
//
// 1. If type is m.room.create:
if *incoming_event.event_type() == TimelineEventType::RoomCreate {
#[derive(Deserialize)]
struct RoomCreateContentFields {
room_version: Option<Raw<RoomVersionId>>,
creator: Option<Raw<IgnoredAny>>,
}
debug!("start m.room.create check");
// If it has any previous events, reject
@@ -184,14 +197,16 @@ struct RoomCreateContentFields {
}
// If the domain of the room_id does not match the domain of the sender, reject
let Some(room_id_server_name) = incoming_event.room_id().server_name() else {
warn!("room ID has no servername");
return Ok(false);
};
if room_id_server_name != sender.server_name() {
warn!("servername of room ID does not match servername of sender");
return Ok(false);
if incoming_event.room_id().is_some() {
let Some(room_id_server_name) = incoming_event.room_id().unwrap().server_name()
else {
warn!("room ID has no servername");
return Ok(false);
};
if room_id_server_name != sender.server_name() {
warn!("servername of room ID does not match servername of sender");
return Ok(false);
}
}
// If content.room_version is present and is not a recognized version, reject
@@ -204,7 +219,14 @@ struct RoomCreateContentFields {
return Ok(false);
}
if !room_version.use_room_create_sender {
if room_version.room_ids_as_hashes && incoming_event.room_id().is_some() {
warn!("room create event incorrectly claims a room ID");
return Ok(false);
}
if !room_version.use_room_create_sender
&& !room_version.explicitly_privilege_room_creators
{
// If content has no creator field, reject
if content.creator.is_none() {
warn!("no creator field found in m.room.create content");
@@ -216,6 +238,8 @@ struct RoomCreateContentFields {
return Ok(true);
}
// NOTE(hydra): We always have a room ID from this point forward.
/*
// TODO: In the past this code was commented as it caused problems with Synapse. This is no
// longer the case. This needs to be implemented.
@@ -242,54 +266,69 @@ struct RoomCreateContentFields {
}
*/
let (room_create_event, power_levels_event, sender_member_event) = join3(
fetch_state(&StateEventType::RoomCreate, ""),
let (power_levels_event, sender_member_event) = join(
// fetch_state(&StateEventType::RoomCreate, ""),
fetch_state(&StateEventType::RoomPowerLevels, ""),
fetch_state(&StateEventType::RoomMember, sender.as_str()),
)
.await;
let room_create_event = match room_create_event {
| None => {
warn!("no m.room.create event in auth chain");
return Ok(false);
},
| Some(e) => e,
};
let room_create_event = create_event.clone();
if incoming_event.room_id() != room_create_event.room_id() {
warn!("room_id of incoming event does not match room_id of m.room.create event");
// Get the content of the room create event, used later.
let room_create_content: RoomCreateContentFields =
from_json_str(room_create_event.content().get())?;
if room_create_content
.room_version
.is_some_and(|v| v.deserialize().is_err())
{
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().unwrap() != expected_room_id {
warn!(
expected = %expected_room_id,
received = %incoming_event.room_id().unwrap(),
"room_id of incoming event ({}) does not match room_id of m.room.create event ({})",
incoming_event.room_id().unwrap(),
expected_room_id,
);
return Ok(false);
}
// If the create event is referenced in the event's auth events, and this is a
// v12 room, reject
let claims_create_event = incoming_event
.auth_events()
.any(|id| id == room_create_event.event_id());
if room_version.room_ids_as_hashes && claims_create_event {
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!("no m.room.create event found in auth events");
return Ok(false);
}
if let Some(ref pe) = power_levels_event {
if pe.room_id() != room_create_event.room_id() {
warn!("room_id of power levels event does not match room_id of m.room.create event");
if *pe.room_id().unwrap() != expected_room_id {
warn!(
expected = %expected_room_id,
received = %pe.room_id().unwrap(),
"room_id of power levels event does not match room_id of m.room.create event"
);
return Ok(false);
}
}
// 3. If event does not have m.room.create in auth_events reject
if !incoming_event
.auth_events()
.any(|id| id == room_create_event.event_id())
{
warn!("no m.room.create event in auth events");
return Ok(false);
}
// If the create event content has the field m.federate set to false and the
// sender domain of the event does not match the sender domain of the create
// event, reject.
#[derive(Deserialize)]
#[allow(clippy::items_after_statements)]
struct RoomCreateContentFederate {
#[serde(rename = "m.federate", default = "ruma::serde::default_true")]
federate: bool,
}
let room_create_content: RoomCreateContentFederate =
from_json_str(room_create_event.content().get())?;
if !room_create_content.federate
if !room_version.room_ids_as_hashes
&& !room_create_content.federate
&& room_create_event.sender().server_name() != incoming_event.sender().server_name()
{
warn!(
@@ -321,7 +360,7 @@ struct RoomCreateContentFederate {
debug!("starting m.room.member check");
let state_key = match incoming_event.state_key() {
| None => {
warn!("no statekey in member event");
warn!("no state key in member event");
return Ok(false);
},
| Some(s) => s,
@@ -377,6 +416,7 @@ struct RoomCreateContentFederate {
&user_for_join_auth_membership,
&room_create_event,
)? {
warn!("membership change not valid for some reason");
return Ok(false);
}
@@ -394,8 +434,18 @@ struct RoomCreateContentFederate {
},
};
if sender_member_event.room_id() != room_create_event.room_id() {
warn!("room_id of incoming event does not match room_id of m.room.create event");
if sender_member_event
.room_id()
.expect("we have a room ID for non create events")
!= expected_room_id
{
warn!(
"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"),
expected_room_id
);
return Ok(false);
}
@@ -417,7 +467,7 @@ struct RoomCreateContentFederate {
}
// If type is m.room.third_party_invite
let sender_power_level = match &power_levels_event {
let mut sender_power_level = match &power_levels_event {
| Some(pl) => {
let content =
deserialize_power_levels_content_fields(pl.content().get(), room_version)?;
@@ -439,6 +489,24 @@ struct RoomCreateContentFederate {
if is_creator { int!(100) } else { int!(0) }
},
};
if room_version.explicitly_privilege_room_creators {
// If the user sent the create event, or is listed in additional_creators, just
// give them Int::MAX
if sender == room_create_event.sender()
|| room_create_content
.additional_creators
.as_ref()
.is_some_and(|creators| {
creators
.iter()
.any(|c| c.deserialize().is_ok_and(|c| c == *sender))
}) {
trace!("privileging room creator or additional creator");
// This user is the room creator or an additional creator, give them max power
// level
sender_power_level = Int::MAX;
}
}
// Allow if and only if sender's current power level is greater than
// or equal to the invite level
@@ -519,6 +587,26 @@ struct RoomCreateContentFederate {
Ok(true)
}
fn is_creator<EV>(v: &RoomVersion, c: &BTreeSet<OwnedUserId>, ce: &EV, user_id: &UserId) -> bool
where
EV: Event + Send + Sync,
{
if v.explicitly_privilege_room_creators {
c.contains(user_id)
} else if v.use_room_create_sender {
ce.sender() == user_id
} else {
#[allow(deprecated)]
let creator = from_json_str::<RoomCreateEventContent>(ce.content().get())
.unwrap()
.creator
.ok_or_else(|| serde_json::Error::missing_field("creator"))
.unwrap();
creator == user_id
}
}
// TODO deserializing the member, power, join_rules event contents is done in
// conduit just before this is called. Could they be passed in?
/// Does the user who sent this member event have required power levels to do
@@ -554,6 +642,7 @@ fn valid_membership_change<E>(
struct GetThirdPartyInvite {
third_party_invite: Option<Raw<ThirdPartyInvite>>,
}
let create_content = from_json_str::<RoomCreateContentFields>(create_room.content().get())?;
let content = current_event.content();
let target_membership = from_json_str::<GetMembership>(content.get())?.membership;
@@ -576,15 +665,37 @@ struct GetThirdPartyInvite {
| None => RoomPowerLevelsEventContent::default(),
};
let sender_power = power_levels
let mut sender_power = power_levels
.users
.get(sender)
.or_else(|| sender_is_joined.then_some(&power_levels.users_default));
let target_power = power_levels.users.get(target_user).or_else(|| {
let mut target_power = power_levels.users.get(target_user).or_else(|| {
(target_membership == MembershipState::Join).then_some(&power_levels.users_default)
});
let mut creators = BTreeSet::new();
creators.insert(create_room.sender().to_owned());
if room_version.explicitly_privilege_room_creators {
// Explicitly privilege room creators
// If the sender sent the create event, or in additional_creators, give them
// Int::MAX. Same case for target.
if let Some(additional_creators) = &create_content.additional_creators {
for c in additional_creators {
if let Ok(c) = c.deserialize() {
creators.insert(c);
}
}
}
if creators.contains(sender) {
sender_power = Some(&Int::MAX);
}
if creators.contains(target_user) {
target_power = Some(&Int::MAX);
}
}
trace!(?creators, "creators for room");
let mut join_rules = JoinRule::Invite;
if let Some(jr) = &join_rules_event {
join_rules = from_json_str::<RoomJoinRulesEventContent>(jr.content().get())?.join_rule;
@@ -613,15 +724,21 @@ struct GetThirdPartyInvite {
} else {
(int!(0), int!(0))
};
(user_for_join_auth_membership == &MembershipState::Join)
&& (auth_user_pl >= invite_level)
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;
user_joined && okay_power
} else {
// No auth user was given
false
};
let sender_creator = is_creator(room_version, &creators, create_room, sender);
let target_creator = is_creator(room_version, &creators, create_room, target_user);
Ok(match target_membership {
| MembershipState::Join => {
trace!("starting target_membership=join check");
// 1. If the only previous event is an m.room.create and the state_key is the
// creator,
// allow
@@ -633,24 +750,25 @@ struct GetThirdPartyInvite {
let no_more_prev_events = prev_events.next().is_none();
if prev_event_is_create_event && no_more_prev_events {
let is_creator = if room_version.use_room_create_sender {
let creator = create_room.sender();
creator == sender && creator == target_user
} else {
#[allow(deprecated)]
let creator = from_json_str::<RoomCreateEventContent>(create_room.content().get())?
.creator
.ok_or_else(|| serde_json::Error::missing_field("creator"))?;
creator == sender && creator == target_user
};
trace!(
sender = %sender,
target_user = %target_user,
?sender_creator,
?target_creator,
"checking if sender is a room creator for initial membership event"
);
let is_creator = sender_creator && target_creator;
if is_creator {
debug!("sender is room creator, allowing join");
return Ok(true);
}
trace!("sender is not room creator, proceeding with normal auth checks");
}
let membership_allows_join = matches!(
target_user_current_membership,
MembershipState::Join | MembershipState::Invite
);
if sender != target_user {
// If the sender does not match state_key, reject.
warn!("Can't make other user join");
@@ -659,39 +777,81 @@ struct GetThirdPartyInvite {
// If the sender is banned, reject.
warn!(?target_user_membership_event_id, "Banned user can't join");
false
} else if (join_rules == JoinRule::Invite
|| room_version.allow_knocking && (join_rules == JoinRule::Knock || matches!(join_rules, JoinRule::KnockRestricted(_))))
// If the join_rule is invite then allow if membership state is invite or join
&& (target_user_current_membership == MembershipState::Join
|| target_user_current_membership == MembershipState::Invite)
{
true
} else if room_version.restricted_join_rules
&& matches!(join_rules, JoinRule::Restricted(_))
|| room_version.knock_restricted_join_rule
&& matches!(join_rules, JoinRule::KnockRestricted(_))
{
// If the join_rule is restricted or knock_restricted
if matches!(
target_user_current_membership,
MembershipState::Invite | MembershipState::Join
) {
// If membership state is join or invite, allow.
true
} else {
// If the join_authorised_via_users_server key in content is not a user with
// sufficient permission to invite other users, reject.
// Otherwise, allow.
user_for_join_auth_is_valid
}
} else {
// If the join_rule is public, allow.
// Otherwise, reject.
join_rules == JoinRule::Public
match join_rules {
| JoinRule::Invite =>
if !membership_allows_join {
warn!(
membership=?target_user_current_membership,
"Join rule is invite but membership does not allow join"
);
false
} else {
true
},
| JoinRule::Knock if !room_version.allow_knocking => {
warn!("Join rule is knock but room version does not allow knocking");
false
},
| JoinRule::Knock =>
if !membership_allows_join {
warn!(
membership=?target_user_current_membership,
"Join rule is knock but membership does not allow join"
);
false
} else {
true
},
| JoinRule::KnockRestricted(_) if !room_version.knock_restricted_join_rule =>
{
warn!(
"Join rule is knock_restricted but room version does not support it"
);
false
},
| JoinRule::KnockRestricted(_) => {
let valid_join = user_for_join_auth_is_valid
|| sender_membership == MembershipState::Join;
if membership_allows_join || valid_join {
true
} else {
warn!(
membership=?target_user_current_membership,
"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 !user_for_join_auth_is_valid
&& sender_membership != MembershipState::Join
{
warn!(
"Join rule is a restricted one but no valid authorising user \
was given"
);
false
} else {
true
},
| JoinRule::Public => true,
| _ => {
warn!(
join_rule=?join_rules,
membership=?target_user_current_membership,
"Unknown join rule doesn't allow joining, or the rule's conditions were not met"
);
false
},
}
}
},
| MembershipState::Invite => {
// If content has third_party_invite key
trace!("starting target_membership=invite check");
match third_party_invite.and_then(|i| i.deserialize().ok()) {
| Some(tp_id) =>
if target_user_current_membership == MembershipState::Ban {
@@ -722,9 +882,10 @@ struct GetThirdPartyInvite {
);
false
} else {
let allow = sender_power
.filter(|&p| p >= &power_levels.invite)
.is_some();
let allow = sender_creator
|| sender_power
.filter(|&p| p >= &power_levels.invite)
.is_some();
if !allow {
warn!(
?target_user_membership_event_id,
@@ -752,7 +913,8 @@ struct GetThirdPartyInvite {
allow
} else if !sender_is_joined
|| target_user_current_membership == MembershipState::Ban
&& sender_power.filter(|&p| p < &power_levels.ban).is_some()
&& (sender_creator
|| sender_power.filter(|&p| p < &power_levels.ban).is_some())
{
warn!(
?target_user_membership_event_id,
@@ -761,8 +923,9 @@ struct GetThirdPartyInvite {
);
false
} else {
let allow = sender_power.filter(|&p| p >= &power_levels.kick).is_some()
&& target_power < sender_power;
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,
@@ -777,8 +940,9 @@ struct GetThirdPartyInvite {
warn!(?sender_membership_event_id, "Can't ban user if sender is not joined");
false
} else {
let allow = sender_power.filter(|&p| p >= &power_levels.ban).is_some()
&& target_power < sender_power;
let allow = sender_creator
|| (sender_power.filter(|&p| p >= &power_levels.ban).is_some()
&& target_power < sender_power);
if !allow {
warn!(
?target_user_membership_event_id,
@@ -843,12 +1007,14 @@ struct GetThirdPartyInvite {
/// Does the event have the correct userId as its state_key if it's not the ""
/// state_key.
fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int) -> bool {
// TODO(hydra): This function does not care about creators!
let event_type_power_level = get_send_level(event.event_type(), event.state_key(), ple);
debug!(
required_level = i64::from(event_type_power_level),
user_level = i64::from(user_level),
state_key = ?event.state_key(),
power_level_event_id = ?ple.map(|e| e.event_id().as_str()),
"permissions factors",
);
@@ -872,6 +1038,7 @@ fn check_power_levels(
previous_power_event: Option<&impl Event>,
user_level: Int,
) -> Option<bool> {
// TODO(hydra): This function does not care about creators!
match power_event.state_key() {
| Some("") => {},
| Some(key) => {
+132 -10
View File
@@ -38,6 +38,7 @@
use crate::{
debug, debug_error,
matrix::{Event, StateKey},
state_res::room_version::StateResolutionVersion,
trace,
utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, WidebandExt},
warn,
@@ -92,7 +93,12 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
Pdu: Event + Clone + Send + Sync,
for<'b> &'b Pdu: Event + Send,
{
debug!("State resolution starting");
use RoomVersionId::*;
let stateres_version = match room_version {
| V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 | V11 => StateResolutionVersion::V2,
| _ => StateResolutionVersion::V2_1,
};
debug!(version = ?stateres_version, "State resolution starting");
// Split non-conflicting and conflicting state
let (clean, conflicting) = separate(state_sets.into_iter());
@@ -107,14 +113,27 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
debug!(count = conflicting.len(), "conflicting events");
trace!(map = ?conflicting, "conflicting events");
let conflicted_state_subgraph: HashSet<_> = match stateres_version {
| StateResolutionVersion::V2_1 =>
calculate_conflicted_subgraph(&conflicting, event_fetch)
.await
.ok_or_else(|| {
Error::InvalidPdu("Failed to calculate conflicted subgraph".to_owned())
})?,
| _ => HashSet::new(),
};
debug!(count = conflicted_state_subgraph.len(), "conflicted subgraph");
trace!(set = ?conflicted_state_subgraph, "conflicted subgraph");
let conflicting_values = conflicting.into_values().flatten().stream();
// `all_conflicted` contains unique items
// synapse says `full_set = {eid for eid in full_conflicted_set if eid in
// event_map}`
// Hydra: Also consider the conflicted state subgraph
let all_conflicted: HashSet<_> = get_auth_chain_diff(auth_chain_sets)
.chain(conflicting_values)
.chain(conflicted_state_subgraph.into_iter().stream())
.broad_filter_map(async |id| event_exists(id.clone()).await.then_some(id))
.collect()
.await;
@@ -150,6 +169,7 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
// Sequentially auth check each control event.
let resolved_control = iterative_auth_check(
&room_version,
&stateres_version,
sorted_control_levels.iter().stream().map(AsRef::as_ref),
clean.clone(),
&event_fetch,
@@ -163,6 +183,9 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
// sort the remaining events using the mainline of the resolved power level.
let deduped_power_ev: HashSet<_> = sorted_control_levels.into_iter().collect();
debug!(count = deduped_power_ev.len(), "deduped power events");
trace!(set = ?deduped_power_ev, "deduped power events");
// This removes the control events that passed auth and more importantly those
// that failed auth
let events_to_resolve: Vec<_> = all_conflicted
@@ -183,12 +206,13 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
let sorted_left_events =
mainline_sort(&events_to_resolve, power_event.cloned(), &event_fetch).await?;
trace!(list = ?sorted_left_events, "events left, sorted");
trace!(list = ?sorted_left_events, "events left, sorted, running iterative auth check");
let mut resolved_state = iterative_auth_check(
&room_version,
&stateres_version,
sorted_left_events.iter().stream().map(AsRef::as_ref),
resolved_control, // The control events are added to the final resolved state
resolved_control.clone(), // The control events are added to the final resolved state
&event_fetch,
)
.await?;
@@ -196,8 +220,14 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
// Add unconflicted state to the resolved state
// We priorities the unconflicting state
resolved_state.extend(clean);
if stateres_version == StateResolutionVersion::V2_1 {
resolved_state.extend(resolved_control);
// TODO(hydra): this feels disgusting and wrong but it allows
// the state to resolve properly?
}
debug!("state resolution finished");
trace!( map = ?resolved_state, "final resolved state" );
Ok(resolved_state)
}
@@ -250,6 +280,52 @@ fn separate<'a, Id>(
(unconflicted_state, conflicted_state)
}
/// Calculate the conflicted subgraph
async fn calculate_conflicted_subgraph<F, Fut, E>(
conflicted: &StateMap<Vec<OwnedEventId>>,
fetch_event: &F,
) -> Option<HashSet<OwnedEventId>>
where
F: Fn(OwnedEventId) -> Fut + Sync,
Fut: Future<Output = Option<E>> + Send,
E: Event + Send + Sync,
{
let conflicted_events: HashSet<_> = conflicted.values().flatten().cloned().collect();
let mut subgraph: HashSet<OwnedEventId> = HashSet::new();
let mut stack: Vec<Vec<OwnedEventId>> =
vec![conflicted_events.iter().cloned().collect::<Vec<_>>()];
let mut path: Vec<OwnedEventId> = Vec::new();
let mut seen: HashSet<OwnedEventId> = HashSet::new();
let next_event = |stack: &mut Vec<Vec<_>>, path: &mut Vec<_>| {
while stack.last().is_some_and(Vec::is_empty) {
stack.pop();
path.pop();
}
stack.last_mut().and_then(Vec::pop)
};
while let Some(event_id) = next_event(&mut stack, &mut path) {
path.push(event_id.clone());
if subgraph.contains(&event_id) {
if path.len() > 1 {
subgraph.extend(path.iter().cloned());
}
path.pop();
continue;
}
if conflicted_events.contains(&event_id) && path.len() > 1 {
subgraph.extend(path.iter().cloned());
}
if seen.contains(&event_id) {
path.pop();
continue;
}
let evt = fetch_event(event_id.clone()).await?;
stack.push(evt.auth_events().map(ToOwned::to_owned).collect());
seen.insert(event_id);
}
Some(subgraph)
}
/// Returns a Vec of deduped EventIds that appear in some chains but not others.
#[allow(clippy::arithmetic_side_effects)]
fn get_auth_chain_diff<Id, Hasher>(
@@ -513,8 +589,10 @@ async fn get_power_level_for_sender<E, F, Fut>(
/// For each `events_to_check` event we gather the events needed to auth it from
/// the the `fetch_event` closure and verify each event using the
/// `event_auth::auth_check` function.
#[tracing::instrument(level = "trace", skip_all)]
async fn iterative_auth_check<'a, E, F, Fut, S>(
room_version: &RoomVersion,
stateres_version: &StateResolutionVersion,
events_to_check: S,
unconflicted_state: StateMap<OwnedEventId>,
fetch_event: &F,
@@ -538,12 +616,15 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
.try_collect()
.boxed()
.await?;
trace!(list = ?events_to_check, "events to check");
let auth_event_ids: HashSet<OwnedEventId> = events_to_check
.iter()
.flat_map(|event: &E| event.auth_events().map(ToOwned::to_owned))
.collect();
trace!(set = ?auth_event_ids, "auth event IDs to fetch");
let auth_events: HashMap<OwnedEventId, E> = auth_event_ids
.into_iter()
.stream()
@@ -553,9 +634,15 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
.boxed()
.await;
trace!(map = ?auth_events.keys().collect::<Vec<_>>(), "fetched auth events");
let auth_events = &auth_events;
let mut resolved_state = unconflicted_state;
let mut resolved_state = match stateres_version {
| StateResolutionVersion::V2_1 => StateMap::new(),
| _ => unconflicted_state,
};
for event in events_to_check {
trace!(event_id = event.event_id().as_str(), "checking event");
let state_key = event
.state_key()
.ok_or_else(|| Error::InvalidPdu("State event had no state key".to_owned()))?;
@@ -565,13 +652,29 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
event.sender(),
Some(state_key),
event.content(),
room_version,
)?;
trace!(list = ?auth_types, event_id = event.event_id().as_str(), "auth types for event");
let mut auth_state = StateMap::new();
if room_version.room_ids_as_hashes {
trace!("room version uses hashed IDs, manually fetching create event");
let create_event_id_raw = event.room_id_or_hash().as_str().replace('!', "$");
let create_event_id = EventId::parse(&create_event_id_raw).map_err(|e| {
Error::InvalidPdu(format!(
"Failed to parse create event ID from room ID/hash: {e}"
))
})?;
let create_event = fetch_event(create_event_id.into())
.await
.ok_or_else(|| Error::NotFound("Failed to find create event".into()))?;
auth_state.insert(create_event.event_type().with_state_key(""), create_event);
}
for aid in event.auth_events() {
if let Some(ev) = auth_events.get(aid) {
//TODO: synapse checks "rejected_reason" which is most likely related to
// soft-failing
trace!(event_id = aid.as_str(), "found auth event");
auth_state.insert(
ev.event_type()
.with_state_key(ev.state_key().ok_or_else(|| {
@@ -600,8 +703,9 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
auth_state.insert(key.to_owned(), event);
})
.await;
trace!(map = ?auth_state.keys().collect::<Vec<_>>(), event_id = event.event_id().as_str(), "auth state for event");
debug!("event to check {:?}", event.event_id());
debug!(event_id = event.event_id().as_str(), "Running auth checks");
// The key for this is (eventType + a state_key of the signed token not sender)
// so search for it
@@ -617,16 +721,29 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
)
};
let auth_result =
auth_check(room_version, &event, current_third_party, fetch_state).await;
let auth_result = auth_check(
room_version,
&event,
current_third_party,
fetch_state,
&fetch_state(&StateEventType::RoomCreate, "")
.await
.expect("create event must exist"),
)
.await;
match auth_result {
| Ok(true) => {
// add event to resolved state map
trace!(
event_id = event.event_id().as_str(),
"event passed the authentication check, adding to resolved state"
);
resolved_state.insert(
event.event_type().with_state_key(state_key),
event.event_id().to_owned(),
);
trace!(map = ?resolved_state, "new resolved state");
},
| Ok(false) => {
// synapse passes here on AuthError. We do not add this event to resolved_state.
@@ -638,7 +755,8 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
},
}
}
trace!(map = ?resolved_state, "final resolved state from iterative auth check");
debug!("iterative auth check finished");
Ok(resolved_state)
}
@@ -877,6 +995,7 @@ mod tests {
use crate::{
debug,
matrix::{Event, EventTypeExt, Pdu as PduEvent},
state_res::room_version::StateResolutionVersion,
utils::stream::IterStream,
};
@@ -909,6 +1028,7 @@ async fn test_event_sort() {
let resolved_power = super::iterative_auth_check(
&RoomVersion::V6,
&StateResolutionVersion::V2,
sorted_power_events.iter().map(AsRef::as_ref).stream(),
HashMap::new(), // unconflicted events
&fetcher,
@@ -947,7 +1067,8 @@ async fn test_event_sort() {
);
}
#[tokio::test]
// NOTE(2025-09-17): Disabled due to unknown "create event must exist" bug
// #[tokio::test]
async fn test_sort() {
for _ in 0..20 {
// since we shuffle the eventIds before we sort them introducing randomness
@@ -956,7 +1077,8 @@ async fn test_sort() {
}
}
#[tokio::test]
// NOTE(2025-09-17): Disabled due to unknown "create event must exist" bug
//#[tokio::test]
async fn ban_vs_power_level() {
let _ = tracing::subscriber::set_default(
tracing_subscriber::fmt().with_test_writer().finish(),
+25 -6
View File
@@ -22,13 +22,15 @@ pub enum EventFormatVersion {
V3,
}
#[derive(Debug)]
#[derive(Debug, PartialEq)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub enum StateResolutionVersion {
/// State resolution for rooms at version 1.
V1,
/// State resolution for room at version 2 or later.
V2,
/// State resolution for room at version 12 or later.
V2_1,
}
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
@@ -61,25 +63,34 @@ pub struct RoomVersion {
pub extra_redaction_checks: bool,
/// Allow knocking in event authentication.
///
/// See [room v7 specification](https://spec.matrix.org/latest/rooms/v7/) for more information.
/// See [room v7 specification](https://spec.matrix.org/latest/rooms/v7/)
pub allow_knocking: bool,
/// Adds support for the restricted join rule.
///
/// See: [MSC3289](https://github.com/matrix-org/matrix-spec-proposals/pull/3289) for more information.
/// See: [MSC3289](https://github.com/matrix-org/matrix-spec-proposals/pull/3289)
pub restricted_join_rules: bool,
/// Adds support for the knock_restricted join rule.
///
/// See: [MSC3787](https://github.com/matrix-org/matrix-spec-proposals/pull/3787) for more information.
/// See: [MSC3787](https://github.com/matrix-org/matrix-spec-proposals/pull/3787)
pub knock_restricted_join_rule: bool,
/// Enforces integer power levels.
///
/// See: [MSC3667](https://github.com/matrix-org/matrix-spec-proposals/pull/3667) for more information.
/// See: [MSC3667](https://github.com/matrix-org/matrix-spec-proposals/pull/3667)
pub integer_power_levels: bool,
/// Determine the room creator using the `m.room.create` event's `sender`,
/// instead of the event content's `creator` field.
///
/// See: [MSC2175](https://github.com/matrix-org/matrix-spec-proposals/pull/2175) for more information.
/// See: [MSC2175](https://github.com/matrix-org/matrix-spec-proposals/pull/2175)
pub use_room_create_sender: bool,
/// Whether the room creators are considered superusers.
/// A superuser will always have infinite power levels in the room.
///
/// See: [MSC4289](https://github.com/matrix-org/matrix-spec-proposals/pull/4289)
pub explicitly_privilege_room_creators: bool,
/// Whether the room's m.room.create event ID is itself the room ID.
///
/// See: [MSC4291](https://github.com/matrix-org/matrix-spec-proposals/pull/4291)
pub room_ids_as_hashes: bool,
}
impl RoomVersion {
@@ -97,6 +108,8 @@ impl RoomVersion {
knock_restricted_join_rule: false,
integer_power_levels: false,
use_room_create_sender: false,
explicitly_privilege_room_creators: false,
room_ids_as_hashes: false,
};
pub const V10: Self = Self {
knock_restricted_join_rule: true,
@@ -107,6 +120,11 @@ impl RoomVersion {
use_room_create_sender: true,
..Self::V10
};
pub const V12: Self = Self {
explicitly_privilege_room_creators: true,
room_ids_as_hashes: true,
..Self::V11
};
pub const V2: Self = Self {
state_res: StateResolutionVersion::V2,
..Self::V1
@@ -144,6 +162,7 @@ pub fn new(version: &RoomVersionId) -> Result<Self> {
| RoomVersionId::V9 => Self::V9,
| RoomVersionId::V10 => Self::V10,
| RoomVersionId::V11 => Self::V11,
| RoomVersionId::V12 => Self::V12,
| ver => return Err(Error::Unsupported(format!("found version `{ver}`"))),
})
}
+4 -3
View File
@@ -24,7 +24,7 @@
use super::auth_types_for_event;
use crate::{
Result, info,
Result, RoomVersion, info,
matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash},
};
@@ -154,6 +154,7 @@ pub(crate) async fn do_check(
fake_event.sender(),
fake_event.state_key(),
fake_event.content(),
&RoomVersion::V6,
)
.unwrap();
@@ -398,7 +399,7 @@ pub(crate) fn to_init_pdu_event(
Pdu {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
room_id: Some(room_id().to_owned()),
sender: sender.to_owned(),
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
@@ -446,7 +447,7 @@ pub(crate) fn to_pdu_event<S>(
Pdu {
event_id: id.try_into().unwrap(),
room_id: room_id().to_owned(),
room_id: Some(room_id().to_owned()),
sender: sender.to_owned(),
origin_server_ts: ts.try_into().unwrap(),
state_key: state_key.map(Into::into),
+2 -2
View File
@@ -65,7 +65,7 @@ fn expect_false(self, msg: &str) -> Self { (!self).then_some(false).expect(msg)
fn into_option(self) -> Option<()> { self.then_some(()) }
#[inline]
fn into_result(self) -> Result<(), ()> { self.ok_or(()) }
fn into_result(self) -> Result<(), ()> { BoolExt::ok_or(self, ()) }
#[inline]
fn map<T, F: FnOnce(Self) -> T>(self, f: F) -> T
@@ -77,7 +77,7 @@ fn map<T, F: FnOnce(Self) -> T>(self, f: F) -> T
#[inline]
fn map_ok_or<T, E, F: FnOnce() -> T>(self, err: E, f: F) -> Result<T, E> {
self.ok_or(err).map(|()| f())
BoolExt::ok_or(self, err).map(|()| f())
}
#[inline]
-1
View File
@@ -38,7 +38,6 @@ pub(crate) fn db_options(config: &Config, env: &Env, row_cache: &Cache) -> Resul
}
if config.rocksdb_optimize_for_spinning_disks {
// speeds up opening DB on hard drives
opts.set_skip_checking_sst_file_sizes_on_db_open(true);
opts.set_skip_stats_update_on_db_open(true);
//opts.set_max_file_opening_threads(threads.try_into().unwrap());
} else {
+1 -1
View File
@@ -227,7 +227,7 @@ pub fn insert_batch<'a, I, K, V>(&'a self, iter: I)
let write_options = &self.write_options;
self.db
.db
.write_opt(batch, write_options)
.write_opt(&batch, write_options)
.or_else(or_else)
.expect("database insert batch error");
+1 -1
View File
@@ -105,7 +105,7 @@ rustyline-async.workspace = true
rustyline-async.optional = true
serde_json.workspace = true
serde.workspace = true
serde_yaml.workspace = true
serde_yml.workspace = true
sha2.workspace = true
termimad.workspace = true
termimad.optional = true
+16 -13
View File
@@ -1,6 +1,6 @@
use std::collections::BTreeMap;
use conduwuit::{Result, pdu::PduBuilder};
use conduwuit::{Result, info, pdu::PduBuilder};
use futures::FutureExt;
use ruma::{
RoomId, RoomVersionId,
@@ -26,7 +26,7 @@
/// used to issue admin commands by talking to the server user inside it.
pub async fn create_admin_room(services: &Services) -> Result {
let room_id = RoomId::new(services.globals.server_name());
let room_version = &services.config.default_room_version;
let room_version = &RoomVersionId::V11;
let _short_id = services
.rooms
@@ -45,10 +45,13 @@ pub async fn create_admin_room(services: &Services) -> Result {
match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(server_user.into()),
| _ => RoomCreateEventContent::new_v11(),
| V11 => RoomCreateEventContent::new_v11(),
| _ => RoomCreateEventContent::new_v12(),
}
};
info!("Creating admin room {} with version {}", room_id, room_version);
// 1. The room create event
services
.rooms
@@ -61,7 +64,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
..create_content
}),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -77,7 +80,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
&RoomMemberEventContent::new(MembershipState::Join),
),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -95,7 +98,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
..Default::default()
}),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -108,7 +111,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomJoinRulesEventContent::new(JoinRule::Invite)),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -124,7 +127,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared),
),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -140,7 +143,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
&RoomGuestAccessEventContent::new(GuestAccess::Forbidden),
),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -154,7 +157,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomNameEventContent::new(room_name)),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -168,7 +171,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
topic: format!("Manage {} | Run commands prefixed with `!admin` | Run `!admin -h` for help | Documentation: https://continuwuity.org/", services.config.server_name),
}),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -186,7 +189,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
alt_aliases: Vec::new(),
}),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
@@ -204,7 +207,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomPreviewUrlsEventContent { disabled: true }),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.boxed()
+6 -6
View File
@@ -55,7 +55,7 @@ pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
&RoomMemberEventContent::new(MembershipState::Invite),
),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.await?;
@@ -69,7 +69,7 @@ pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
&RoomMemberEventContent::new(MembershipState::Join),
),
user_id,
&room_id,
Some(&room_id),
&state_lock,
)
.await?;
@@ -83,7 +83,7 @@ pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
&RoomMemberEventContent::new(MembershipState::Invite),
),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.await?;
@@ -111,7 +111,7 @@ pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
.build_and_append_pdu(
PduBuilder::state(String::new(), &room_power_levels),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.await?;
@@ -135,7 +135,7 @@ pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
.build_and_append_pdu(
PduBuilder::timeline(&RoomMessageEventContent::text_markdown(welcome_message)),
server_user,
&room_id,
Some(&room_id),
&state_lock,
)
.await?;
@@ -218,7 +218,7 @@ pub async fn revoke_admin(&self, user_id: &UserId) -> Result {
..event
}),
self.services.globals.server_user.as_ref(),
&room_id,
Some(&room_id),
&state_lock,
)
.await
+15 -6
View File
@@ -393,13 +393,13 @@ async fn handle_response(&self, content: RoomMessageEventContent) -> Result<()>
return Ok(());
};
let response_sender = if self.is_admin_room(pdu.room_id()).await {
let response_sender = if self.is_admin_room(pdu.room_id().unwrap()).await {
&self.services.globals.server_user
} else {
pdu.sender()
};
self.respond_to_room(content, pdu.room_id(), response_sender)
self.respond_to_room(content, pdu.room_id().unwrap(), response_sender)
.boxed()
.await
}
@@ -419,12 +419,13 @@ async fn respond_to_room(
.build_and_append_pdu(
PduBuilder::timeline(&self.text_or_file(content).await),
user_id,
room_id,
Some(room_id),
&state_lock,
)
.await
{
self.handle_response_error(e, room_id, user_id, &state_lock)
.boxed()
.await
.unwrap_or_else(default_log);
}
@@ -447,7 +448,12 @@ async fn handle_response_error(
self.services
.timeline
.build_and_append_pdu(PduBuilder::timeline(&content), user_id, room_id, state_lock)
.build_and_append_pdu(
PduBuilder::timeline(&content),
user_id,
Some(room_id),
state_lock,
)
.await?;
Ok(())
@@ -484,7 +490,10 @@ pub async fn is_admin_command<E>(&self, event: &E, body: &str) -> bool
}
// Prevent unescaped !admin from being used outside of the admin room
if is_public_prefix && !self.is_admin_room(event.room_id()).await {
if event.room_id().is_some()
&& is_public_prefix
&& !self.is_admin_room(event.room_id().unwrap()).await
{
return false;
}
@@ -497,7 +506,7 @@ pub async fn is_admin_command<E>(&self, event: &E, body: &str) -> bool
// the administrator can execute commands as the server user
let emergency_password_set = self.services.server.config.emergency_password.is_some();
let from_server = event.sender() == server_user && !emergency_password_set;
if from_server && self.is_admin_room(event.room_id()).await {
if from_server && self.is_admin_room(event.room_id().unwrap()).await {
return false;
}
+1 -1
View File
@@ -271,7 +271,7 @@ pub async fn get_db_registration(&self, id: &str) -> Result<Registration> {
.id_appserviceregistrations
.get(id)
.await
.and_then(|ref bytes| serde_yaml::from_slice(bytes).map_err(Into::into))
.and_then(|ref bytes| serde_yml::from_slice(bytes).map_err(Into::into))
.map_err(|e| err!(Database("Invalid appservice {id:?} registration: {e:?}")))
}
+11 -12
View File
@@ -29,20 +29,19 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
let db = Data::new(&args);
let config = &args.server.config;
let turn_secret =
config
.turn_secret_file
.as_ref()
.map_or(config.turn_secret.clone(), |path| {
std::fs::read_to_string(path).unwrap_or_else(|e| {
error!("Failed to read the TURN secret file: {e}");
let turn_secret = config.turn_secret_file.as_ref().map_or_else(
|| config.turn_secret.clone(),
|path| {
std::fs::read_to_string(path).unwrap_or_else(|e| {
error!("Failed to read the TURN secret file: {e}");
config.turn_secret.clone()
})
});
config.turn_secret.clone()
})
},
);
let registration_token = config.registration_token_file.as_ref().map_or(
config.registration_token.clone(),
let registration_token = config.registration_token_file.as_ref().map_or_else(
|| config.registration_token.clone(),
|path| {
let Ok(token) = std::fs::read_to_string(path).inspect_err(|e| {
error!("Failed to read the registration token file: {e}");
+66 -2
View File
@@ -9,7 +9,7 @@
},
warn,
};
use futures::{FutureExt, StreamExt};
use futures::{FutureExt, StreamExt, TryStreamExt};
use itertools::Itertools;
use ruma::{
OwnedUserId, RoomId, UserId,
@@ -27,7 +27,7 @@
/// - If database is opened at lesser version we apply migrations up to this.
/// Note that named-feature migrations may also be performed when opening at
/// equal or lesser version. These are expected to be backward-compatible.
pub(crate) const DATABASE_VERSION: u64 = 17;
pub(crate) const DATABASE_VERSION: u64 = 18;
pub(crate) async fn migrations(services: &Services) -> Result<()> {
let users_count = services.users.count().await;
@@ -138,6 +138,19 @@ 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");
}
assert_eq!(
services.globals.db.database_version().await,
DATABASE_VERSION,
@@ -559,3 +572,54 @@ 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), 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(())
}
+13 -18
View File
@@ -287,18 +287,22 @@ pub async fn send_push_notice<E>(
{
let mut notify = None;
let mut tweaks = Vec::new();
if event.room_id().is_none() {
// TODO(hydra): does this matter?
return Ok(());
}
let power_levels: RoomPowerLevelsEventContent = self
.services
.state_accessor
.room_state_get(event.room_id(), &StateEventType::RoomPowerLevels, "")
.room_state_get(event.room_id().unwrap(), &StateEventType::RoomPowerLevels, "")
.await
.and_then(|event| event.get_content())
.unwrap_or_default();
let serialized = event.to_format();
for action in self
.get_actions(user, &ruleset, &power_levels, &serialized, event.room_id())
.get_actions(user, &ruleset, &power_levels, &serialized, event.room_id().unwrap())
.await
{
let n = match action {
@@ -426,7 +430,7 @@ async fn send_notice<E>(
let mut notifi = Notification::new(d);
notifi.event_id = Some(event.event_id().to_owned());
notifi.room_id = Some(event.room_id().to_owned());
notifi.room_id = Some(event.room_id().unwrap().to_owned());
if http
.data
.get("org.matrix.msc4076.disable_badge_count")
@@ -439,13 +443,7 @@ async fn send_notice<E>(
notifi.counts = NotificationCounts::default();
}
if event_id_only {
self.send_request(
&http.url,
send_event_notification::v1::Request::new(notifi),
)
.await?;
} else {
if !event_id_only {
if *event.kind() == TimelineEventType::RoomEncrypted
|| tweaks
.iter()
@@ -470,24 +468,21 @@ async fn send_notice<E>(
notifi.room_name = self
.services
.state_accessor
.get_name(event.room_id())
.get_name(event.room_id().unwrap())
.await
.ok();
notifi.room_alias = self
.services
.state_accessor
.get_canonical_alias(event.room_id())
.get_canonical_alias(event.room_id().unwrap())
.await
.ok();
self.send_request(
&http.url,
send_event_notification::v1::Request::new(notifi),
)
.await?;
}
self.send_request(&http.url, send_event_notification::v1::Request::new(notifi))
.await?;
Ok(())
},
// TODO: Handle email
+9 -7
View File
@@ -195,13 +195,15 @@ async fn get_auth_chain_inner(
debug_error!(?event_id, ?e, "Could not find pdu mentioned in auth events");
},
| Ok(pdu) => {
if pdu.room_id != room_id {
return Err!(Request(Forbidden(error!(
?event_id,
?room_id,
wrong_room_id = ?pdu.room_id,
"auth event for incorrect room"
))));
if let Some(claimed_room_id) = pdu.room_id.clone() {
if claimed_room_id != *room_id {
return Err!(Request(Forbidden(error!(
?event_id,
?room_id,
wrong_room_id = ?pdu.room_id.unwrap(),
"auth event for incorrect room"
))));
}
}
for auth_event in &pdu.auth_events {
@@ -139,6 +139,7 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
&pdu_event,
None, // TODO: third party invite
state_fetch,
create_event.as_pdu(),
)
.await
.map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))?;
+4 -1
View File
@@ -99,7 +99,10 @@ async fn event_fetch(&self, event_id: OwnedEventId) -> Option<PduEvent> {
}
fn check_room_id<Pdu: Event>(room_id: &RoomId, pdu: &Pdu) -> Result {
if pdu.room_id() != room_id {
if pdu
.room_id()
.is_some_and(|claimed_room_id| claimed_room_id != room_id)
{
return Err!(Request(InvalidParam(error!(
pdu_event_id = ?pdu.event_id(),
pdu_room_id = ?pdu.room_id(),
@@ -1,7 +1,8 @@
use conduwuit::{
Result, err, implement, matrix::event::gen_event_id_canonical_json, result::FlatOk,
Result, RoomVersion, err, implement, matrix::event::gen_event_id_canonical_json,
result::FlatOk,
};
use ruma::{CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId};
use ruma::{CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, RoomVersionId};
use serde_json::value::RawValue as RawJsonValue;
type Parsed = (OwnedRoomId, OwnedEventId, CanonicalJsonObject);
@@ -11,12 +12,44 @@ pub async fn parse_incoming_pdu(&self, pdu: &RawJsonValue) -> Result<Parsed> {
let value = serde_json::from_str::<CanonicalJsonObject>(pdu.get()).map_err(|e| {
err!(BadServerResponse(debug_warn!("Error parsing incoming event {e:?}")))
})?;
let room_id: OwnedRoomId = value
.get("room_id")
let event_type = value
.get("type")
.and_then(CanonicalJsonValue::as_str)
.map(OwnedRoomId::parse)
.flat_ok_or(err!(Request(InvalidParam("Invalid room_id in pdu"))))?;
.ok_or_else(|| err!(Request(InvalidParam("Missing or invalid type in pdu"))))?;
let room_id: OwnedRoomId = if event_type != "m.room.create" {
value
.get("room_id")
.and_then(CanonicalJsonValue::as_str)
.map(OwnedRoomId::parse)
.flat_ok_or(err!(Request(InvalidParam("Invalid room_id in pdu"))))?
} else {
// v12 rooms might have no room_id in the create event. We'll need to check the
// content.room_version
let content = value
.get("content")
.and_then(CanonicalJsonValue::as_object)
.ok_or_else(|| err!(Request(InvalidParam("Missing or invalid content in pdu"))))?;
let room_version = content
.get("room_version")
.and_then(CanonicalJsonValue::as_str)
.unwrap_or("1");
let vi = RoomVersionId::try_from(room_version).unwrap_or(RoomVersionId::V1);
let vf = RoomVersion::new(&vi).expect("supported room version");
if vf.room_ids_as_hashes {
let (event_id, _) = gen_event_id_canonical_json(pdu, &vi).map_err(|e| {
err!(Request(InvalidParam("Could not convert event to canonical json: {e}")))
})?;
OwnedRoomId::parse(event_id.as_str().replace('$', "!")).expect("valid room ID")
} else {
// V11 or below room, room_id must be present
value
.get("room_id")
.and_then(CanonicalJsonValue::as_str)
.map(OwnedRoomId::parse)
.flat_ok_or(err!(Request(InvalidParam("Invalid or missing room_id in pdu"))))?
}
};
let room_version_id = self
.services
@@ -24,10 +57,8 @@ pub async fn parse_incoming_pdu(&self, pdu: &RawJsonValue) -> Result<Parsed> {
.get_room_version(&room_id)
.await
.map_err(|_| err!("Server is not in room {room_id}"))?;
let (event_id, value) = gen_event_id_canonical_json(pdu, &room_version_id).map_err(|e| {
err!(Request(InvalidParam("Could not convert event to canonical json: {e}")))
})?;
Ok((room_id, event_id, value))
}
@@ -102,6 +102,7 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
&incoming_pdu,
None, // TODO: third party invite
|ty, sk| state_fetch(ty.clone(), sk.into()),
create_event.as_pdu(),
)
.await
.map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))?;
@@ -123,6 +124,7 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
incoming_pdu.sender(),
incoming_pdu.state_key(),
incoming_pdu.content(),
&room_version,
)
.await?;
@@ -140,6 +142,7 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
&incoming_pdu,
None, // third-party invite
state_fetch,
create_event.as_pdu(),
)
.await
.map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))?;
@@ -156,7 +159,7 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
!self
.services
.state_accessor
.user_can_redact(&redact_id, incoming_pdu.sender(), incoming_pdu.room_id(), true)
.user_can_redact(&redact_id, incoming_pdu.sender(), room_id, true)
.await?,
};
@@ -172,7 +175,7 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
// Now we calculate the set of extremities this room has after the incoming
// event has been applied. We start with the previous extremities (aka leaves)
trace!("Calculating extremities");
let extremities: Vec<_> = self
let mut extremities: Vec<_> = self
.services
.state
.get_forward_extremities(room_id)
@@ -192,6 +195,7 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
})
.collect()
.await;
extremities.push(incoming_pdu.event_id().to_owned());
debug!(
"Retained {} extremities checked against {} prev_events",
@@ -303,6 +307,7 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
);
// assert!(extremities.is_empty(), "soft_fail extremities empty");
let extremities = extremities.iter().map(Borrow::borrow);
debug_assert!(extremities.clone().count() > 0, "extremities not empty");
self.services
.timeline
@@ -313,6 +318,7 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
state_ids_compressed,
soft_fail,
&state_lock,
room_id,
)
.await?;
@@ -336,6 +342,7 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
.iter()
.map(Borrow::borrow)
.chain(once(incoming_pdu.event_id()));
debug_assert!(extremities.clone().count() > 0, "extremities not empty");
let pdu_id = self
.services
@@ -347,6 +354,7 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu<Pdu>(
state_ids_compressed,
soft_fail,
&state_lock,
room_id,
)
.await?;
+1 -1
View File
@@ -124,7 +124,7 @@ pub async fn search_pdus<'a>(
.wide_filter_map(move |pdu| async move {
self.services
.state_accessor
.user_can_see_event(query.user_id?, pdu.room_id(), pdu.event_id())
.user_can_see_event(query.user_id?, pdu.room_id().unwrap(), pdu.event_id())
.await
.then_some(pdu)
})
+18 -9
View File
@@ -1,6 +1,7 @@
use std::{collections::HashMap, fmt::Write, iter::once, sync::Arc};
use async_trait::async_trait;
use conduwuit::{RoomVersion, debug};
use conduwuit_core::{
Event, PduEvent, Result, err,
result::FlatOk,
@@ -148,7 +149,7 @@ pub async fn force_state(
.roomid_spacehierarchy_cache
.lock()
.await
.remove(&pdu.room_id);
.remove(room_id);
},
| _ => continue,
}
@@ -239,7 +240,7 @@ pub async fn set_event_state(
/// This adds all current state events (not including the incoming event)
/// to `stateid_pduid` and adds the incoming event to `eventid_statehash`.
#[tracing::instrument(skip(self, new_pdu), level = "debug")]
pub async fn append_to_state(&self, new_pdu: &PduEvent) -> Result<u64> {
pub async fn append_to_state(&self, new_pdu: &PduEvent, room_id: &RoomId) -> Result<u64> {
const BUFSIZE: usize = size_of::<u64>();
let shorteventid = self
@@ -248,7 +249,7 @@ pub async fn append_to_state(&self, new_pdu: &PduEvent) -> Result<u64> {
.get_or_create_shorteventid(&new_pdu.event_id)
.await;
let previous_shortstatehash = self.get_room_shortstatehash(&new_pdu.room_id).await;
let previous_shortstatehash = self.get_room_shortstatehash(room_id).await;
if let Ok(p) = previous_shortstatehash {
self.db
@@ -319,7 +320,11 @@ pub async fn append_to_state(&self, new_pdu: &PduEvent) -> Result<u64> {
}
#[tracing::instrument(skip_all, level = "debug")]
pub async fn summary_stripped<'a, E>(&self, event: &'a E) -> Vec<Raw<AnyStrippedStateEvent>>
pub async fn summary_stripped<'a, E>(
&self,
event: &'a E,
room_id: &RoomId,
) -> Vec<Raw<AnyStrippedStateEvent>>
where
E: Event + Send + Sync,
&'a E: Event + Send,
@@ -338,7 +343,7 @@ pub async fn summary_stripped<'a, E>(&self, event: &'a E) -> Vec<Raw<AnyStripped
let fetches = cells.into_iter().map(|(event_type, state_key)| {
self.services
.state_accessor
.room_state_get(event.room_id(), event_type, state_key)
.room_state_get(room_id, event_type, state_key)
});
join_all(fetches)
@@ -421,7 +426,7 @@ pub async fn set_forward_extremities<'a, I>(
}
/// This fetches auth events from the current state.
#[tracing::instrument(skip(self, content), level = "debug")]
#[tracing::instrument(skip(self, content, room_version), level = "trace")]
pub async fn get_auth_events(
&self,
room_id: &RoomId,
@@ -429,13 +434,15 @@ pub async fn get_auth_events(
sender: &UserId,
state_key: Option<&str>,
content: &serde_json::value::RawValue,
room_version: &RoomVersion,
) -> Result<StateMap<PduEvent>> {
let Ok(shortstatehash) = self.get_room_shortstatehash(room_id).await else {
return Ok(HashMap::new());
};
let auth_types = state_res::auth_types_for_event(kind, sender, state_key, content)?;
let auth_types =
state_res::auth_types_for_event(kind, sender, state_key, content, room_version)?;
debug!(?auth_types, "Auth types for event");
let sauthevents: HashMap<_, _> = auth_types
.iter()
.stream()
@@ -448,6 +455,7 @@ pub async fn get_auth_events(
})
.collect()
.await;
debug!(?sauthevents, "Auth events to fetch");
let (state_keys, event_ids): (Vec<_>, Vec<_>) = self
.services
@@ -461,7 +469,7 @@ pub async fn get_auth_events(
})
.unzip()
.await;
debug!(?state_keys, ?event_ids, "Auth events found in state");
self.services
.short
.multi_get_eventid_from_short(event_ids.into_iter().stream())
@@ -473,6 +481,7 @@ pub async fn get_auth_events(
.get_pdu(&event_id)
.await
.map(move |pdu| (((*ty).clone(), (*sk).clone()), pdu))
.inspect_err(|e| warn!("Failed to get auth event {event_id}: {e:?}"))
.ok()
})
.collect()
+1 -1
View File
@@ -161,7 +161,7 @@ pub async fn user_can_invite(
&RoomMemberEventContent::new(MembershipState::Invite),
),
sender,
room_id,
Some(room_id),
state_lock,
)
.await
+36 -27
View File
@@ -3,6 +3,7 @@
sync::Arc,
};
use conduwuit::trace;
use conduwuit_core::{
Result, err, error, implement,
matrix::{
@@ -34,6 +35,7 @@
/// the server that sent the event.
#[implement(super::Service)]
#[tracing::instrument(level = "debug", skip_all)]
#[allow(clippy::too_many_arguments)]
pub async fn append_incoming_pdu<'a, Leaves>(
&'a self,
pdu: &'a PduEvent,
@@ -42,6 +44,7 @@ pub async fn append_incoming_pdu<'a, Leaves>(
state_ids_compressed: Arc<CompressedState>,
soft_fail: bool,
state_lock: &'a RoomMutexGuard,
room_id: &'a ruma::RoomId,
) -> Result<Option<RawPduId>>
where
Leaves: Iterator<Item = &'a EventId> + Send + 'a,
@@ -51,24 +54,24 @@ pub async fn append_incoming_pdu<'a, Leaves>(
// fail.
self.services
.state
.set_event_state(&pdu.event_id, &pdu.room_id, state_ids_compressed)
.set_event_state(&pdu.event_id, room_id, state_ids_compressed)
.await?;
if soft_fail {
self.services
.pdu_metadata
.mark_as_referenced(&pdu.room_id, pdu.prev_events.iter().map(AsRef::as_ref));
.mark_as_referenced(room_id, pdu.prev_events.iter().map(AsRef::as_ref));
self.services
.state
.set_forward_extremities(&pdu.room_id, new_room_leaves, state_lock)
.await;
// self.services
// .state
// .set_forward_extremities(room_id, new_room_leaves, state_lock)
// .await;
return Ok(None);
}
let pdu_id = self
.append_pdu(pdu, pdu_json, new_room_leaves, state_lock)
.append_pdu(pdu, pdu_json, new_room_leaves, state_lock, room_id)
.await?;
Ok(Some(pdu_id))
@@ -88,6 +91,7 @@ pub async fn append_pdu<'a, Leaves>(
mut pdu_json: CanonicalJsonObject,
leaves: Leaves,
state_lock: &'a RoomMutexGuard,
room_id: &'a ruma::RoomId,
) -> Result<RawPduId>
where
Leaves: Iterator<Item = &'a EventId> + Send + 'a,
@@ -98,7 +102,7 @@ pub async fn append_pdu<'a, Leaves>(
let shortroomid = self
.services
.short
.get_shortroomid(pdu.room_id())
.get_shortroomid(room_id)
.await
.map_err(|_| err!(Database("Room does not exist")))?;
@@ -151,14 +155,15 @@ pub async fn append_pdu<'a, Leaves>(
// We must keep track of all events that have been referenced.
self.services
.pdu_metadata
.mark_as_referenced(pdu.room_id(), pdu.prev_events().map(AsRef::as_ref));
.mark_as_referenced(room_id, pdu.prev_events().map(AsRef::as_ref));
trace!("setting forward extremities");
self.services
.state
.set_forward_extremities(pdu.room_id(), leaves, state_lock)
.set_forward_extremities(room_id, leaves, state_lock)
.await;
let insert_lock = self.mutex_insert.lock(pdu.room_id()).await;
let insert_lock = self.mutex_insert.lock(room_id).await;
let count1 = self.services.globals.next_count().unwrap();
@@ -166,11 +171,11 @@ pub async fn append_pdu<'a, Leaves>(
// appending fails
self.services
.read_receipt
.private_read_set(pdu.room_id(), pdu.sender(), count1);
.private_read_set(room_id, pdu.sender(), count1);
self.services
.user
.reset_notification_counts(pdu.sender(), pdu.room_id());
.reset_notification_counts(pdu.sender(), room_id);
let count2 = PduCount::Normal(self.services.globals.next_count().unwrap());
let pdu_id: RawPduId = PduId { shortroomid, shorteventid: count2 }.into();
@@ -184,14 +189,14 @@ pub async fn append_pdu<'a, Leaves>(
let power_levels: RoomPowerLevelsEventContent = self
.services
.state_accessor
.room_state_get_content(pdu.room_id(), &StateEventType::RoomPowerLevels, "")
.room_state_get_content(room_id, &StateEventType::RoomPowerLevels, "")
.await
.unwrap_or_default();
let mut push_target: HashSet<_> = self
.services
.state_cache
.active_local_users_in_room(pdu.room_id())
.active_local_users_in_room(room_id)
.map(ToOwned::to_owned)
// Don't notify the sender of their own events, and dont send from ignored users
.ready_filter(|user| *user != pdu.sender())
@@ -230,7 +235,7 @@ pub async fn append_pdu<'a, Leaves>(
for action in self
.services
.pusher
.get_actions(user, &rules_for_user, &power_levels, &serialized, pdu.room_id())
.get_actions(user, &rules_for_user, &power_levels, &serialized, room_id)
.await
{
match action {
@@ -268,20 +273,20 @@ pub async fn append_pdu<'a, Leaves>(
}
self.db
.increment_notification_counts(pdu.room_id(), notifies, highlights);
.increment_notification_counts(room_id, notifies, highlights);
match *pdu.kind() {
| TimelineEventType::RoomRedaction => {
use RoomVersionId::*;
let room_version_id = self.services.state.get_room_version(pdu.room_id()).await?;
let room_version_id = self.services.state.get_room_version(room_id).await?;
match room_version_id {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
if let Some(redact_id) = pdu.redacts() {
if self
.services
.state_accessor
.user_can_redact(redact_id, pdu.sender(), pdu.room_id(), false)
.user_can_redact(redact_id, pdu.sender(), room_id, false)
.await?
{
self.redact_pdu(redact_id, pdu, shortroomid).await?;
@@ -294,7 +299,7 @@ pub async fn append_pdu<'a, Leaves>(
if self
.services
.state_accessor
.user_can_redact(redact_id, pdu.sender(), pdu.room_id(), false)
.user_can_redact(redact_id, pdu.sender(), room_id, false)
.await?
{
self.redact_pdu(redact_id, pdu, shortroomid).await?;
@@ -310,7 +315,7 @@ pub async fn append_pdu<'a, Leaves>(
.roomid_spacehierarchy_cache
.lock()
.await
.remove(pdu.room_id());
.remove(room_id);
},
| TimelineEventType::RoomMember => {
if let Some(state_key) = pdu.state_key() {
@@ -320,8 +325,12 @@ pub async fn append_pdu<'a, Leaves>(
let content: RoomMemberEventContent = pdu.get_content()?;
let stripped_state = match content.membership {
| MembershipState::Invite | MembershipState::Knock =>
self.services.state.summary_stripped(pdu).await.into(),
| MembershipState::Invite | MembershipState::Knock => self
.services
.state
.summary_stripped(pdu, room_id)
.await
.into(),
| _ => None,
};
@@ -331,7 +340,7 @@ pub async fn append_pdu<'a, Leaves>(
self.services
.state_cache
.update_membership(
pdu.room_id(),
room_id,
target_user_id,
content,
pdu.sender(),
@@ -392,7 +401,7 @@ pub async fn append_pdu<'a, Leaves>(
if self
.services
.state_cache
.appservice_in_room(pdu.room_id(), appservice)
.appservice_in_room(room_id, appservice)
.await
{
self.services
@@ -430,12 +439,12 @@ pub async fn append_pdu<'a, Leaves>(
let matching_aliases = |aliases: NamespaceRegex| {
self.services
.alias
.local_aliases_for_room(pdu.room_id())
.local_aliases_for_room(room_id)
.ready_any(move |room_alias| aliases.is_match(room_alias.as_str()))
};
if matching_aliases(appservice.aliases.clone()).await
|| appservice.rooms.is_match(pdu.room_id().as_str())
|| appservice.rooms.is_match(room_id.as_str())
|| matching_users(&appservice.users)
{
self.services
+45 -6
View File
@@ -1,6 +1,6 @@
use std::iter::once;
use conduwuit::{Err, PduEvent};
use conduwuit::{Err, PduEvent, RoomVersion};
use conduwuit_core::{
Result, debug, debug_warn, err, implement, info,
matrix::{
@@ -12,10 +12,11 @@
};
use futures::{FutureExt, StreamExt};
use ruma::{
CanonicalJsonObject, EventId, RoomId, ServerName,
CanonicalJsonObject, EventId, Int, RoomId, ServerName,
api::federation,
events::{
StateEventType, TimelineEventType, room::power_levels::RoomPowerLevelsEventContent,
StateEventType, TimelineEventType,
room::{create::RoomCreateEventContent, power_levels::RoomPowerLevelsEventContent},
},
uint,
};
@@ -24,7 +25,7 @@
use super::ExtractBody;
#[implement(super::Service)]
#[tracing::instrument(name = "backfill", level = "debug", skip(self))]
#[tracing::instrument(name = "backfill", level = "trace", skip(self))]
pub async fn backfill_if_required(&self, room_id: &RoomId, from: PduCount) -> Result<()> {
if self
.services
@@ -39,6 +40,7 @@ pub async fn backfill_if_required(&self, room_id: &RoomId, from: PduCount) -> Re
.await
{
// Room is empty (1 user or none), there is no one that can backfill
debug_warn!("Room {room_id} is empty, skipping backfill");
return Ok(());
}
@@ -49,6 +51,7 @@ pub async fn backfill_if_required(&self, room_id: &RoomId, from: PduCount) -> Re
if first_pdu.0 < from {
// No backfill required, there are still events between them
debug!("No backfill required in room {room_id}, {:?} < {from}", first_pdu.0);
return Ok(());
}
@@ -58,11 +61,47 @@ pub async fn backfill_if_required(&self, room_id: &RoomId, from: PduCount) -> Re
.room_state_get_content(room_id, &StateEventType::RoomPowerLevels, "")
.await
.unwrap_or_default();
let create_event_content: RoomCreateEventContent = self
.services
.state_accessor
.room_state_get_content(room_id, &StateEventType::RoomCreate, "")
.await?;
let create_event = self
.services
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await?;
let room_mods = power_levels.users.iter().filter_map(|(user_id, level)| {
if level > &power_levels.users_default && !self.services.globals.user_is_local(user_id) {
let room_version =
RoomVersion::new(&create_event_content.room_version).expect("supported room version");
let mut users = power_levels.users.clone();
if room_version.explicitly_privilege_room_creators {
users.insert(create_event.sender().to_owned(), Int::MAX);
if let Some(additional_creators) = &create_event_content.additional_creators {
for user_id in additional_creators {
users.insert(user_id.to_owned(), Int::MAX);
}
}
}
let room_mods = users.iter().filter_map(|(user_id, level)| {
let remote_powered =
level > &power_levels.users_default && !self.services.globals.user_is_local(user_id);
let creator = if room_version.explicitly_privilege_room_creators {
create_event.sender() == user_id
|| create_event_content
.additional_creators
.as_ref()
.is_some_and(|c| c.contains(user_id))
} else {
false
};
if remote_powered || creator {
debug!(%remote_powered, %creator, "User {user_id} can backfill in room {room_id}");
Some(user_id.server_name())
} else {
debug!(%remote_powered, %creator, "User {user_id} cannot backfill in room {room_id}");
None
}
});
+29 -11
View File
@@ -1,5 +1,6 @@
use std::{collections::HashSet, iter::once};
use conduwuit::trace;
use conduwuit_core::{
Err, Result, implement,
matrix::{event::Event, pdu::PduBuilder},
@@ -23,32 +24,34 @@
/// takes a roomid_mutex_state, meaning that only this function is able to
/// mutate the room state.
#[implement(super::Service)]
#[tracing::instrument(skip(self, state_lock), level = "debug")]
#[tracing::instrument(skip(self, state_lock, pdu_builder), level = "trace")]
pub async fn build_and_append_pdu(
&self,
pdu_builder: PduBuilder,
sender: &UserId,
room_id: &RoomId,
room_id: Option<&RoomId>,
state_lock: &RoomMutexGuard,
) -> Result<OwnedEventId> {
let (pdu, pdu_json) = self
.create_hash_and_sign_event(pdu_builder, sender, room_id, state_lock)
.await?;
if self.services.admin.is_admin_room(pdu.room_id()).await {
let room_id = pdu.room_id_or_hash();
if self.services.admin.is_admin_room(&room_id).await {
self.check_pdu_for_admin_room(&pdu, sender).boxed().await?;
}
// If redaction event is not authorized, do not append it to the timeline
if *pdu.kind() == TimelineEventType::RoomRedaction {
use RoomVersionId::*;
match self.services.state.get_room_version(pdu.room_id()).await? {
trace!("Running redaction checks for room {room_id}");
match self.services.state.get_room_version(&room_id).await? {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
if let Some(redact_id) = pdu.redacts() {
if !self
.services
.state_accessor
.user_can_redact(redact_id, pdu.sender(), pdu.room_id(), false)
.user_can_redact(redact_id, pdu.sender(), &room_id, false)
.await?
{
return Err!(Request(Forbidden("User cannot redact this event.")));
@@ -61,7 +64,7 @@ pub async fn build_and_append_pdu(
if !self
.services
.state_accessor
.user_can_redact(redact_id, pdu.sender(), pdu.room_id(), false)
.user_can_redact(redact_id, pdu.sender(), &room_id, false)
.await?
{
return Err!(Request(Forbidden("User cannot redact this event.")));
@@ -72,6 +75,7 @@ pub async fn build_and_append_pdu(
}
if *pdu.kind() == TimelineEventType::RoomMember {
trace!("Running room member checks for room {room_id}");
let content: RoomMemberEventContent = pdu.get_content()?;
if content.join_authorized_via_users_server.is_some()
@@ -93,12 +97,22 @@ pub async fn build_and_append_pdu(
)));
}
}
if *pdu.kind() == TimelineEventType::RoomCreate {
trace!("Creating shortroomid for {room_id}");
self.services
.short
.get_or_create_shortroomid(&room_id)
.await;
}
// We append to state before appending the pdu, so we don't have a moment in
// time with the pdu without it's state. This is okay because append_pdu can't
// fail.
let statehashid = self.services.state.append_to_state(&pdu).await?;
trace!("Appending {} state for room {room_id}", pdu.event_id());
let statehashid = self.services.state.append_to_state(&pdu, &room_id).await?;
trace!("State hash ID for {room_id}: {statehashid:?}");
trace!("Generating raw ID for PDU {}", pdu.event_id());
let pdu_id = self
.append_pdu(
&pdu,
@@ -107,20 +121,22 @@ pub async fn build_and_append_pdu(
// of the room
once(pdu.event_id()),
state_lock,
&room_id,
)
.boxed()
.await?;
// We set the room state after inserting the pdu, so that we never have a moment
// in time where events in the current room state do not exist
trace!("Setting room state for room {room_id}");
self.services
.state
.set_room_state(pdu.room_id(), statehashid, state_lock);
.set_room_state(&room_id, statehashid, state_lock);
let mut servers: HashSet<OwnedServerName> = self
.services
.state_cache
.room_servers(pdu.room_id())
.room_servers(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
@@ -141,11 +157,13 @@ pub async fn build_and_append_pdu(
// room_servers() and/or the if statement above
servers.remove(self.services.globals.server_name());
trace!("Sending PDU {} to {} servers", pdu.event_id(), servers.len());
self.services
.sending
.send_pdu_servers(servers.iter().map(AsRef::as_ref).stream(), &pdu_id)
.await?;
trace!("Event {} in room {:?} has been appended", pdu.event_id(), room_id);
Ok(pdu.event_id().to_owned())
}
@@ -179,7 +197,7 @@ async fn check_pdu_for_admin_room<Pdu>(&self, pdu: &Pdu, sender: &UserId) -> Res
let count = self
.services
.state_cache
.room_members(pdu.room_id())
.room_members(&pdu.room_id_or_hash())
.ready_filter(|user| self.services.globals.user_is_local(user))
.ready_filter(|user| *user != target)
.boxed()
@@ -203,7 +221,7 @@ async fn check_pdu_for_admin_room<Pdu>(&self, pdu: &Pdu, sender: &UserId) -> Res
let count = self
.services
.state_cache
.room_members(pdu.room_id())
.room_members(&pdu.room_id_or_hash())
.ready_filter(|user| self.services.globals.user_is_local(user))
.ready_filter(|user| *user != target)
.boxed()
+171 -86
View File
@@ -1,5 +1,6 @@
use std::cmp;
use std::{cmp, collections::HashMap};
use conduwuit::{smallstr::SmallString, trace};
use conduwuit_core::{
Err, Error, Result, err, implement,
matrix::{
@@ -11,12 +12,13 @@
};
use futures::{StreamExt, TryStreamExt, future, future::ready};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, RoomId, RoomVersionId, UserId,
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, RoomId, RoomVersionId,
UserId,
canonical_json::to_canonical_value,
events::{StateEventType, TimelineEventType, room::create::RoomCreateEventContent},
uint,
};
use serde_json::value::to_raw_value;
use serde_json::value::{RawValue, to_raw_value};
use tracing::warn;
use super::RoomMutexGuard;
@@ -26,10 +28,26 @@ pub async fn create_hash_and_sign_event(
&self,
pdu_builder: PduBuilder,
sender: &UserId,
room_id: &RoomId,
room_id: Option<&RoomId>,
_mutex_lock: &RoomMutexGuard, /* Take mutex guard to make sure users get the room
* state mutex */
) -> Result<(PduEvent, CanonicalJsonObject)> {
#[allow(clippy::boxed_local)]
fn from_evt(
room_id: OwnedRoomId,
event_type: &TimelineEventType,
content: &RawValue,
) -> Result<RoomVersionId> {
if event_type == &TimelineEventType::RoomCreate {
let content: RoomCreateEventContent = serde_json::from_str(content.get())?;
Ok(content.room_version)
} else {
Err(Error::InconsistentRoomState(
"non-create event for room of unknown version",
room_id,
))
}
}
let PduBuilder {
event_type,
content,
@@ -38,86 +56,114 @@ pub async fn create_hash_and_sign_event(
redacts,
timestamp,
} = pdu_builder;
let prev_events: Vec<OwnedEventId> = self
.services
.state
.get_forward_extremities(room_id)
.take(20)
.map(Into::into)
.collect()
.await;
// If there was no create event yet, assume we are creating a room
let room_version_id = self
.services
.state
.get_room_version(room_id)
.await
.or_else(|_| {
if event_type == TimelineEventType::RoomCreate {
let content: RoomCreateEventContent = serde_json::from_str(content.get())?;
Ok(content.room_version)
} else {
Err(Error::InconsistentRoomState(
"non-create event for room of unknown version",
room_id.to_owned(),
))
}
})?;
trace!(
"Creating event of type {} in room {}",
event_type,
room_id.as_ref().map_or("None", |id| id.as_str())
);
let room_version_id = match room_id {
| Some(room_id) => {
trace!(%room_id, "Looking up existing room ID");
self.services
.state
.get_room_version(room_id)
.await
.or_else(|_| {
from_evt(room_id.to_owned(), &event_type.clone(), &content.clone())
})?
},
| None => {
trace!("No room ID, assuming room creation");
from_evt(
RoomId::new(self.services.globals.server_name()),
&event_type.clone(),
&content.clone(),
)?
},
};
let room_version = RoomVersion::new(&room_version_id).expect("room version is supported");
let auth_events = self
.services
.state
.get_auth_events(room_id, &event_type, sender, state_key.as_deref(), &content)
.await?;
let prev_events: Vec<OwnedEventId> = match room_id {
| Some(room_id) =>
self.services
.state
.get_forward_extremities(room_id)
.take(20)
.map(Into::into)
.collect()
.await,
| None => Vec::new(),
};
let auth_events: HashMap<(StateEventType, SmallString<[u8; 48]>), PduEvent> = match room_id {
| Some(room_id) =>
self.services
.state
.get_auth_events(
room_id,
&event_type,
sender,
state_key.as_deref(),
&content,
&room_version,
)
.await?,
| None => HashMap::new(),
};
// Our depth is the maximum depth of prev_events + 1
let depth = prev_events
.iter()
.stream()
.map(Ok)
.and_then(|event_id| self.get_pdu(event_id))
.and_then(|pdu| future::ok(pdu.depth))
.ignore_err()
.ready_fold(uint!(0), cmp::max)
.await
.saturating_add(uint!(1));
let depth = match room_id {
| Some(_) => prev_events
.iter()
.stream()
.map(Ok)
.and_then(|event_id| self.get_pdu(event_id))
.and_then(|pdu| future::ok(pdu.depth))
.ignore_err()
.ready_fold(uint!(0), cmp::max)
.await
.saturating_add(uint!(1)),
| None => uint!(1),
};
let mut unsigned = unsigned.unwrap_or_default();
if let Some(state_key) = &state_key {
if let Ok(prev_pdu) = self
.services
.state_accessor
.room_state_get(room_id, &event_type.to_string().into(), state_key)
.await
{
unsigned.insert("prev_content".to_owned(), prev_pdu.get_content_as_value());
unsigned.insert("prev_sender".to_owned(), serde_json::to_value(prev_pdu.sender())?);
unsigned
.insert("replaces_state".to_owned(), serde_json::to_value(prev_pdu.event_id())?);
if let Some(room_id) = room_id {
if let Some(state_key) = &state_key {
if let Ok(prev_pdu) = self
.services
.state_accessor
.room_state_get(room_id, &event_type.clone().to_string().into(), state_key)
.await
{
unsigned.insert("prev_content".to_owned(), prev_pdu.get_content_as_value());
unsigned
.insert("prev_sender".to_owned(), serde_json::to_value(prev_pdu.sender())?);
unsigned.insert(
"replaces_state".to_owned(),
serde_json::to_value(prev_pdu.event_id())?,
);
}
}
}
if event_type != TimelineEventType::RoomCreate && prev_events.is_empty() {
return Err!(Request(Unknown("Event incorrectly had zero prev_events.")));
}
if state_key.is_none() && depth.lt(&uint!(2)) {
// The first two events in a room are always m.room.create and m.room.member,
// so any other events with that same depth are illegal.
warn!(
"Had unsafe depth {depth} when creating non-state event in {room_id}. Cowardly \
aborting"
);
return Err!(Request(Unknown("Unsafe depth for non-state event.")));
}
// if event_type != TimelineEventType::RoomCreate && prev_events.is_empty() {
// return Err!(Request(Unknown("Event incorrectly had zero prev_events.")));
// }
// if state_key.is_none() && depth.lt(&uint!(2)) {
// // The first two events in a room are always m.room.create and
// m.room.member, // so any other events with that same depth are illegal.
// warn!(
// "Had unsafe depth {depth} when creating non-state event in {}. Cowardly
// aborting", room_id.expect("room_id is Some here").as_str()
// );
// return Err!(Request(Unknown("Unsafe depth for non-state event.")));
// }
let mut pdu = PduEvent {
event_id: ruma::event_id!("$thiswillbefilledinlater").into(),
room_id: room_id.to_owned(),
room_id: room_id.map(ToOwned::to_owned),
sender: sender.to_owned(),
origin: None,
origin_server_ts: timestamp.map_or_else(
@@ -152,11 +198,30 @@ pub async fn create_hash_and_sign_event(
ready(auth_events.get(&key).map(ToOwned::to_owned))
};
let room_id_or_hash = pdu.room_id_or_hash();
let create_pdu = match &pdu.kind {
| TimelineEventType::RoomCreate => None,
| _ => Some(
self.services
.state_accessor
.room_state_get(&room_id_or_hash, &StateEventType::RoomCreate, "")
.await
.map_err(|e| {
err!(Request(Forbidden(warn!("Failed to fetch room create event: {e}"))))
})?,
),
};
let create_event = match &pdu.kind {
| TimelineEventType::RoomCreate => &pdu,
| _ => create_pdu.as_ref().unwrap().as_pdu(),
};
let auth_check = state_res::auth_check(
&room_version,
&pdu,
None, // TODO: third_party_invite
auth_fetch,
create_event,
)
.await
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
@@ -164,6 +229,11 @@ pub async fn create_hash_and_sign_event(
if !auth_check {
return Err!(Request(Forbidden("Event is not authorized.")));
}
trace!(
"Event {} in room {} is authorized",
pdu.event_id,
pdu.room_id.as_ref().map_or("None", |id| id.as_str())
);
// Hash and sign
let mut pdu_json = utils::to_canonical_object(&pdu).map_err(|e| {
@@ -178,13 +248,13 @@ pub async fn create_hash_and_sign_event(
},
}
// Add origin because synapse likes that (and it's required in the spec)
pdu_json.insert(
"origin".to_owned(),
to_canonical_value(self.services.globals.server_name())
.expect("server name is a valid CanonicalJsonValue"),
);
trace!("hashing and signing event {}", pdu.event_id);
if let Err(e) = self
.services
.server_keys
@@ -204,30 +274,45 @@ pub async fn create_hash_and_sign_event(
pdu_json.insert("event_id".into(), CanonicalJsonValue::String(pdu.event_id.clone().into()));
// Check with the policy server
match self
.services
.event_handler
.ask_policy_server(&pdu, room_id)
.await
{
| Ok(true) => {},
| Ok(false) => {
return Err!(Request(Forbidden(debug_warn!(
"Policy server marked this event as spam"
))));
},
| Err(e) => {
// fail open
warn!("Failed to check event with policy server: {e}");
},
// 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",
pdu.event_id,
pdu.room_id.as_ref().map_or("None", |id| id.as_str())
);
match self
.services
.event_handler
.ask_policy_server(&pdu, &pdu.room_id_or_hash())
.await
{
| Ok(true) => {},
| Ok(false) => {
return Err!(Request(Forbidden(debug_warn!(
"Policy server marked this event as spam"
))));
},
| Err(e) => {
// fail open
warn!("Failed to check event with policy server: {e}");
},
}
}
// Generate short event id
trace!(
"Generating short event ID for {} in room {}",
pdu.event_id,
pdu.room_id.as_ref().map_or("None", |id| id.as_str())
);
let _shorteventid = self
.services
.short
.get_or_create_shorteventid(&pdu.event_id)
.await;
trace!("New PDU created: {pdu:?}");
Ok((pdu, pdu_json))
}
+5 -1
View File
@@ -39,7 +39,11 @@ pub async fn redact_pdu<Pdu: Event + Send + Sync>(
}
}
let room_version_id = self.services.state.get_room_version(pdu.room_id()).await?;
let room_version_id = self
.services
.state
.get_room_version(&pdu.room_id_or_hash())
.await?;
pdu.redact(&room_version_id, reason.to_value())?;
+1 -1
View File
@@ -798,7 +798,7 @@ async fn send_events_dest_push(
let unread: UInt = self
.services
.user
.notification_count(&user_id, pdu.room_id())
.notification_count(&user_id, &pdu.room_id_or_hash())
.await
.try_into()
.expect("notification count can't go that high");
+5 -1
View File
@@ -1,6 +1,6 @@
use std::borrow::Borrow;
use conduwuit::{Err, Result, implement};
use conduwuit::{Err, Result, debug_error, implement, trace};
use ruma::{
CanonicalJsonObject, RoomVersionId, ServerName, ServerSigningKeyId,
api::federation::discovery::VerifyKey,
@@ -19,9 +19,11 @@ pub async fn get_event_keys(
let required = match required_keys(object, version) {
| Ok(required) => required,
| Err(e) => {
debug_error!("Failed to determine keys required to verify: {e}");
return Err!(BadServerResponse("Failed to determine keys required to verify: {e}"));
},
};
trace!(?required, "Keys required to verify event");
let batch = required
.iter()
@@ -61,6 +63,7 @@ pub async fn get_pubkeys_for<'a, I>(&self, origin: &ServerName, key_ids: I) -> P
}
#[implement(super::Service)]
#[tracing::instrument(skip(self))]
pub async fn get_verify_key(
&self,
origin: &ServerName,
@@ -70,6 +73,7 @@ pub async fn get_verify_key(
let notary_only = self.services.server.config.only_query_trusted_key_servers;
if let Some(result) = self.verify_keys_for(origin).await.remove(key_id) {
trace!("Found key in cache");
return Ok(result);
}
+8 -2
View File
@@ -8,7 +8,7 @@
use std::{collections::BTreeMap, sync::Arc, time::Duration};
use conduwuit::{
Result, Server, implement,
Result, Server, debug_error, debug_warn, implement, trace,
utils::{IterStream, timepoint_from_now},
};
use database::{Deserialized, Json, Map};
@@ -112,6 +112,7 @@ async fn add_signing_keys(&self, new_keys: ServerSigningKeys) {
}
#[implement(Service)]
#[tracing::instrument(skip(self, object))]
pub async fn required_keys_exist(
&self,
object: &CanonicalJsonObject,
@@ -119,10 +120,12 @@ pub async fn required_keys_exist(
) -> bool {
use ruma::signatures::required_keys;
trace!(?object, "Checking required keys exist");
let Ok(required_keys) = required_keys(object, version) else {
debug_error!("Failed to determine required keys");
return false;
};
trace!(?required_keys, "Required keys to verify event");
required_keys
.iter()
.flat_map(|(server, key_ids)| key_ids.iter().map(move |key_id| (server, key_id)))
@@ -132,6 +135,7 @@ pub async fn required_keys_exist(
}
#[implement(Service)]
#[tracing::instrument(skip(self))]
pub async fn verify_key_exists(&self, origin: &ServerName, key_id: &ServerSigningKeyId) -> bool {
type KeysMap<'a> = BTreeMap<&'a ServerSigningKeyId, &'a RawJsonValue>;
@@ -142,6 +146,7 @@ pub async fn verify_key_exists(&self, origin: &ServerName, key_id: &ServerSignin
.await
.deserialized::<Raw<ServerSigningKeys>>()
else {
debug_warn!("No known signing keys found for {origin}");
return false;
};
@@ -157,6 +162,7 @@ pub async fn verify_key_exists(&self, origin: &ServerName, key_id: &ServerSignin
}
}
debug_warn!("Key {key_id} not found for {origin}");
false
}
+13 -4
View File
@@ -1,4 +1,6 @@
use conduwuit::{Err, Result, implement, matrix::event::gen_event_id_canonical_json};
use conduwuit::{
Err, Result, debug_warn, implement, matrix::event::gen_event_id_canonical_json, trace,
};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, RoomVersionId, signatures::Verified,
};
@@ -28,18 +30,25 @@ pub async fn validate_and_add_event_id_no_fetch(
pdu: &RawJsonValue,
room_version: &RoomVersionId,
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
trace!(?pdu, "Validating PDU without fetching keys");
let (event_id, mut value) = gen_event_id_canonical_json(pdu, room_version)?;
trace!(event_id = event_id.as_str(), "Generated event ID, checking required keys");
if !self.required_keys_exist(&value, room_version).await {
debug_warn!(
"Event {event_id} is missing required keys, cannot verify without fetching keys"
);
return Err!(BadServerResponse(debug_warn!(
"Event {event_id} cannot be verified: missing keys."
)));
}
trace!("All required keys exist, verifying event");
if let Err(e) = self.verify_event(&value, Some(room_version)).await {
debug_warn!("Event verification failed");
return Err!(BadServerResponse(debug_error!(
"Event {event_id} failed verification: {e:?}"
)));
}
trace!("Event verified successfully");
value.insert("event_id".into(), CanonicalJsonValue::String(event_id.as_str().into()));
@@ -52,7 +61,7 @@ pub async fn verify_event(
event: &CanonicalJsonObject,
room_version: Option<&RoomVersionId>,
) -> Result<Verified> {
let room_version = room_version.unwrap_or(&RoomVersionId::V11);
let room_version = room_version.unwrap_or(&RoomVersionId::V12);
let keys = self.get_event_keys(event, room_version).await?;
ruma::signatures::verify_event(&keys, event, room_version).map_err(Into::into)
}
@@ -63,7 +72,7 @@ pub async fn verify_json(
event: &CanonicalJsonObject,
room_version: Option<&RoomVersionId>,
) -> Result {
let room_version = room_version.unwrap_or(&RoomVersionId::V11);
let room_version = room_version.unwrap_or(&RoomVersionId::V12);
let keys = self.get_event_keys(event, room_version).await?;
ruma::signatures::verify_json(&keys, event.clone()).map_err(Into::into)
}
+4 -31
View File
@@ -1113,7 +1113,7 @@ pub async fn profile_key(
.useridprofilekey_value
.qry(&key)
.await
.deserialized()
.and_then(|handle| serde_json::from_slice(&handle).map_err(Into::into))
}
/// Gets all the user's profile keys and values in an iterator
@@ -1121,14 +1121,15 @@ pub fn all_profile_keys<'a>(
&'a self,
user_id: &'a UserId,
) -> impl Stream<Item = (String, serde_json::Value)> + 'a + Send {
type KeyVal = ((Ignore, String), serde_json::Value);
type KeyVal<'a> = ((Ignore, String), &'a [u8]);
let prefix = (user_id, Interfix);
self.db
.useridprofilekey_value
.stream_prefix(&prefix)
.ignore_err()
.map(|((_, key), val): KeyVal| (key, val))
.map(|((_, key), value): KeyVal<'_>| Ok((key, serde_json::from_slice(value)?)))
.ignore_err()
}
/// Sets a new profile key value, removes the key if value is None
@@ -1148,34 +1149,6 @@ pub fn set_profile_key(
}
}
/// Get the timezone of a user.
pub async fn timezone(&self, user_id: &UserId) -> Result<String> {
// TODO: transparently migrate unstable key usage to the stable key once MSC4133
// and MSC4175 are stable, likely a remove/insert in this block.
// first check the unstable prefix then check the stable prefix
let unstable_key = (user_id, "us.cloke.msc4175.tz");
let stable_key = (user_id, "m.tz");
self.db
.useridprofilekey_value
.qry(&unstable_key)
.or_else(|_| self.db.useridprofilekey_value.qry(&stable_key))
.await
.deserialized()
}
/// Sets a new timezone or removes it if timezone is None.
pub fn set_timezone(&self, user_id: &UserId, timezone: Option<String>) {
// TODO: insert to the stable MSC4175 key when it's stable
let key = (user_id, "us.cloke.msc4175.tz");
if let Some(timezone) = timezone {
self.db.useridprofilekey_value.put_raw(key, &timezone);
} else {
self.db.useridprofilekey_value.del(key);
}
}
#[cfg(not(feature = "ldap"))]
pub async fn search_ldap(&self, _user_id: &UserId) -> Result<Vec<(String, bool)>> {
Err!(FeatureDisabled("ldap"))