diff --git a/.forgejo/actions/create-docker-manifest/action.yml b/.forgejo/actions/create-docker-manifest/action.yml new file mode 100644 index 000000000..10c9953aa --- /dev/null +++ b/.forgejo/actions/create-docker-manifest/action.yml @@ -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 diff --git a/.forgejo/actions/prepare-docker-build/action.yml b/.forgejo/actions/prepare-docker-build/action.yml new file mode 100644 index 000000000..6a397b8a4 --- /dev/null +++ b/.forgejo/actions/prepare-docker-build/action.yml @@ -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 }} diff --git a/.forgejo/actions/timelord/action.yml b/.forgejo/actions/timelord/action.yml index 40c6d0a6c..43ad6c095 100644 --- a/.forgejo/actions/timelord/action.yml +++ b/.forgejo/actions/timelord/action.yml @@ -27,27 +27,46 @@ runs: 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: Restore binary cache id: binary-cache uses: actions/cache/restore@v4 with: - path: /usr/share/rust/.cargo/bin - key: timelord-binaries-v1 + path: | + /usr/share/rust/.cargo/bin + ~/.cargo/bin + key: timelord-binaries-v3 - name: Check if binaries need installation shell: bash id: check-binaries run: | NEED_INSTALL=false - if [ ! -e /usr/share/rust/.cargo/bin/timelord ]; then - echo "timelord-cli not found at /usr/share/rust/.cargo/bin/timelord, needs installation" + + # 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 - if [ ! -e /usr/share/rust/.cargo/bin/git-warp-time ]; then - echo "git-warp-time not found at /usr/share/rust/.cargo/bin/git-warp-time, needs installation" + + # 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 @@ -60,8 +79,10 @@ runs: if: steps.check-binaries.outputs.need-install == 'true' uses: actions/cache/save@v4 with: - path: /usr/share/rust/.cargo/bin - key: timelord-binaries-v1 + path: | + /usr/share/rust/.cargo/bin + ~/.cargo/bin + key: timelord-binaries-v3 - name: Restore timelord cache with fallbacks @@ -80,9 +101,9 @@ runs: echo "Complete timelord cache miss - running git-warp-time" git fetch --unshallow if [ "${{ env.TIMELORD_PATH }}" = "." ]; then - /usr/share/rust/.cargo/bin/git-warp-time --quiet + git-warp-time --quiet else - /usr/share/rust/.cargo/bin/git-warp-time --quiet ${{ env.TIMELORD_PATH }} + git-warp-time --quiet ${{ env.TIMELORD_PATH }} fi echo "Git timestamps restored" @@ -90,7 +111,7 @@ runs: shell: bash run: | mkdir -p ${{ env.TIMELORD_CACHE_PATH }} - /usr/share/rust/.cargo/bin/timelord sync --source-dir ${{ env.TIMELORD_PATH }} --cache-dir ${{ 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 diff --git a/.forgejo/actions/upload-docker-artifacts/action.yml b/.forgejo/actions/upload-docker-artifacts/action.yml new file mode 100644 index 000000000..ac91e31a1 --- /dev/null +++ b/.forgejo/actions/upload-docker-artifacts/action.yml @@ -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 diff --git a/.forgejo/workflows/release-image.yml b/.forgejo/workflows/release-image.yml index b35195bf0..90ad64904 100644 --- a/.forgejo/workflows/release-image.yml +++ b/.forgejo/workflows/release-image.yml @@ -29,50 +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@v8 - 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: - name: "Build ${{ matrix.slug }}${{ matrix.target_cpu != '' && format('-{0}', matrix.target_cpu) || '' }}" + build-release: + name: "Build ${{ matrix.slug }} (release)" runs-on: dind - needs: define-variables permissions: contents: read packages: write @@ -83,139 +45,25 @@ jobs: include: - platform: "linux/amd64" slug: "linux-amd64" - target_cpu: "" - profile: "release" - - platform: "linux/amd64" - slug: "linux-amd64" - target_cpu: "haswell" - profile: "release-max-perf" - platform: "linux/arm64" slug: "linux-arm64" - target_cpu: "" - profile: "release" steps: - - name: Set CPU suffix variable - run: | - if [[ -n "${{ matrix.target_cpu }}" ]]; then - echo "CPU_SUFFIX=-${{ matrix.target_cpu }}" >> $GITHUB_ENV - else - echo "CPU_SUFFIX=" >> $GITHUB_ENV - fi - - - 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@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 - 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 }}-${{ matrix.slug }}-${{ matrix.profile }} - key: cargo-target${{ env.CPU_SUFFIX }}-${{ 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${{ env.CPU_SUFFIX }}-${{ matrix.slug }}-${{ matrix.profile }}": { - "target": "/app/target", - "id": "cargo-target${{ env.CPU_SUFFIX }}-${{ matrix.slug }}-${{ matrix.profile }}" - }, - "var-cache-apt-${{ matrix.slug }}": "/var/cache/apt", - "var-lib-apt-${{ matrix.slug }}": "/var/lib/apt", - "{{ steps.timelord.outputs.database-path }}":"/timelord" - } - 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 @@ -228,137 +76,129 @@ jobs: 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=${{ matrix.profile }} + 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 }}" - 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/ - - # Binary extracted via local output for all builds - - name: Rename extracted binary - run: mv /tmp/binaries/sbin/conduwuit /tmp/binaries/conduwuit${{ env.CPU_SUFFIX }}-${{ 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${{ env.CPU_SUFFIX }}-${{ matrix.slug }}-${{ matrix.profile }} - path: /tmp/binaries/conduwuit${{ env.CPU_SUFFIX }}-${{ 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 }}${{ env.CPU_SUFFIX }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 5 - - merge: - name: "Create ${{ matrix.target_cpu == '' && 'Multi-arch' || 'Haswell' }} Manifest" + merge-release: + name: "Create Multi-arch Release Manifest" runs-on: dind - needs: [define-variables, build-image] + needs: build-release + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + persist-credentials: false + - name: Create multi-platform manifest + uses: ./.forgejo/actions/create-docker-manifest + with: + 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 }} + + 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: - - target_cpu: "" - digest_pattern: "digests-linux-{amd64,arm64}" - - target_cpu: "haswell" - digest_pattern: "digests-linux-amd64-haswell" + - platform: "linux/amd64" + slug: "linux-amd64" + target_cpu: "haswell" + - platform: "linux/arm64" + slug: "linux-arm64" + target_cpu: "" + 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: ${{ matrix.digest_pattern }} - 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: Prepare max-perf Docker build environment + id: prepare + uses: ./.forgejo/actions/prepare-docker-build with: - registry: ${{ env.BUILTIN_REGISTRY }} - username: ${{ vars.BUILTIN_REGISTRY_USER || github.actor }} - 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 + 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: - # 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=${{ matrix.target_cpu != '' && format('-{0}', matrix.target_cpu) || '' }} - type=semver,pattern={{major}}.{{minor}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.0.') }},prefix=v,suffix=${{ matrix.target_cpu != '' && format('-{0}', matrix.target_cpu) || '' }} - type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }},prefix=v,suffix=${{ matrix.target_cpu != '' && format('-{0}', matrix.target_cpu) || '' }} - type=ref,event=branch,prefix=${{ format('refs/heads/{0}', github.event.repository.default_branch) != github.ref && 'branch-' || '' }},suffix=${{ matrix.target_cpu != '' && format('-{0}', matrix.target_cpu) || '' }} - type=ref,event=pr,suffix=${{ matrix.target_cpu != '' && format('-{0}', matrix.target_cpu) || '' }} - type=sha,format=short,suffix=${{ matrix.target_cpu != '' && format('-{0}', matrix.target_cpu) || '' }} - type=raw,value=latest${{ matrix.target_cpu != '' && format('-{0}', matrix.target_cpu) || '' }},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 + 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 }} diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml index ba02c3edb..c3d3032f3 100644 --- a/.forgejo/workflows/renovate.yml +++ b/.forgejo/workflows/renovate.yml @@ -43,7 +43,7 @@ jobs: name: Renovate runs-on: ubuntu-latest container: - image: ghcr.io/renovatebot/renovate:41.99.9@sha256:8eef7d1db0368b6085d5556a226a518a85dedf51fe9a22c2c66b570ca6169750 + image: ghcr.io/renovatebot/renovate:41.115.0@sha256:7a84af6c88502752f670c16520f869f93789e387ff617d7ed92b1a9c00297f66 options: --tmpfs /tmp:exec steps: - name: Checkout diff --git a/Cargo.lock b/Cargo.lock index 830fc96b0..6d67e8615 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4107,7 +4107,7 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "ruma" version = "0.10.1" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=11d23caf2f03951148161bad46718c9c82dffc39#11d23caf2f03951148161bad46718c9c82dffc39" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=78135227dceb9ce5eb9515b4ae85d452cdcabac8#78135227dceb9ce5eb9515b4ae85d452cdcabac8" dependencies = [ "assign", "js_int", @@ -4127,7 +4127,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.10.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=11d23caf2f03951148161bad46718c9c82dffc39#11d23caf2f03951148161bad46718c9c82dffc39" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=78135227dceb9ce5eb9515b4ae85d452cdcabac8#78135227dceb9ce5eb9515b4ae85d452cdcabac8" dependencies = [ "js_int", "ruma-common", @@ -4139,7 +4139,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.18.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=11d23caf2f03951148161bad46718c9c82dffc39#11d23caf2f03951148161bad46718c9c82dffc39" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=78135227dceb9ce5eb9515b4ae85d452cdcabac8#78135227dceb9ce5eb9515b4ae85d452cdcabac8" dependencies = [ "as_variant", "assign", @@ -4162,7 +4162,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.13.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=11d23caf2f03951148161bad46718c9c82dffc39#11d23caf2f03951148161bad46718c9c82dffc39" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=78135227dceb9ce5eb9515b4ae85d452cdcabac8#78135227dceb9ce5eb9515b4ae85d452cdcabac8" dependencies = [ "as_variant", "base64 0.22.1", @@ -4194,7 +4194,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.28.1" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=11d23caf2f03951148161bad46718c9c82dffc39#11d23caf2f03951148161bad46718c9c82dffc39" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=78135227dceb9ce5eb9515b4ae85d452cdcabac8#78135227dceb9ce5eb9515b4ae85d452cdcabac8" dependencies = [ "as_variant", "indexmap 2.11.1", @@ -4219,7 +4219,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=11d23caf2f03951148161bad46718c9c82dffc39#11d23caf2f03951148161bad46718c9c82dffc39" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=78135227dceb9ce5eb9515b4ae85d452cdcabac8#78135227dceb9ce5eb9515b4ae85d452cdcabac8" dependencies = [ "bytes", "headers", @@ -4241,7 +4241,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.9.5" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=11d23caf2f03951148161bad46718c9c82dffc39#11d23caf2f03951148161bad46718c9c82dffc39" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=78135227dceb9ce5eb9515b4ae85d452cdcabac8#78135227dceb9ce5eb9515b4ae85d452cdcabac8" dependencies = [ "js_int", "thiserror 2.0.16", @@ -4250,7 +4250,7 @@ dependencies = [ [[package]] name = "ruma-identity-service-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=11d23caf2f03951148161bad46718c9c82dffc39#11d23caf2f03951148161bad46718c9c82dffc39" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=78135227dceb9ce5eb9515b4ae85d452cdcabac8#78135227dceb9ce5eb9515b4ae85d452cdcabac8" dependencies = [ "js_int", "ruma-common", @@ -4260,7 +4260,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.13.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=11d23caf2f03951148161bad46718c9c82dffc39#11d23caf2f03951148161bad46718c9c82dffc39" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=78135227dceb9ce5eb9515b4ae85d452cdcabac8#78135227dceb9ce5eb9515b4ae85d452cdcabac8" dependencies = [ "cfg-if", "proc-macro-crate", @@ -4275,7 +4275,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.9.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=11d23caf2f03951148161bad46718c9c82dffc39#11d23caf2f03951148161bad46718c9c82dffc39" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=78135227dceb9ce5eb9515b4ae85d452cdcabac8#78135227dceb9ce5eb9515b4ae85d452cdcabac8" dependencies = [ "js_int", "ruma-common", @@ -4287,7 +4287,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.15.0" -source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=11d23caf2f03951148161bad46718c9c82dffc39#11d23caf2f03951148161bad46718c9c82dffc39" +source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=78135227dceb9ce5eb9515b4ae85d452cdcabac8#78135227dceb9ce5eb9515b4ae85d452cdcabac8" dependencies = [ "base64 0.22.1", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index 836ef7082..19a2ba4dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -352,7 +352,7 @@ version = "0.1.2" [workspace.dependencies.ruma] git = "https://forgejo.ellis.link/continuwuation/ruwuma" #branch = "conduwuit-changes" -rev = "11d23caf2f03951148161bad46718c9c82dffc39" +rev = "78135227dceb9ce5eb9515b4ae85d452cdcabac8" features = [ "compat", "rand", @@ -388,6 +388,7 @@ features = [ "unstable-msc4210", # remove legacy mentions "unstable-extensible-events", "unstable-pdu", + "unstable-msc4155" ] [workspace.dependencies.rust-rocksdb] diff --git a/renovate.json b/renovate.json index ca85cfe78..8c3c35c44 100644 --- a/renovate.json +++ b/renovate.json @@ -6,6 +6,7 @@ "enabled": true, "schedule": ["at any time"] }, + "platformAutomerge": true, "nix": { "enabled": true }, @@ -30,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", @@ -42,25 +48,28 @@ "groupName": "rust-toolchain" }, { - "description": "Group lockfile updates into a single PR", - "matchUpdateTypes": ["lockFileMaintenance"], - "groupName": "lockfile-maintenance" - }, - { - "description": "Batch patch-level Rust dependency updates", - "matchManagers": ["cargo"], - "matchUpdateTypes": ["patch"], - "groupName": "rust-patch-updates" - }, - { - "matchManagers": ["cargo"], - "prConcurrentLimit": 5 + "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" } ], "customManagers": [ diff --git a/src/api/client/account.rs b/src/api/client/account.rs index 67268c9f0..dde3af667 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -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(""); @@ -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, }) diff --git a/src/api/client/profile.rs b/src/api/client/profile.rs index eaa66e70f..e83e747ca 100644 --- a/src/api/client/profile.rs +++ b/src/api/client/profile.rs @@ -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 = 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, }) } diff --git a/src/api/client/unstable.rs b/src/api/client/unstable.rs index f8703ff33..be10c13c4 100644 --- a/src/api/client/unstable.rs +++ b/src/api/client/unstable.rs @@ -2,18 +2,14 @@ use std::collections::BTreeMap; 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, - body: Ruma, -) -> Result { - 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, - body: Ruma, -) -> Result { - 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 = 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 = 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, - body: Ruma, -) -> Result { - 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, body: Ruma, @@ -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()); diff --git a/src/api/router.rs b/src/api/router.rs index 42934f700..4ab9780b1 100644 --- a/src/api/router.rs +++ b/src/api/router.rs @@ -22,12 +22,9 @@ use crate::{client, server}; pub fn build(router: Router, server: &Server) -> Router { 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) diff --git a/src/api/router/auth.rs b/src/api/router/auth.rs index 44afc3ef8..4baf0148e 100644 --- a/src/api/router/auth.rs +++ b/src/api/router/auth.rs @@ -20,9 +20,7 @@ use ruma::{ 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(_) => { diff --git a/src/api/server/query.rs b/src/api/server/query.rs index 9d4fcf738..bd68afda6 100644 --- a/src/api/server/query.rs +++ b/src/api/server/query.rs @@ -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, }) } diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index 931f4b987..3e1dbb6fe 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -1124,6 +1124,34 @@ impl Service { Ok(user_id) } + #[inline] + fn parse_profile_kv( + &self, + user_id: &UserId, + key: &str, + value: Vec, + ) -> Result { + match serde_json::from_slice(&value) { + | Ok(value) => Ok(value), + | Err(error) => { + // Due to an old bug, some conduwuit databases have `us.cloke.msc4175.tz` user + // profile fields with raw strings instead of quoted JSON ones. + if key == "us.cloke.msc4175.tz" { + // TODO insert a hint about this being a cold path + debug_warn!( + "Fixing corrupt `us.cloke.msc4175.tz` field in the profile of {}", + user_id + ); + let raw_tz = serde_json::Value::String(String::from_utf8(value)?); + self.set_profile_key(user_id, "us.cloke.msc4175.tz", Some(raw_tz.clone())); + Ok(raw_tz) + } else { + Err(error.into()) + } + }, + } + } + /// Gets a specific user profile key pub async fn profile_key( &self, @@ -1135,7 +1163,7 @@ impl Service { .useridprofilekey_value .qry(&key) .await - .deserialized() + .and_then(|handle| self.parse_profile_kv(user_id, profile_key, handle.to_vec())) } /// Gets all the user's profile keys and values in an iterator @@ -1143,14 +1171,18 @@ impl Service { &'a self, user_id: &'a UserId, ) -> impl Stream + '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<'_>| { + let value = self.parse_profile_kv(user_id, &key, value.to_vec())?; + Ok((key, value)) + }) + .ignore_err() } /// Sets a new profile key value, removes the key if value is None @@ -1170,34 +1202,6 @@ impl Service { } } - /// Get the timezone of a user. - pub async fn timezone(&self, user_id: &UserId) -> Result { - // 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) { - // 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> { Err!(FeatureDisabled("ldap"))