mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-04-13 19:25:45 +00:00
Compare commits
2 Commits
renovate/h
...
jade/chang
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97d11a65bc | ||
|
|
0228fb502d |
2
.envrc
2
.envrc
@@ -2,7 +2,7 @@
|
||||
|
||||
dotenv_if_exists
|
||||
|
||||
if command -v nix >/dev/null 2>&1; then
|
||||
if [ -f /etc/os-release ] && grep -q '^ID=nixos' /etc/os-release; then
|
||||
use flake ".#${DIRENV_DEVSHELL:-default}"
|
||||
fi
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ runs:
|
||||
|
||||
- name: Login to builtin registry
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.BUILTIN_REGISTRY }}
|
||||
username: ${{ inputs.registry_user }}
|
||||
@@ -52,7 +52,7 @@ runs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@v4
|
||||
with:
|
||||
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
|
||||
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
|
||||
@@ -61,7 +61,7 @@ runs:
|
||||
- name: Extract metadata (tags) for Docker
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
flavor: |
|
||||
latest=auto
|
||||
|
||||
@@ -67,7 +67,7 @@ runs:
|
||||
uses: ./.forgejo/actions/rust-toolchain
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@v4
|
||||
with:
|
||||
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
|
||||
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
|
||||
@@ -75,11 +75,11 @@ runs:
|
||||
|
||||
- name: Set up QEMU
|
||||
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Login to builtin registry
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.BUILTIN_REGISTRY }}
|
||||
username: ${{ inputs.registry_user }}
|
||||
@@ -87,7 +87,7 @@ runs:
|
||||
|
||||
- name: Extract metadata (labels, annotations) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ inputs.images }}
|
||||
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
|
||||
@@ -111,3 +111,59 @@ runs:
|
||||
|
||||
- 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: continuwuity-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: continuwuity-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: continuwuity-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: continuwuity-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.2
|
||||
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 }}
|
||||
|
||||
@@ -33,7 +33,7 @@ runs:
|
||||
echo "version=$(rustup --version)" >> $GITHUB_OUTPUT
|
||||
- name: Cache rustup toolchains
|
||||
if: steps.rustup-version.outputs.version == ''
|
||||
uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.rustup
|
||||
|
||||
@@ -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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
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: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/usr/bin/clang-*
|
||||
@@ -120,7 +120,7 @@ runs:
|
||||
|
||||
- name: Install additional packages
|
||||
if: inputs.extra-packages != ''
|
||||
uses: https://github.com/awalsh128/cache-apt-pkgs-action@2c09a5e66da6c8016428a2172bd76e5e4f14bb17 # latest
|
||||
uses: https://github.com/awalsh128/cache-apt-pkgs-action@latest
|
||||
with:
|
||||
packages: ${{ inputs.extra-packages }}
|
||||
version: 1.0
|
||||
|
||||
@@ -65,7 +65,7 @@ runs:
|
||||
|
||||
- name: Cache toolchain binaries
|
||||
id: toolchain-cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cargo/bin
|
||||
@@ -76,7 +76,7 @@ runs:
|
||||
|
||||
- name: Cache Cargo registry and git
|
||||
id: registry-cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cargo/registry/index
|
||||
@@ -149,6 +149,37 @@ runs:
|
||||
- name: Setup sccache
|
||||
uses: https://git.tomfos.tr/tom/sccache-action@v1
|
||||
|
||||
- name: Cache dependencies
|
||||
id: deps-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
target/**/.fingerprint
|
||||
target/**/deps
|
||||
target/**/*.d
|
||||
target/**/.cargo-lock
|
||||
target/**/CACHEDIR.TAG
|
||||
target/**/.rustc_info.json
|
||||
/timelord/
|
||||
# Dependencies cache - based on Cargo.lock, survives source code changes
|
||||
key: >-
|
||||
continuwuity-deps-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ steps.rust-setup.outputs.version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
continuwuity-deps-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ steps.rust-setup.outputs.version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-
|
||||
|
||||
- name: Cache incremental compilation
|
||||
id: incremental-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
target/**/incremental
|
||||
# Incremental cache - based on source code changes
|
||||
key: >-
|
||||
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ steps.rust-setup.outputs.version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}-${{ hashFiles('**/*.rs', '**/Cargo.toml') }}
|
||||
restore-keys: |
|
||||
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ steps.rust-setup.outputs.version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}-
|
||||
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ steps.rust-setup.outputs.version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-
|
||||
|
||||
- name: End build cache restore group
|
||||
shell: bash
|
||||
run: echo "::endgroup::"
|
||||
|
||||
@@ -31,7 +31,7 @@ runs:
|
||||
|
||||
- name: Restore binary cache
|
||||
id: binary-cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
/usr/share/rust/.cargo/bin
|
||||
@@ -71,13 +71,13 @@ runs:
|
||||
|
||||
- name: Install timelord-cli and git-warp-time
|
||||
if: steps.check-binaries.outputs.need-install == 'true'
|
||||
uses: https://github.com/taiki-e/install-action@d0f23220b09a75c6db730f13bb37c4f8144b4382 # v2
|
||||
uses: https://github.com/taiki-e/install-action@v2
|
||||
with:
|
||||
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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
/usr/share/rust/.cargo/bin
|
||||
@@ -87,7 +87,7 @@ runs:
|
||||
|
||||
- name: Restore timelord cache with fallbacks
|
||||
id: timelord-restore
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: ${{ env.TIMELORD_CACHE_PATH }}
|
||||
key: ${{ env.TIMELORD_KEY }}
|
||||
@@ -114,7 +114,7 @@ runs:
|
||||
timelord sync --source-dir ${{ env.TIMELORD_PATH }} --cache-dir ${{ env.TIMELORD_CACHE_PATH }}
|
||||
|
||||
- name: Save updated timelord cache immediately
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: ${{ env.TIMELORD_CACHE_PATH }}
|
||||
key: ${{ env.TIMELORD_KEY }}
|
||||
|
||||
@@ -54,13 +54,13 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Checkout repository with full history
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
- name: Cache Cargo registry
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
|
||||
@@ -30,14 +30,14 @@ jobs:
|
||||
echo "Fedora version: $VERSION"
|
||||
|
||||
- name: Checkout repository with full history
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
|
||||
- name: Cache DNF packages
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/var/cache/dnf
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
dnf-fedora${{ steps.fedora.outputs.version }}-
|
||||
|
||||
- name: Cache Cargo registry
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
cargo-fedora${{ steps.fedora.outputs.version }}-
|
||||
|
||||
- name: Cache Rust build dependencies
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/rpmbuild/BUILD/*/target/release/deps
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: Checks / Changelog
|
||||
name: Check Changelog
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
|
||||
concurrency:
|
||||
@@ -16,11 +16,11 @@ permissions:
|
||||
|
||||
jobs:
|
||||
check-changelog:
|
||||
name: Check changelog is added
|
||||
name: Check for changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
@@ -33,9 +33,9 @@ jobs:
|
||||
git fetch origin ${GITHUB_BASE_REF}
|
||||
|
||||
# Check for Added (A) or Modified (M) files in changelog.d
|
||||
CHANGELOG_CHANGES=$(git diff --name-status origin/${GITHUB_BASE_REF}...HEAD -- changelog.d/)
|
||||
CHANGELOG_CHANGES=$(git diff --name-status origin/${GITHUB_BASE_REF} HEAD -- changelog.d/)
|
||||
|
||||
SRC_CHANGES=$(git diff --name-status origin/${GITHUB_BASE_REF}...HEAD -- src/)
|
||||
SRC_CHANGES=$(git diff --name-status origin/${GITHUB_BASE_REF} HEAD -- src/)
|
||||
|
||||
echo "Changes in changelog.d/:"
|
||||
echo "$CHANGELOG_CHANGES"
|
||||
@@ -54,8 +54,8 @@ jobs:
|
||||
echo "src_changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Manage PR Labels
|
||||
uses: https://github.com/actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
- name: Manage PR Comment
|
||||
uses: https://github.com/actions/github-script@v8
|
||||
env:
|
||||
HAS_CHANGELOG: ${{ steps.check_files.outputs.has_changelog }}
|
||||
SRC_CHANGED: ${{ steps.check_files.outputs.src_changed }}
|
||||
@@ -63,37 +63,41 @@ jobs:
|
||||
script: |
|
||||
const hasChangelog = process.env.HAS_CHANGELOG === 'true';
|
||||
const srcChanged = process.env.SRC_CHANGED === 'true';
|
||||
const commentSignature = '<!-- changelog-check-action -->';
|
||||
const commentBody = `${commentSignature}\nPlease add a changelog fragment to \`changelog.d/\` describing your changes.`;
|
||||
|
||||
const { data: pullRequest } = await github.rest.pulls.get({
|
||||
const { data: currentUser } = await github.rest.users.getAuthenticated();
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const currentLabels = pullRequest.labels.map(l => l.name);
|
||||
const botComment = comments.find(comment =>
|
||||
comment.user.id === currentUser.id &&
|
||||
comment.body.includes(commentSignature)
|
||||
);
|
||||
|
||||
if (hasChangelog) {
|
||||
console.log('PR has changelog');
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['Changelog/Added'],
|
||||
});
|
||||
} else if (currentLabels.includes('Changelog/None')) {
|
||||
console.log('PR has Changelog/None label, skipping.');
|
||||
} else if (srcChanged) {
|
||||
console.log('PR is missing changelog');
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['Changelog/Missing'],
|
||||
});
|
||||
core.setFailed("Missing changelog entry (detected)");
|
||||
} else if (currentLabels.includes('Changelog/Missing')) {
|
||||
core.setFailed("Missing changelog entry (label)");
|
||||
const shouldWarn = srcChanged && !hasChangelog;
|
||||
|
||||
if (!shouldWarn) {
|
||||
if (botComment) {
|
||||
console.log('Changelog found or not required. Deleting existing warning comment.');
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('Changelog not needed');
|
||||
// Changelog is probably not needed
|
||||
if (!botComment) {
|
||||
console.log('Changelog missing and required. Creating warning comment.');
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: commentBody,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Sync repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -32,12 +32,12 @@ 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@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: https://github.com/actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Cache npm dependencies
|
||||
uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: continuwuity-rspress-${{ steps.runner-env.outputs.slug }}-${{ steps.runner-env.outputs.arch }}-node-${{ steps.runner-env.outputs.node_version }}-${{ hashFiles('package-lock.json') }}
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
- name: Deploy to Cloudflare Pages (Production)
|
||||
if: github.ref == 'refs/heads/main' && vars.CLOUDFLARE_PROJECT_NAME != ''
|
||||
uses: https://github.com/cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3
|
||||
uses: https://github.com/cloudflare/wrangler-action@v3
|
||||
with:
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
- name: Deploy to Cloudflare Pages (Preview)
|
||||
if: github.ref != 'refs/heads/main' && vars.CLOUDFLARE_PROJECT_NAME != ''
|
||||
uses: https://github.com/cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3
|
||||
uses: https://github.com/cloudflare/wrangler-action@v3
|
||||
with:
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: https://github.com/actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
uses: https://github.com/actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: 🚀 Deploy to Cloudflare Pages
|
||||
if: vars.CLOUDFLARE_PROJECT_NAME != ''
|
||||
id: deploy
|
||||
uses: https://github.com/cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3
|
||||
uses: https://github.com/cloudflare/wrangler-action@v3
|
||||
with:
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
DOCKER_MIRROR_TOKEN: ${{ secrets.DOCKER_MIRROR_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -51,8 +51,10 @@ jobs:
|
||||
# owner: continuwuity
|
||||
# repositories: continuwuity
|
||||
|
||||
- name: Install regsync
|
||||
uses: https://github.com/regclient/actions/regsync-installer@f07124ffba4b0cbf96b2a666d481ed9d44b5e7e4 # main
|
||||
- name: Install regctl
|
||||
uses: https://forgejo.ellis.link/continuwuation/regclient-actions/regctl-installer@main
|
||||
with:
|
||||
binary: regsync
|
||||
|
||||
- name: Check what images need mirroring
|
||||
run: |
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Prepare Docker build environment
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push Docker image by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: "docker/Dockerfile"
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
needs: build-release
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Create multi-platform manifest
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Prepare max-perf Docker build environment
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
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@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: "docker/Dockerfile"
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
needs: build-maxperf
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Create max-perf manifest
|
||||
|
||||
@@ -43,11 +43,11 @@ jobs:
|
||||
name: Renovate
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/renovatebot/renovate:43.111.0@sha256:da5fcac20c48d9792aac9c61fd234531bfa8df61263a39387cd8920263ca4768
|
||||
image: ghcr.io/renovatebot/renovate:43.59.4@sha256:f951508dea1e7d71cbe6deca298ab0a05488e7631229304813f630cc06010892
|
||||
options: --tmpfs /tmp:exec
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
@@ -55,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: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
/tmp/renovate/cache/renovate/repository
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
renovate-repo-cache-
|
||||
|
||||
- name: Restore renovate package cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
/tmp/renovate/cache/renovate/renovate-cache-sqlite
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
renovate-package-cache-
|
||||
|
||||
- name: Restore renovate OSV cache
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
/tmp/osv
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
|
||||
- name: Save renovate package cache
|
||||
if: always()
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
/tmp/renovate/cache/renovate/renovate-cache-sqlite
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
|
||||
- name: Save renovate OSV cache
|
||||
if: always()
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
/tmp/osv
|
||||
|
||||
@@ -14,21 +14,50 @@ jobs:
|
||||
update-flake-hashes:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: false
|
||||
fetch-single-branch: true
|
||||
submodules: false
|
||||
persist-credentials: true
|
||||
token: ${{ secrets.FORGEJO_TOKEN }}
|
||||
|
||||
- name: Install Lix
|
||||
uses: https://github.com/samueldr/lix-gha-installer-action@f5e94192f565f53d84f41a056956dc0d3183b343
|
||||
- uses: https://github.com/cachix/install-nix-action@19effe9fe722874e6d46dd7182e4b8b7a43c4a99 # v31.10.0
|
||||
with:
|
||||
extra_nix_config: experimental-features = nix-command flakes flake-self-attrs
|
||||
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 "detected changes in $(cat changed_files.txt)"
|
||||
# Join files with commas
|
||||
files=$(paste -sd, changed_files.txt)
|
||||
echo "files=$files" >> $FORGEJO_OUTPUT
|
||||
|
||||
- name: Debug output
|
||||
run: |
|
||||
echo "State of output"
|
||||
echo "Changed files: ${{ steps.changes.outputs.files }}"
|
||||
|
||||
- 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 = lib.fakeSha256;"); found=0} 1' nix/rust.nix > temp.nix
|
||||
mv temp.nix nix/rust.nix
|
||||
awk '/fromToolchainFile *\{/{found=1; print; next} found && /sha256 =/{sub(/sha256 = .*/, "sha256 = lib.fakeSha256;"); found=0} 1' nix/packages/rust.nix > temp.nix
|
||||
mv temp.nix nix/packages/rust.nix
|
||||
|
||||
# Build continuwuity and filter for the new hash
|
||||
# We do `|| true` because we want this to fail without stopping the workflow
|
||||
@@ -36,17 +65,36 @@ jobs:
|
||||
|
||||
# Place the new hash in place of the empty hash
|
||||
new_hash=$(cat new_toolchain_hash.txt)
|
||||
sed -i "s|lib.fakeSha256|\"$new_hash\"|" nix/rust.nix
|
||||
sed -i "s|lib.fakeSha256|\"$new_hash\"|" nix/packages/rust.nix
|
||||
|
||||
echo "New hash:"
|
||||
awk -F'"' '/fromToolchainFile/{found=1; next} found && /sha256 =/{print $2; found=0}' nix/rust.nix
|
||||
awk -F'"' '/fromToolchainFile/{found=1; next} found && /sha256 =/{print $2; found=0}' nix/packages/rust.nix
|
||||
echo "Expected new hash:"
|
||||
cat new_toolchain_hash.txt
|
||||
|
||||
rm new_toolchain_hash.txt
|
||||
|
||||
- name: Update rocksdb
|
||||
run: nix run .#update-rocksdb
|
||||
- name: Get new rocksdb hash
|
||||
if: contains(steps.changes.outputs.files, '.nix') || contains(steps.changes.outputs.files, 'flake.lock')
|
||||
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 = lib.fakeSha256;"); found=0} 1' nix/packages/rocksdb/package.nix > temp.nix
|
||||
mv temp.nix nix/packages/rocksdb/package.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|lib.fakeSha256|\"$new_hash\"|" nix/packages/rocksdb/package.nix
|
||||
|
||||
echo "New hash:"
|
||||
awk -F'"' '/repo = "rocksdb";/{found=1; next} found && /sha256 =/{print $2; found=0}' nix/packages/rocksdb/package.nix
|
||||
echo "Expected new hash:"
|
||||
cat new_rocksdb_hash.txt
|
||||
|
||||
rm new_rocksdb_hash.txt
|
||||
|
||||
- name: Show diff
|
||||
run: git diff flake.nix nix
|
||||
|
||||
@@ -24,7 +24,7 @@ repos:
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.45.0
|
||||
rev: v1.44.0
|
||||
hooks:
|
||||
- id: typos
|
||||
- id: typos
|
||||
|
||||
@@ -1 +1,131 @@
|
||||
Contributors are expected to follow the [Continuwuity Community Guidelines](continuwuity.org/community/guidelines).
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement over Matrix at [#continuwuity:continuwuity.org](https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org) or email at <tom@tcpip.uk>, <jade@continuwuity.org> and <nex@continuwuity.org> respectively.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
||||
808
Cargo.lock
generated
808
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
55
Cargo.toml
55
Cargo.toml
@@ -39,7 +39,7 @@ features = ["ffi", "std", "union"]
|
||||
version = "0.7.0"
|
||||
|
||||
[workspace.dependencies.ctor]
|
||||
version = "0.9.0"
|
||||
version = "0.6.0"
|
||||
|
||||
[workspace.dependencies.cargo_toml]
|
||||
version = "0.22"
|
||||
@@ -60,7 +60,7 @@ default-features = false
|
||||
|
||||
# used for TURN server authentication
|
||||
[workspace.dependencies.hmac]
|
||||
version = "0.13.0"
|
||||
version = "0.12.1"
|
||||
default-features = false
|
||||
|
||||
# used for checking if an IP is in specific subnets / CIDR ranges easier
|
||||
@@ -159,7 +159,7 @@ features = ["raw_value"]
|
||||
|
||||
# Used for appservice registration files
|
||||
[workspace.dependencies.serde-saphyr]
|
||||
version = "0.0.23"
|
||||
version = "0.0.21"
|
||||
|
||||
# Used to load forbidden room/user regex from config
|
||||
[workspace.dependencies.serde_regex]
|
||||
@@ -278,7 +278,7 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.hyper-util]
|
||||
version = "=0.1.20"
|
||||
version = "=0.1.17"
|
||||
default-features = false
|
||||
features = [
|
||||
"server-auto",
|
||||
@@ -332,7 +332,7 @@ version = "0.4.0"
|
||||
|
||||
# used for MPMC channels
|
||||
[workspace.dependencies.async-channel]
|
||||
version = "2.5.0"
|
||||
version = "2.3.1"
|
||||
|
||||
[workspace.dependencies.async-trait]
|
||||
version = "0.1.88"
|
||||
@@ -344,7 +344,7 @@ version = "0.1.2"
|
||||
[workspace.dependencies.ruma]
|
||||
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
||||
#branch = "conduwuit-changes"
|
||||
rev = "1415caf8a32af4d943580c5ea4e12be1974593c2"
|
||||
rev = "a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||
features = [
|
||||
"compat",
|
||||
"rand",
|
||||
@@ -383,13 +383,12 @@ features = [
|
||||
"unstable-pdu",
|
||||
"unstable-msc4155",
|
||||
"unstable-msc4143", # livekit well_known response
|
||||
"unstable-msc4284",
|
||||
"unstable-msc4439", # pgp_key in .well_known/matrix/support
|
||||
"unstable-msc4284"
|
||||
]
|
||||
|
||||
[workspace.dependencies.rust-rocksdb]
|
||||
git = "https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1"
|
||||
rev = "31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9"
|
||||
rev = "61d9d23872197e9ace4a477f2617d5c9f50ecb23"
|
||||
default-features = false
|
||||
features = [
|
||||
"multi-threaded-cf",
|
||||
@@ -404,7 +403,7 @@ version = "0.10.8"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.sha1]
|
||||
version = "0.11.0"
|
||||
version = "0.10.6"
|
||||
default-features = false
|
||||
|
||||
# optional opentelemetry, performance measurements, flamegraphs, etc for performance measurements and monitoring
|
||||
@@ -452,7 +451,7 @@ version = "0.46.0"
|
||||
# jemalloc usage
|
||||
[workspace.dependencies.tikv-jemalloc-sys]
|
||||
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
rev = "df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554"
|
||||
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
default-features = false
|
||||
features = [
|
||||
"background_threads_runtime_support",
|
||||
@@ -460,7 +459,7 @@ features = [
|
||||
]
|
||||
[workspace.dependencies.tikv-jemallocator]
|
||||
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
rev = "df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554"
|
||||
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
default-features = false
|
||||
features = [
|
||||
"background_threads_runtime_support",
|
||||
@@ -468,7 +467,7 @@ features = [
|
||||
]
|
||||
[workspace.dependencies.tikv-jemalloc-ctl]
|
||||
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
rev = "df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554"
|
||||
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
default-features = false
|
||||
features = ["use_std"]
|
||||
|
||||
@@ -481,7 +480,7 @@ default-features = false
|
||||
features = ["resource"]
|
||||
|
||||
[workspace.dependencies.sd-notify]
|
||||
version = "0.5.0"
|
||||
version = "0.4.5"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.hardened_malloc-rs]
|
||||
@@ -494,7 +493,7 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.rustyline-async]
|
||||
version = "0.4.9"
|
||||
version = "0.4.3"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.termimad]
|
||||
@@ -527,7 +526,7 @@ version = "0.4.13"
|
||||
version = "2.0"
|
||||
|
||||
[workspace.dependencies.core_affinity]
|
||||
version = "0.8.3"
|
||||
version = "0.8.1"
|
||||
|
||||
[workspace.dependencies.libc]
|
||||
version = "0.2"
|
||||
@@ -557,19 +556,6 @@ version = "1.0.1"
|
||||
[workspace.dependencies.askama]
|
||||
version = "0.15.0"
|
||||
|
||||
[workspace.dependencies.lettre]
|
||||
version = "0.11.19"
|
||||
default-features = false
|
||||
features = ["smtp-transport", "pool", "hostname", "builder", "rustls", "rustls-native-certs", "tokio1", "ring", "tokio1-rustls", "tracing", "serde"]
|
||||
|
||||
[workspace.dependencies.governor]
|
||||
version = "0.10.4"
|
||||
default-features = false
|
||||
features = ["std"]
|
||||
|
||||
[workspace.dependencies.nonzero_ext]
|
||||
version = "0.3.0"
|
||||
|
||||
#
|
||||
# Patches
|
||||
#
|
||||
@@ -582,25 +568,25 @@ version = "0.3.0"
|
||||
# adds event for CTRL+\: https://forgejo.ellis.link/continuwuation/rustyline-async/src/branch/main/.patchy/0001-add-event-for-ctrl.patch
|
||||
[patch.crates-io.rustyline-async]
|
||||
git = "https://forgejo.ellis.link/continuwuation/rustyline-async"
|
||||
rev = "b13aca2cc08d5f78303746cd192d9a03d73e768e"
|
||||
rev = "e9f01cf8c6605483cb80b3b0309b400940493d7f"
|
||||
|
||||
# adds LIFO queue scheduling; this should be updated with PR progress.
|
||||
[patch.crates-io.event-listener]
|
||||
git = "https://forgejo.ellis.link/continuwuation/event-listener"
|
||||
rev = "b2c19bcaf5a0a69c38c034e417bda04a9b991529"
|
||||
rev = "fe4aebeeaae435af60087ddd56b573a2e0be671d"
|
||||
[patch.crates-io.async-channel]
|
||||
git = "https://forgejo.ellis.link/continuwuation/async-channel"
|
||||
rev = "e990f0006b68dc9bace7a3c95fc90b5c4e44948d"
|
||||
rev = "92e5e74063bf2a3b10414bcc8a0d68b235644280"
|
||||
|
||||
# adds affinity masks for selecting more than one core at a time
|
||||
[patch.crates-io.core_affinity]
|
||||
git = "https://forgejo.ellis.link/continuwuation/core_affinity_rs"
|
||||
rev = "7c7a9dea35382743a63837cdd1d977efdb8f1b8a"
|
||||
rev = "9c8e51510c35077df888ee72a36b4b05637147da"
|
||||
|
||||
# reverts hyperium#148 conflicting with our delicate federation resolver hooks
|
||||
[patch.crates-io.hyper-util]
|
||||
git = "https://forgejo.ellis.link/continuwuation/hyper-util"
|
||||
rev = "09fcd3bf4656c81a8ad573bee410ab2b57f60b86"
|
||||
rev = "5886d5292bf704c246206ad72d010d674a7b77d0"
|
||||
|
||||
#
|
||||
# Our crates
|
||||
@@ -930,6 +916,7 @@ fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
get_unwrap = "warn"
|
||||
impl_trait_in_params = "warn"
|
||||
let_underscore_untyped = "warn"
|
||||
lossy_float_literal = "warn"
|
||||
mem_forget = "warn"
|
||||
missing_assert_message = "warn"
|
||||
|
||||
3
LICENSE
3
LICENSE
@@ -1,3 +1,4 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
@@ -186,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2023 Continuwuity Team and contributors
|
||||
Copyright 2023 June
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
23
book.toml
Normal file
23
book.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[book]
|
||||
title = "continuwuity"
|
||||
description = "continuwuity is a community continuation of the conduwuit Matrix homeserver, written in Rust."
|
||||
language = "en"
|
||||
authors = ["The continuwuity Community"]
|
||||
text-direction = "ltr"
|
||||
src = "docs"
|
||||
|
||||
[build]
|
||||
build-dir = "public"
|
||||
create-missing = true
|
||||
extra-watch-dirs = ["debian", "docs"]
|
||||
|
||||
[rust]
|
||||
edition = "2024"
|
||||
|
||||
[output.html]
|
||||
edit-url-template = "https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/{path}"
|
||||
git-repository-url = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||
git-repository-icon = "fa-git-alt"
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 15
|
||||
@@ -1 +0,0 @@
|
||||
Added support for associating email addresses with accounts, requiring email addresses for registration, and resetting passwords via email. Contributed by @ginger
|
||||
@@ -1 +0,0 @@
|
||||
Added support for requiring users to accept terms and conditions when registering.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed room alias deletion so removing one local alias no longer removes other aliases from room alias listings.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed internal server errors for fetching thumbnails. Contributed by @PerformativeJade
|
||||
@@ -1 +0,0 @@
|
||||
Fixed error 500 when joining non-existent rooms. Contributed by @ezera.
|
||||
@@ -1 +0,0 @@
|
||||
Refactored nix package. Breaking, since `all-features` package no longer exists. Continuwuity is now built with jemalloc and liburing by default. Contributed by @Henry-Hiles (QuadRadical).
|
||||
@@ -1,2 +0,0 @@
|
||||
Add new config option for [MSC4439](https://github.com/matrix-org/matrix-spec-proposals/pull/4439)
|
||||
PGP key URIs. Contributed by LogN.
|
||||
@@ -1 +0,0 @@
|
||||
Added `!admin users reset-push-rules` command to reset the notification settings of users. Contributed by @nex.
|
||||
@@ -1 +0,0 @@
|
||||
Notification pushers are now automatically removed when their associated device is. Admin commands now exist for manual cleanup too. Contributed by @nex.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed resolving IP of servers that only use SRV delegation. Contributed by @tulir.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed compiler warning in cf_opts.rs when building in release. Contributed by @ezera.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed "Sender must be a local user" error for make_join, make_knock, and make_leave federation routes. Contributed by @nex.
|
||||
@@ -1 +0,0 @@
|
||||
Added admin commands to get build information and features. Contributed by @Jade
|
||||
@@ -1 +0,0 @@
|
||||
Fixed restricted joins not being signed when we are being used as an authorising server. Contributed by @nex, reported by [vel](matrix:u/vel:nhjkl.com?action=chat).
|
||||
@@ -95,10 +95,6 @@
|
||||
# engine API. To use this, set a database backup path that continuwuity
|
||||
# can write to.
|
||||
#
|
||||
# If you are using systemd, you will need to add the path to
|
||||
# ReadWritePaths in the service file, preferably via a drop-in file
|
||||
# through `systemctl edit`.
|
||||
#
|
||||
# For more information, see:
|
||||
# https://continuwuity.org/maintenance.html#backups
|
||||
#
|
||||
@@ -523,18 +519,6 @@
|
||||
#
|
||||
#recaptcha_private_site_key =
|
||||
|
||||
# Policy documents, such as terms and conditions or a privacy policy,
|
||||
# which users must agree to when registering an account.
|
||||
#
|
||||
# Example:
|
||||
# ```ignore
|
||||
# [global.registration_terms.privacy_policy]
|
||||
# en = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
|
||||
# es = { name = "Política de Privacidad", url = "https://homeserver.example/es/privacy_policy.html" }
|
||||
# ```
|
||||
#
|
||||
#registration_terms = {}
|
||||
|
||||
# Controls whether encrypted rooms and events are allowed.
|
||||
#
|
||||
#allow_encryption = true
|
||||
@@ -1881,11 +1865,6 @@
|
||||
#
|
||||
#support_mxid =
|
||||
|
||||
# PGP key URI for server support contacts, to be served as part of the
|
||||
# MSC1929 server support endpoint.
|
||||
#
|
||||
#support_pgp_key =
|
||||
|
||||
# **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
|
||||
#
|
||||
# A list of MatrixRTC foci URLs which will be served as part of the
|
||||
@@ -2058,41 +2037,3 @@
|
||||
# web->synapseHTTPAntispam->authorization
|
||||
#
|
||||
#secret =
|
||||
|
||||
#[global.smtp]
|
||||
|
||||
# A `smtp://`` URI which will be used to connect to a mail server.
|
||||
# Uncommenting the [global.smtp] group and setting this option enables
|
||||
# features which depend on the ability to send email,
|
||||
# such as self-service password resets.
|
||||
#
|
||||
# For most modern mail servers, format the URI like this:
|
||||
# `smtps://username:password@hostname:port`
|
||||
# Note that you will need to URL-encode the username and password. If your
|
||||
# username _is_ your email address, you will need to replace the `@` with
|
||||
# `%40`.
|
||||
#
|
||||
# For a guide on the accepted URI syntax, consult Lettre's documentation:
|
||||
# https://docs.rs/lettre/latest/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url
|
||||
#
|
||||
#connection_uri =
|
||||
|
||||
# The outgoing address which will be used for sending emails.
|
||||
#
|
||||
# For a syntax guide, see https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||
#
|
||||
# ...or if you don't want to read the RFC, for some reason:
|
||||
# - `Name <address@domain.org>` to specify a sender name
|
||||
# - `address@domain.org` to not use a name
|
||||
#
|
||||
#sender =
|
||||
|
||||
# Whether to require that users provide an email address when they
|
||||
# register.
|
||||
#
|
||||
#require_email_for_registration = false
|
||||
|
||||
# Whether to require that users who register with a registration token
|
||||
# provide an email address.
|
||||
#
|
||||
#require_email_for_token_registration = false
|
||||
|
||||
@@ -15,13 +15,13 @@ ARG LLVM_VERSION=21
|
||||
|
||||
# Install repo tools
|
||||
# Line one: compiler tools
|
||||
# Line two: curl, for downloading binaries and wget because llvm.sh is broken with curl
|
||||
# Line two: curl, for downloading binaries
|
||||
# Line three: for xx-verify
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get update && apt-get install -y \
|
||||
pkg-config make jq \
|
||||
wget curl git software-properties-common \
|
||||
curl git software-properties-common \
|
||||
file
|
||||
|
||||
# LLVM packages
|
||||
@@ -48,7 +48,7 @@ EOF
|
||||
|
||||
# Developer tool versions
|
||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||
ENV BINSTALL_VERSION=1.18.0
|
||||
ENV BINSTALL_VERSION=1.17.8
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -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.18.0
|
||||
ENV BINSTALL_VERSION=1.17.8
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -5,7 +5,7 @@ # Matrix RTC/Element Call Setup
|
||||
:::
|
||||
|
||||
:::tip
|
||||
You can find help setting up MatrixRTC in our dedicated room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
|
||||
You can find help setting up Matrix RTC in our dedicated room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
|
||||
:::
|
||||
|
||||
## Instructions
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
# Continuwuity Community Guidelines
|
||||
|
||||
Welcome to the Continuwuity commuwunity! We're excited to have you here.
|
||||
Welcome to the Continuwuity commuwunity! We're excited to have you here. Continuwuity is a
|
||||
continuation of the conduwuit homeserver, which in turn is a hard-fork of the Conduit homeserver,
|
||||
aimed at making Matrix more accessible and inclusive for everyone.
|
||||
|
||||
Our project aims to make Matrix more accessible and inclusive for everyone. To that end, we are dedicated to fostering a positive, supportive, safe and welcoming environment for our community.
|
||||
This space is dedicated to fostering a positive, supportive, and welcoming environment for everyone.
|
||||
These guidelines apply to all Continuwuity spaces, including our Matrix rooms and any other
|
||||
community channels that reference them. We've written these guidelines to help us all create an
|
||||
environment where everyone feels safe and respected.
|
||||
|
||||
These guidelines apply to all Continuwuity spaces, including our Matrix rooms and code forge.
|
||||
|
||||
Our community spaces are intended for individuals aged 16 or over, because we expect maturity and respect from our community members.
|
||||
For code and contribution guidelines, please refer to the
|
||||
[Contributor's Covenant](https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CODE_OF_CONDUCT.md).
|
||||
Below are additional guidelines specific to the Continuwuity community.
|
||||
|
||||
## Our Values and Expected Behaviors
|
||||
|
||||
@@ -24,21 +29,17 @@ ## Our Values and Expected Behaviors
|
||||
|
||||
3. **Communicate Clearly and Kindly**: Our community includes neurodivergent individuals and those
|
||||
who may not appreciate sarcasm or subtlety. Communicate clearly and kindly. Avoid ambiguity and
|
||||
ensure your messages can be easily understood by all.
|
||||
|
||||
4. **Be Considerate and Proactive**: Not everyone has the same time, resource and experience to spare.
|
||||
Don't expect others to give up their time and labour for you; be thankful for what you have already been given.
|
||||
Avoid placing the burden of education on
|
||||
ensure your messages can be easily understood by all. Avoid placing the burden of education on
|
||||
marginalized groups; please make an effort to look into your questions before asking others for
|
||||
detailed explanations.
|
||||
|
||||
5. **Be Engaged and Open-Minded**: Actively participate in making our community more inclusive.
|
||||
4. **Be Open to Improving Inclusivity**: Actively participate in making our community more inclusive.
|
||||
Report behaviour that contradicts these guidelines (see Reporting and Enforcement below) and be
|
||||
open to constructive feedback aimed at improving our community. Understand that discussing
|
||||
negative experiences can be emotionally taxing; focus on the message, not the tone.
|
||||
|
||||
6. **Commit to Our Values**: Building an inclusive community requires ongoing effort from everyone.
|
||||
Recognise that creating a welcoming and open community is a continuous process that needs commitment
|
||||
5. **Commit to Our Values**: Building an inclusive community requires ongoing effort from everyone.
|
||||
Recognise that addressing bias and discrimination is a continuous process that needs commitment
|
||||
and action from all members.
|
||||
|
||||
## Unacceptable Behaviors
|
||||
@@ -71,6 +72,36 @@ ## Unacceptable Behaviors
|
||||
This is not an exhaustive list. Any behaviour that makes others feel unsafe or unwelcome may be
|
||||
subject to enforcement action.
|
||||
|
||||
## Matrix Community
|
||||
|
||||
These Community Guidelines apply to the entire
|
||||
[Continuwuity Matrix Space](https://matrix.to/#/#space:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org) and its rooms, including:
|
||||
|
||||
### [#continuwuity:continuwuity.org](https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org)
|
||||
|
||||
This room is for support and discussions about Continuwuity. Ask questions, share insights, and help
|
||||
each other out while adhering to these guidelines.
|
||||
|
||||
We ask that this room remain focused on the Continuwuity software specifically: the team are
|
||||
typically happy to engage in conversations about related subjects in the off-topic room.
|
||||
|
||||
### [#offtopic:continuwuity.org](https://matrix.to/#/#offtopic:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org)
|
||||
|
||||
For off-topic community conversations about any subject. While this room allows for a wide range of
|
||||
topics, the same guidelines apply. Please keep discussions respectful and inclusive, and avoid
|
||||
divisive or stressful subjects like specific country/world politics unless handled with exceptional
|
||||
care and respect for diverse viewpoints.
|
||||
|
||||
General topics, such as world events, are welcome as long as they follow the guidelines. If a member
|
||||
of the team asks for the conversation to end, please respect their decision.
|
||||
|
||||
### [#dev:continuwuity.org](https://matrix.to/#/#dev:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org)
|
||||
|
||||
This room is dedicated to discussing active development of Continuwuity, including ongoing issues or
|
||||
code development. Collaboration here must follow these guidelines, and please consider raising
|
||||
[an issue](https://forgejo.ellis.link/continuwuation/continuwuity/issues) on the repository to help
|
||||
track progress.
|
||||
|
||||
## Reporting and Enforcement
|
||||
|
||||
We take these Community Guidelines seriously to protect our community members. If you witness or
|
||||
@@ -83,7 +114,6 @@ ## Reporting and Enforcement
|
||||
will immediately alert all available moderators.
|
||||
* **Direct Message:** If you're not comfortable raising the issue publicly, please send a direct
|
||||
message (DM) to one of the room moderators.
|
||||
* **Email**: Please email Jade and/or Nex at `jade@continuwuity.org` and `nex@continuwuity.org` respectively, or email `team@continuwuity.org`.
|
||||
|
||||
Reports will be handled with discretion. We will investigate promptly and thoroughly.
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ ### Prebuilt binary
|
||||
run the `uname -m` to check which you need.
|
||||
|
||||
Prebuilt binaries are available from:
|
||||
|
||||
- **Tagged releases**: [Latest release page](https://forgejo.ellis.link/continuwuation/continuwuity/releases/latest)
|
||||
- **Development builds**: CI artifacts from the `main` branch
|
||||
(includes Debian/Ubuntu packages)
|
||||
@@ -43,36 +42,32 @@ #### Performance-optimised builds
|
||||
[link-time optimisation (LTO)](https://doc.rust-lang.org/cargo/reference/profiles.html#lto)
|
||||
and, for amd64, target the haswell CPU architecture.
|
||||
|
||||
### Nix
|
||||
|
||||
Theres a Nix package defined in our flake, available for Linux and MacOS. Add continuwuity as an input to your flake, and use `inputs.continuwuity.packages.${system}.default` to get a working Continuwuity package.
|
||||
|
||||
If you simply wish to generate a binary using Nix, you can run `nix build git+https://forgejo.ellis.link/continuwuation/continuwuity` to generate a binary in `result/bin/conduwuit`.
|
||||
|
||||
### Compiling
|
||||
|
||||
Alternatively, you may compile the binary yourself.
|
||||
|
||||
#### Using Docker
|
||||
### Building with the Rust toolchain
|
||||
|
||||
If you would like to build using docker, you can run the command `docker build -f ./docker/Dockerfile -t forgejo.ellis.link/continuwuation/continuwuity:main .` to compile continuwuity.
|
||||
If wanting to build using standard Rust toolchains, make sure you install:
|
||||
|
||||
#### Manual
|
||||
|
||||
##### Dependencies
|
||||
|
||||
- Run `nix develop` to get a devshell with everything you need
|
||||
- Or, install the following:
|
||||
- (On linux) `liburing-dev` on the compiling machine, and `liburing` on the target host
|
||||
- (On linux) `pkg-config` on the compiling machine to allow finding `liburing`
|
||||
- A C++ compiler and (on linux) `libclang` for RocksDB
|
||||
|
||||
##### Build
|
||||
- (On linux) `liburing-dev` on the compiling machine, and `liburing` on the target host
|
||||
- (On linux) `pkg-config` on the compiling machine to allow finding `liburing`
|
||||
- A C++ compiler and (on linux) `libclang` for RocksDB
|
||||
|
||||
You can build Continuwuity using `cargo build --release`.
|
||||
|
||||
Continuwuity supports various optional features that can be enabled during compilation. Please see the Cargo.toml file for a comprehensive list, or ask in our rooms.
|
||||
|
||||
### Building with Nix
|
||||
|
||||
If you prefer, you can use Nix (or [Lix](https://lix.systems)) to build Continuwuity. This provides improved reproducibility and makes it easy to set up a build environment and generate output. This approach also allows for easy cross-compilation.
|
||||
|
||||
You can run the `nix build -L .#static-x86_64-linux-musl-all-features` or
|
||||
`nix build -L .#static-aarch64-linux-musl-all-features` commands based
|
||||
on architecture to cross-compile the necessary static binary located at
|
||||
`result/bin/conduwuit`. This is reproducible with the static binaries produced
|
||||
in our CI.
|
||||
|
||||
## Adding a Continuwuity user
|
||||
|
||||
While Continuwuity can run as any user, it is better to use dedicated users for
|
||||
@@ -133,11 +128,13 @@ ## Setting up a systemd service
|
||||
ReadWritePaths=/path/to/custom/database/path
|
||||
```
|
||||
|
||||
|
||||
### Example systemd Unit File
|
||||
|
||||
<details>
|
||||
<summary>Click to expand systemd unit file (conduwuit.service)</summary>
|
||||
|
||||
|
||||
```ini file="../../pkg/conduwuit.service"
|
||||
|
||||
```
|
||||
@@ -205,27 +202,23 @@ ### Other Reverse Proxies
|
||||
As we prefer our users to use Caddy, we do not provide configuration files for other proxies.
|
||||
|
||||
You will need to reverse proxy everything under the following routes:
|
||||
|
||||
- `/_matrix/` - core Matrix C-S and S-S APIs
|
||||
- `/_conduwuit/` and/or `/_continuwuity/` - ad-hoc Continuwuity routes such as `/local_user_count` and
|
||||
`/server_version`
|
||||
`/server_version`
|
||||
|
||||
You can optionally reverse proxy the following individual routes:
|
||||
|
||||
- `/.well-known/matrix/client` and `/.well-known/matrix/server` if using
|
||||
Continuwuity to perform delegation (see the `[global.well_known]` config section)
|
||||
Continuwuity to perform delegation (see the `[global.well_known]` config section)
|
||||
- `/.well-known/matrix/support` if using Continuwuity to send the homeserver admin
|
||||
contact and support page (formerly known as MSC1929)
|
||||
contact and support page (formerly known as MSC1929)
|
||||
- `/` if you would like to see `hewwo from conduwuit woof!` at the root
|
||||
|
||||
See the following spec pages for more details on these files:
|
||||
|
||||
- [`/.well-known/matrix/server`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixserver)
|
||||
- [`/.well-known/matrix/client`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient)
|
||||
- [`/.well-known/matrix/support`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixsupport)
|
||||
|
||||
Examples of delegation:
|
||||
|
||||
- https://continuwuity.org/.well-known/matrix/server
|
||||
- https://continuwuity.org/.well-known/matrix/client
|
||||
- https://ellis.link/.well-known/matrix/server
|
||||
@@ -239,7 +232,6 @@ ### Other Reverse Proxies
|
||||
If using Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent httpd from interfering with the `X-Matrix` header (note that Apache is not ideal as a general reverse proxy, so we discourage using it if alternatives are available).
|
||||
|
||||
If using Nginx, you need to pass the request URI to Continuwuity using `$request_uri`, like this:
|
||||
|
||||
- `proxy_pass http://127.0.0.1:6167$request_uri;`
|
||||
- `proxy_pass http://127.0.0.1:6167;`
|
||||
|
||||
@@ -279,17 +271,17 @@ # If federation is enabled
|
||||
```
|
||||
|
||||
- To check if your server can communicate with other homeservers, use the
|
||||
[Matrix Federation Tester](https://federationtester.mtrnord.blog/). If you can
|
||||
register but cannot join federated rooms, check your configuration and verify
|
||||
that port 8448 is open and forwarded correctly.
|
||||
[Matrix Federation Tester](https://federationtester.mtrnord.blog/). If you can
|
||||
register but cannot join federated rooms, check your configuration and verify
|
||||
that port 8448 is open and forwarded correctly.
|
||||
|
||||
## What's next?
|
||||
# What's next?
|
||||
|
||||
### Audio/Video calls
|
||||
## Audio/Video calls
|
||||
|
||||
For Audio/Video call functionality see the [Calls](../calls.md) page.
|
||||
|
||||
### Appservices
|
||||
## Appservices
|
||||
|
||||
If you want to set up an appservice, take a look at the [Appservice
|
||||
Guide](../appservices.md).
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
# Continuwuity for NixOS
|
||||
|
||||
## Nix package
|
||||
NixOS packages Continuwuity as `matrix-continuwuity`. This package includes both the Continuwuity software and a dedicated NixOS module for configuration and deployment.
|
||||
|
||||
You can get a Nix package for Continuwuity from the following sources:
|
||||
## Installation methods
|
||||
|
||||
- Directly from Nixpkgs: `pkgs.matrix-continuwuity`
|
||||
- Or, using `continuwuity.packages.${system}.default` from:
|
||||
- The `flake.nix` at the root of the Continuwuity repo, by adding Continuwuity to your flake inputs:
|
||||
You can acquire Continuwuity with Nix (or [Lix][lix]) from these sources:
|
||||
|
||||
```nix
|
||||
inputs.continuwuity.url = "git+https://forgejo.ellis.link/continuwuation/continuwuity";
|
||||
```
|
||||
|
||||
- The `default.nix` at the root of the Continuwuity repo
|
||||
* Directly from Nixpkgs using the official package (`pkgs.matrix-continuwuity`)
|
||||
* The `flake.nix` at the root of the Continuwuity repo
|
||||
* The `default.nix` at the root of the Continuwuity repo
|
||||
|
||||
## NixOS module
|
||||
|
||||
Continuwuity has an official NixOS module that simplifies configuration and deployment. The module is available in Nixpkgs as `services.matrix-continuwuity`.
|
||||
Continuwuity now has an official NixOS module that simplifies configuration and deployment. The module is available in Nixpkgs as `services.matrix-continuwuity` from NixOS 25.05.
|
||||
|
||||
Here's a basic example of how to use the module:
|
||||
|
||||
```nix
|
||||
services.matrix-continuwuity = {
|
||||
enable = true;
|
||||
settings = {
|
||||
global = {
|
||||
server_name = "example.com";
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
# Continuwuity listens on localhost by default,
|
||||
# address and port are handled automatically
|
||||
|
||||
# You can add any further configuration here, e.g.
|
||||
# trusted_servers = [ "matrix.org" ];
|
||||
{
|
||||
services.matrix-continuwuity = {
|
||||
enable = true;
|
||||
settings = {
|
||||
global = {
|
||||
server_name = "example.com";
|
||||
# Listening on localhost by default
|
||||
# address and port are handled automatically
|
||||
allow_registration = false;
|
||||
allow_encryption = true;
|
||||
allow_federation = true;
|
||||
trusted_servers = [ "matrix.org" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Available options
|
||||
@@ -45,30 +45,86 @@ ### Available options
|
||||
- `user`: The user to run Continuwuity as (defaults to "continuwuity")
|
||||
- `group`: The group to run Continuwuity as (defaults to "continuwuity")
|
||||
- `extraEnvironment`: Extra environment variables to pass to the Continuwuity server
|
||||
- `package`: The Continuwuity package to use, defaults to `pkgs.matrix-continuwuity`
|
||||
- You may want to override this to be from our flake, for faster updates and unstable versions:
|
||||
```nix
|
||||
package = inputs.continuwuity.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
```
|
||||
- `admin.enable`: Whether to add the `conduwuit` binary to `PATH` for administration (enabled by default)
|
||||
- `settings`: The Continuwuity configuration
|
||||
- `package`: The Continuwuity package to use
|
||||
- `settings`: The Continuwuity configuration (in TOML format)
|
||||
|
||||
Use the `settings` option to configure Continuwuity itself. See the [example configuration file](../reference/config.mdx) for all available options.
|
||||
|
||||
Settings are automatically translated from Nix to TOML. For example, the following line of Nix:
|
||||
### UNIX sockets
|
||||
|
||||
The NixOS module natively supports UNIX sockets through the `global.unix_socket_path` option. When using UNIX sockets, set `global.address` to `null`:
|
||||
|
||||
```nix
|
||||
settings.global.well_known.client = "https://matrix.example.com";
|
||||
services.matrix-continuwuity = {
|
||||
enable = true;
|
||||
settings = {
|
||||
global = {
|
||||
server_name = "example.com";
|
||||
address = null; # Must be null when using unix_socket_path
|
||||
unix_socket_path = "/run/continuwuity/continuwuity.sock";
|
||||
unix_socket_perms = 660; # Default permissions for the socket
|
||||
# ...
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Would become this equivalent TOML configuration:
|
||||
The module automatically sets the correct `RestrictAddressFamilies` in the systemd service configuration to allow access to UNIX sockets.
|
||||
|
||||
```toml
|
||||
[global.well_known]
|
||||
client = "https://matrix.example.com"
|
||||
### RocksDB database
|
||||
|
||||
Continuwuity exclusively uses RocksDB as its database backend. The system configures the database path automatically to `/var/lib/continuwuity/` and you cannot change it due to the service's reliance on systemd's StateDir.
|
||||
|
||||
If you're migrating from Conduit with SQLite, use this [tool to migrate a Conduit SQLite database to RocksDB](https://github.com/ShadowJonathan/conduit_toolbox/).
|
||||
|
||||
### jemalloc and hardened profile
|
||||
|
||||
Continuwuity uses jemalloc by default. This may interfere with the [`hardened.nix` profile][hardened.nix] because it uses `scudo` by default. Either disable/hide `scudo` from Continuwuity or disable jemalloc like this:
|
||||
|
||||
```nix
|
||||
services.matrix-continuwuity = {
|
||||
enable = true;
|
||||
package = pkgs.matrix-continuwuity.override {
|
||||
enableJemalloc = false;
|
||||
};
|
||||
# ...
|
||||
};
|
||||
```
|
||||
|
||||
## Upgrading from Conduit
|
||||
|
||||
If you previously used Conduit with the `services.matrix-conduit` module:
|
||||
|
||||
1. Ensure your Conduit uses the RocksDB backend, or migrate from SQLite using the [migration tool](https://github.com/ShadowJonathan/conduit_toolbox/)
|
||||
2. Switch to the new module by changing `services.matrix-conduit` to `services.matrix-continuwuity` in your configuration
|
||||
3. Update any custom configuration to match the new module's structure
|
||||
|
||||
## Reverse proxy configuration
|
||||
|
||||
You'll need to set up a reverse proxy (like NGINX or Caddy) to expose Continuwuity to the internet. You can configure your reverse proxy using NixOS options (e.g. `services.caddy`).
|
||||
See the [reverse proxy setup guide](./generic.mdx#setting-up-the-reverse-proxy) for information on correct reverse proxy configuration.
|
||||
You'll need to set up a reverse proxy (like nginx or caddy) to expose Continuwuity to the internet. Configure your reverse proxy to forward requests to `/_matrix` on port 443 and 8448 to your Continuwuity instance.
|
||||
|
||||
Here's an example nginx configuration:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
listen 8448 ssl;
|
||||
listen [::]:8448 ssl;
|
||||
|
||||
server_name example.com;
|
||||
|
||||
# SSL configuration here...
|
||||
|
||||
location /_matrix/ {
|
||||
proxy_pass http://127.0.0.1:6167$request_uri;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[lix]: https://lix.systems/
|
||||
[hardened.nix]: https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/profiles/hardened.nix
|
||||
|
||||
@@ -130,10 +130,6 @@ ## `!admin debug database-files`
|
||||
|
||||
List database files
|
||||
|
||||
## `!admin debug send-test-email`
|
||||
|
||||
Send a test email to the invoking admin's email address
|
||||
|
||||
## `!admin debug tester`
|
||||
|
||||
Developer test stubs
|
||||
|
||||
@@ -133,18 +133,6 @@ ### `!admin query pusher get-pushers`
|
||||
|
||||
Returns all the pushers for the user
|
||||
|
||||
### `!admin query pusher delete-pusher`
|
||||
|
||||
Deletes a specific pusher by ID
|
||||
|
||||
### `!admin query pusher delete-all-user`
|
||||
|
||||
Deletes all pushers for a user
|
||||
|
||||
### `!admin query pusher delete-all-device`
|
||||
|
||||
Deletes all pushers associated with a device ID
|
||||
|
||||
## `!admin query short`
|
||||
|
||||
short service
|
||||
|
||||
@@ -47,11 +47,3 @@ ## `!admin server restart`
|
||||
## `!admin server shutdown`
|
||||
|
||||
Shutdown the server
|
||||
|
||||
## `!admin server list-features`
|
||||
|
||||
List features built into the server
|
||||
|
||||
## `!admin server build-info`
|
||||
|
||||
Build information
|
||||
|
||||
@@ -12,24 +12,6 @@ ## `!admin users reset-password`
|
||||
|
||||
Reset user password
|
||||
|
||||
## `!admin users issue-password-reset-link`
|
||||
|
||||
Issue a self-service password reset link for a user
|
||||
|
||||
## `!admin users get-email`
|
||||
|
||||
Get a user's associated email address
|
||||
|
||||
## `!admin users get-user-by-email`
|
||||
|
||||
Get the user with the given email address
|
||||
|
||||
## `!admin users change-email`
|
||||
|
||||
Update or remove a user's email address.
|
||||
|
||||
If `email` is not supplied, the user's existing address will be removed.
|
||||
|
||||
## `!admin users deactivate`
|
||||
|
||||
Deactivate a user
|
||||
@@ -157,7 +139,3 @@ ## `!admin users force-join-all-local-users`
|
||||
At least 1 server admin must be in the room to reduce abuse.
|
||||
|
||||
Requires the `--yes-i-want-to-do-this` flag.
|
||||
|
||||
## `!admin users reset-push-rules`
|
||||
|
||||
Resets the push-rules (notification settings) of the target user to the server defaults
|
||||
|
||||
48
flake.lock
generated
48
flake.lock
generated
@@ -3,11 +3,11 @@
|
||||
"advisory-db": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1775907537,
|
||||
"narHash": "sha256-vbeLNgmsx1Z6TwnlDV0dKyeBCcon3UpkV9yLr/yc6HM=",
|
||||
"lastModified": 1773786698,
|
||||
"narHash": "sha256-o/J7ZculgwSs1L4H4UFlFZENOXTJzq1X0n71x6oNNvY=",
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"rev": "d99f7b9eb81731bddebf80a355f8be7b2f8b1b28",
|
||||
"rev": "99e9de91bb8b61f06ef234ff84e11f758ecd5384",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -18,11 +18,11 @@
|
||||
},
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1775839657,
|
||||
"narHash": "sha256-SPm9ck7jh3Un9nwPuMGbRU04UroFmOHjLP56T10MOeM=",
|
||||
"lastModified": 1773189535,
|
||||
"narHash": "sha256-E1G/Or6MWeP+L6mpQ0iTFLpzSzlpGrITfU2220Gq47g=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "7cf72d978629469c4bd4206b95c402514c1f6000",
|
||||
"rev": "6fa2fb4cf4a89ba49fc9dd5a3eb6cde99d388269",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -39,11 +39,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775891769,
|
||||
"narHash": "sha256-EOfVlTKw2n8w1uhfh46GS4hEGnQ7oWrIWQfIY6utIkI=",
|
||||
"lastModified": 1773732206,
|
||||
"narHash": "sha256-HKibxaUXyWd4Hs+ZUnwo6XslvaFqFqJh66uL9tphU4Q=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "6fbc54dde15aee725bdc7aae5e478849685d5f56",
|
||||
"rev": "0aa13c1b54063a8d8679b28a5cd357ba98f4a56b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -74,11 +74,11 @@
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775087534,
|
||||
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
|
||||
"lastModified": 1772408722,
|
||||
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
|
||||
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -89,11 +89,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1775710090,
|
||||
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
|
||||
"lastModified": 1773734432,
|
||||
"narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
|
||||
"rev": "cda48547b432e8d3b18b4180ba07473762ec8558",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -105,11 +105,11 @@
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1774748309,
|
||||
"narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=",
|
||||
"lastModified": 1772328832,
|
||||
"narHash": "sha256-e+/T/pmEkLP6BHhYjx6GmwP5ivonQQn0bJdH9YrRB+Q=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "333c4e0545a6da976206c74db8773a1645b5870a",
|
||||
"rev": "c185c7a5e5dd8f9add5b2f8ebeff00888b070742",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -132,11 +132,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1775843361,
|
||||
"narHash": "sha256-j53ZgyDvmYf3Sjh1IPvvTjqa614qUfVQSzj59+MpzkY=",
|
||||
"lastModified": 1773697963,
|
||||
"narHash": "sha256-xdKI77It9PM6eNrCcDZsnP4SKulZwk8VkDgBRVMnCb8=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "9eb97ea96d8400e8957ddd56702e962614296583",
|
||||
"rev": "2993637174252ff60a582fd1f55b9ab52c39db6d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -153,11 +153,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775636079,
|
||||
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
|
||||
"lastModified": 1773297127,
|
||||
"narHash": "sha256-6E/yhXP7Oy/NbXtf1ktzmU8SdVqJQ09HC/48ebEGBpk=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
|
||||
"rev": "71b125cd05fbfd78cab3e070b73544abe24c5016",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
url = "github:edolstra/flake-compat?ref=master";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
outputs =
|
||||
@@ -36,10 +37,10 @@
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
imports = [ ./nix ];
|
||||
systems = [
|
||||
# good support
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
# support untested but theoretically there
|
||||
"aarch64-darwin"
|
||||
"aarch64-linux"
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
107
nix/checks/default.nix
Normal file
107
nix/checks/default.nix
Normal file
@@ -0,0 +1,107 @@
|
||||
{ inputs, ... }:
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
self',
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
uwulib = inputs.self.uwulib.init pkgs;
|
||||
|
||||
rocksdbAllFeatures = self'.packages.rocksdb.override {
|
||||
enableJemalloc = true;
|
||||
};
|
||||
|
||||
commonAttrs = (uwulib.build.commonAttrs { }) // {
|
||||
buildInputs = [
|
||||
pkgs.liburing
|
||||
pkgs.rust-jemalloc-sys-unprefixed
|
||||
rocksdbAllFeatures
|
||||
];
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
# bindgen needs the build platform's libclang. Apparently due to "splicing
|
||||
# weirdness", pkgs.rustPlatform.bindgenHook on its own doesn't quite do the
|
||||
# right thing here.
|
||||
pkgs.rustPlatform.bindgenHook
|
||||
];
|
||||
env = {
|
||||
LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.llvmPackages.libclang.lib ];
|
||||
LD_LIBRARY_PATH = lib.makeLibraryPath [
|
||||
pkgs.liburing
|
||||
pkgs.rust-jemalloc-sys-unprefixed
|
||||
rocksdbAllFeatures
|
||||
];
|
||||
}
|
||||
// uwulib.environment.buildPackageEnv
|
||||
// {
|
||||
ROCKSDB_INCLUDE_DIR = "${rocksdbAllFeatures}/include";
|
||||
ROCKSDB_LIB_DIR = "${rocksdbAllFeatures}/lib";
|
||||
};
|
||||
};
|
||||
cargoArtifacts = self'.packages.continuwuity-all-features-deps;
|
||||
in
|
||||
{
|
||||
# taken from
|
||||
#
|
||||
# https://crane.dev/examples/quick-start.html
|
||||
checks = {
|
||||
continuwuity-all-features-build = self'.packages.continuwuity-all-features-bin;
|
||||
|
||||
continuwuity-all-features-clippy = uwulib.build.craneLibForChecks.cargoClippy (
|
||||
commonAttrs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
cargoClippyExtraArgs = "-- --deny warnings";
|
||||
}
|
||||
);
|
||||
|
||||
continuwuity-all-features-docs = uwulib.build.craneLibForChecks.cargoDoc (
|
||||
commonAttrs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
# This can be commented out or tweaked as necessary, e.g. set to
|
||||
# `--deny rustdoc::broken-intra-doc-links` to only enforce that lint
|
||||
env.RUSTDOCFLAGS = "--deny warnings";
|
||||
}
|
||||
);
|
||||
|
||||
# Check formatting
|
||||
continuwuity-all-features-fmt = uwulib.build.craneLibForChecks.cargoFmt {
|
||||
src = uwulib.build.src;
|
||||
};
|
||||
|
||||
continuwuity-all-features-toml-fmt = uwulib.build.craneLibForChecks.taploFmt {
|
||||
src = pkgs.lib.sources.sourceFilesBySuffices uwulib.build.src [ ".toml" ];
|
||||
# taplo arguments can be further customized below as needed
|
||||
taploExtraArgs = "--config ${inputs.self}/taplo.toml";
|
||||
};
|
||||
|
||||
# Audit dependencies
|
||||
continuwuity-all-features-audit = uwulib.build.craneLibForChecks.cargoAudit {
|
||||
inherit (inputs) advisory-db;
|
||||
src = uwulib.build.src;
|
||||
};
|
||||
|
||||
# Audit licenses
|
||||
continuwuity-all-features-deny = uwulib.build.craneLibForChecks.cargoDeny {
|
||||
src = uwulib.build.src;
|
||||
};
|
||||
|
||||
# Run tests with cargo-nextest
|
||||
# Consider setting `doCheck = false` on `continuwuity-all-features` if you do not want
|
||||
# the tests to run twice
|
||||
continuwuity-all-features-nextest = uwulib.build.craneLibForChecks.cargoNextest (
|
||||
commonAttrs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
partitions = 1;
|
||||
partitionType = "count";
|
||||
cargoNextestPartitionsExtraArgs = "--no-tests=pass";
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{ inputs, ... }:
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
pkgs,
|
||||
self',
|
||||
...
|
||||
}:
|
||||
{
|
||||
_module.args.craneLib = (inputs.crane.mkLib pkgs).overrideToolchain (
|
||||
pkgs: self'.packages.stable-toolchain
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
imports = [
|
||||
./rust.nix
|
||||
./crane.nix
|
||||
./checks
|
||||
./packages
|
||||
./devshell.nix
|
||||
./shells
|
||||
./tests
|
||||
|
||||
./hydra.nix
|
||||
./fmt.nix
|
||||
./rocksdb-updater.nix
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
craneLib,
|
||||
self',
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
# basic nix shell containing all things necessary to build continuwuity in all flavors manually (on x86_64-linux)
|
||||
devShells.default = craneLib.devShell {
|
||||
packages = [
|
||||
self'.packages.rocksdb
|
||||
pkgs.nodejs
|
||||
pkgs.pkg-config
|
||||
]
|
||||
++ lib.optionals pkgs.stdenv.isLinux [
|
||||
pkgs.liburing
|
||||
pkgs.rust-jemalloc-sys-unprefixed
|
||||
];
|
||||
|
||||
env = {
|
||||
LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.llvmPackages.libclang.lib ];
|
||||
LD_LIBRARY_PATH = lib.makeLibraryPath (
|
||||
[
|
||||
pkgs.stdenv.cc.cc.lib
|
||||
]
|
||||
++ lib.optionals pkgs.stdenv.isLinux [
|
||||
pkgs.liburing
|
||||
pkgs.jemalloc
|
||||
]
|
||||
);
|
||||
}
|
||||
// lib.optionalAttrs pkgs.stdenv.isLinux {
|
||||
PKG_CONFIG_PATH = lib.makeSearchPath "lib/pkgconfig" [
|
||||
pkgs.liburing.dev
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
9
nix/hydra.nix
Normal file
9
nix/hydra.nix
Normal file
@@ -0,0 +1,9 @@
|
||||
{ inputs, ... }:
|
||||
let
|
||||
lib = inputs.nixpkgs.lib;
|
||||
in
|
||||
{
|
||||
flake.hydraJobs.packages = builtins.mapAttrs (
|
||||
_name: lib.hydraJob
|
||||
) inputs.self.packages.x86_64-linux;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
self,
|
||||
stdenv,
|
||||
liburing,
|
||||
craneLib,
|
||||
pkg-config,
|
||||
callPackage,
|
||||
rustPlatform,
|
||||
cargoExtraArgs ? "",
|
||||
rocksdb ? callPackage ./rocksdb.nix { },
|
||||
}:
|
||||
let
|
||||
# see https://crane.dev/API.html#cranelibfiltercargosources
|
||||
# we need to keep the `web` directory which would be filtered out by the regular source filtering function
|
||||
# https://crane.dev/API.html#cranelibcleancargosource
|
||||
isWebTemplate = path: _type: builtins.match ".*(src/(web|service)|docs).*" path != null;
|
||||
isRust = craneLib.filterCargoSources;
|
||||
isNix = path: _type: builtins.match ".+/nix.*" path != null;
|
||||
webOrRustNotNix = p: t: !(isNix p t) && (isWebTemplate p t || isRust p t);
|
||||
|
||||
src = lib.cleanSourceWith {
|
||||
src = self;
|
||||
filter = webOrRustNotNix;
|
||||
name = "source";
|
||||
};
|
||||
|
||||
attrs = {
|
||||
inherit src;
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
rustPlatform.bindgenHook
|
||||
];
|
||||
buildInputs = lib.optionals stdenv.hostPlatform.isLinux [ liburing ];
|
||||
env = {
|
||||
ROCKSDB_INCLUDE_DIR = "${rocksdb}/include";
|
||||
ROCKSDB_LIB_DIR = "${rocksdb}/lib";
|
||||
};
|
||||
};
|
||||
in
|
||||
craneLib.buildPackage (
|
||||
lib.recursiveUpdate attrs {
|
||||
inherit cargoExtraArgs;
|
||||
cargoArtifacts = craneLib.buildDepsOnly attrs;
|
||||
|
||||
# Needed to make continuwuity link to rocksdb
|
||||
postFixup = lib.optionalString stdenv.hostPlatform.isLinux ''
|
||||
old_rpath="$(patchelf --print-rpath $out/bin/conduwuit)"
|
||||
extra_rpath="${
|
||||
lib.makeLibraryPath [
|
||||
rocksdb
|
||||
]
|
||||
}"
|
||||
|
||||
patchelf --set-rpath "$old_rpath:$extra_rpath" $out/bin/conduwuit
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "A community-driven Matrix homeserver in Rust";
|
||||
mainProgram = "conduwuit";
|
||||
platforms = lib.platforms.all;
|
||||
maintainers = with lib.maintainers; [ quadradical ];
|
||||
};
|
||||
}
|
||||
)
|
||||
59
nix/packages/continuwuity/default.nix
Normal file
59
nix/packages/continuwuity/default.nix
Normal file
@@ -0,0 +1,59 @@
|
||||
{ inputs, ... }:
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
self',
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
uwulib = inputs.self.uwulib.init pkgs;
|
||||
in
|
||||
{
|
||||
packages =
|
||||
lib.pipe
|
||||
[
|
||||
# this is the default variant
|
||||
{
|
||||
variantName = "default";
|
||||
commonAttrsArgs.profile = "release";
|
||||
rocksdb = self'.packages.rocksdb;
|
||||
features = { };
|
||||
}
|
||||
# this is the variant with all features enabled (liburing + jemalloc)
|
||||
{
|
||||
variantName = "all-features";
|
||||
commonAttrsArgs.profile = "release";
|
||||
rocksdb = self'.packages.rocksdb.override {
|
||||
enableJemalloc = true;
|
||||
};
|
||||
features = {
|
||||
enabledFeatures = "all";
|
||||
disabledFeatures = uwulib.features.defaultDisabledFeatures ++ [ "bindgen-static" ];
|
||||
};
|
||||
}
|
||||
]
|
||||
[
|
||||
(builtins.map (cfg: rec {
|
||||
deps = {
|
||||
name = "continuwuity-${cfg.variantName}-deps";
|
||||
value = uwulib.build.buildDeps {
|
||||
features = uwulib.features.calcFeatures cfg.features;
|
||||
inherit (cfg) commonAttrsArgs rocksdb;
|
||||
};
|
||||
};
|
||||
bin = {
|
||||
name = "continuwuity-${cfg.variantName}-bin";
|
||||
value = uwulib.build.buildPackage {
|
||||
deps = self'.packages.${deps.name};
|
||||
features = uwulib.features.calcFeatures cfg.features;
|
||||
inherit (cfg) commonAttrsArgs rocksdb;
|
||||
};
|
||||
};
|
||||
}))
|
||||
(builtins.concatMap builtins.attrValues)
|
||||
builtins.listToAttrs
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
{
|
||||
self,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
./continuwuity
|
||||
./rocksdb
|
||||
./rust.nix
|
||||
./uwulib
|
||||
];
|
||||
|
||||
perSystem =
|
||||
{ self', ... }:
|
||||
{
|
||||
pkgs,
|
||||
craneLib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
packages = {
|
||||
rocksdb = pkgs.callPackage ./rocksdb.nix { };
|
||||
default = pkgs.callPackage ./continuwuity.nix { inherit self craneLib; };
|
||||
};
|
||||
packages.default = self'.packages.continuwuity-default-bin;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
stdenv,
|
||||
rocksdb,
|
||||
fetchFromGitea,
|
||||
rust-jemalloc-sys-unprefixed,
|
||||
...
|
||||
}:
|
||||
(rocksdb.override {
|
||||
# rocksdb fails to build with prefixed jemalloc, which is required on
|
||||
# darwin due to [1]. In this case, fall back to building rocksdb with
|
||||
# libc malloc. This should not cause conflicts, because all of the
|
||||
# jemalloc symbols are prefixed.
|
||||
#
|
||||
# [1]: https://github.com/tikv/jemallocator/blob/ab0676d77e81268cd09b059260c75b38dbef2d51/jemalloc-sys/src/env.rs#L17
|
||||
jemalloc = rust-jemalloc-sys-unprefixed;
|
||||
enableJemalloc = stdenv.hostPlatform.isLinux;
|
||||
}).overrideAttrs
|
||||
({
|
||||
version = "continuwuity-v0.5.0-unstable-2026-03-27";
|
||||
src = fetchFromGitea {
|
||||
domain = "forgejo.ellis.link";
|
||||
owner = "continuwuation";
|
||||
repo = "rocksdb";
|
||||
rev = "463f47afceebfe088f6922420265546bd237f249";
|
||||
hash = "sha256-1ef75IDMs5Hba4VWEyXPJb02JyShy5k4gJfzGDhopRk=";
|
||||
};
|
||||
|
||||
# We have this already at https://forgejo.ellis.link/continuwuation/rocksdb/commit/a935c0273e1ba44eacf88ce3685a9b9831486155
|
||||
# Unsetting `patches` so we don't have to revert it and make this nix exclusive
|
||||
patches = [ ];
|
||||
|
||||
# Unset postPatch, as our version override breaks version-specific sed calls in the original package
|
||||
postPatch = "";
|
||||
})
|
||||
12
nix/packages/rocksdb/default.nix
Normal file
12
nix/packages/rocksdb/default.nix
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
packages = {
|
||||
rocksdb = pkgs.callPackage ./package.nix { };
|
||||
};
|
||||
};
|
||||
}
|
||||
87
nix/packages/rocksdb/package.nix
Normal file
87
nix/packages/rocksdb/package.nix
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
|
||||
rocksdb,
|
||||
liburing,
|
||||
rust-jemalloc-sys-unprefixed,
|
||||
|
||||
enableJemalloc ? false,
|
||||
|
||||
fetchFromGitea,
|
||||
|
||||
...
|
||||
}:
|
||||
let
|
||||
notDarwin = !stdenv.hostPlatform.isDarwin;
|
||||
in
|
||||
(rocksdb.override {
|
||||
# Override the liburing input for the build with our own so
|
||||
# we have it built with the library flag
|
||||
inherit liburing;
|
||||
jemalloc = rust-jemalloc-sys-unprefixed;
|
||||
|
||||
# rocksdb fails to build with prefixed jemalloc, which is required on
|
||||
# darwin due to [1]. In this case, fall back to building rocksdb with
|
||||
# libc malloc. This should not cause conflicts, because all of the
|
||||
# jemalloc symbols are prefixed.
|
||||
#
|
||||
# [1]: https://github.com/tikv/jemallocator/blob/ab0676d77e81268cd09b059260c75b38dbef2d51/jemalloc-sys/src/env.rs#L17
|
||||
enableJemalloc = enableJemalloc && notDarwin;
|
||||
|
||||
# for some reason enableLiburing in nixpkgs rocksdb is default true
|
||||
# which breaks Darwin entirely
|
||||
enableLiburing = notDarwin;
|
||||
}).overrideAttrs
|
||||
(old: {
|
||||
src = fetchFromGitea {
|
||||
domain = "forgejo.ellis.link";
|
||||
owner = "continuwuation";
|
||||
repo = "rocksdb";
|
||||
rev = "10.5.fb";
|
||||
sha256 = "sha256-X4ApGLkHF9ceBtBg77dimEpu720I79ffLoyPa8JMHaU=";
|
||||
};
|
||||
version = "10.5.fb";
|
||||
cmakeFlags =
|
||||
lib.subtractLists (builtins.map (flag: lib.cmakeBool flag true) [
|
||||
# No real reason to have snappy or zlib, no one uses this
|
||||
"WITH_SNAPPY"
|
||||
"ZLIB"
|
||||
"WITH_ZLIB"
|
||||
# We don't need to use ldb or sst_dump (core_tools)
|
||||
"WITH_CORE_TOOLS"
|
||||
# We don't need to build rocksdb tests
|
||||
"WITH_TESTS"
|
||||
# We use rust-rocksdb via C interface and don't need C++ RTTI
|
||||
"USE_RTTI"
|
||||
# This doesn't exist in RocksDB, and USE_SSE is deprecated for
|
||||
# PORTABLE=$(march)
|
||||
"FORCE_SSE42"
|
||||
]) old.cmakeFlags
|
||||
++ (builtins.map (flag: lib.cmakeBool flag false) [
|
||||
# No real reason to have snappy, no one uses this
|
||||
"WITH_SNAPPY"
|
||||
"ZLIB"
|
||||
"WITH_ZLIB"
|
||||
# We don't need to use ldb or sst_dump (core_tools)
|
||||
"WITH_CORE_TOOLS"
|
||||
# We don't need trace tools
|
||||
"WITH_TRACE_TOOLS"
|
||||
# We don't need to build rocksdb tests
|
||||
"WITH_TESTS"
|
||||
# We use rust-rocksdb via C interface and don't need C++ RTTI
|
||||
"USE_RTTI"
|
||||
]);
|
||||
|
||||
enableLiburing = notDarwin;
|
||||
|
||||
# outputs has "tools" which we don't need or use
|
||||
outputs = [ "out" ];
|
||||
|
||||
# preInstall hooks has stuff for messing with ldb/sst_dump which we don't need or use
|
||||
preInstall = "";
|
||||
|
||||
# We have this already at https://forgejo.ellis.link/continuwuation/rocksdb/commit/a935c0273e1ba44eacf88ce3685a9b9831486155
|
||||
# Unsetting `patches` so we don't have to revert it and make this nix exclusive
|
||||
patches = [ ];
|
||||
})
|
||||
@@ -4,7 +4,6 @@
|
||||
{
|
||||
system,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
@@ -12,7 +11,7 @@
|
||||
let
|
||||
fnx = inputs.fenix.packages.${system};
|
||||
|
||||
stable-toolchain = fnx.fromToolchainFile {
|
||||
stable = fnx.fromToolchainFile {
|
||||
file = inputs.self + "/rust-toolchain.toml";
|
||||
|
||||
# See also `rust-toolchain.toml`
|
||||
@@ -20,10 +19,11 @@
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit stable-toolchain;
|
||||
|
||||
# used for building nix stuff (doesn't include rustfmt overhead)
|
||||
build-toolchain = stable;
|
||||
# used for dev shells
|
||||
dev-toolchain = fnx.combine [
|
||||
stable-toolchain
|
||||
stable
|
||||
# use the nightly rustfmt because we use nightly features
|
||||
fnx.complete.rustfmt
|
||||
];
|
||||
122
nix/packages/uwulib/build.nix
Normal file
122
nix/packages/uwulib/build.nix
Normal file
@@ -0,0 +1,122 @@
|
||||
args@{ pkgs, inputs, ... }:
|
||||
let
|
||||
inherit (pkgs) lib;
|
||||
uwuenv = import ./environment.nix args;
|
||||
selfpkgs = inputs.self.packages.${pkgs.stdenv.system};
|
||||
in
|
||||
rec {
|
||||
# basic, very minimal instance of the crane library with a minimal rust toolchain
|
||||
craneLib = (inputs.crane.mkLib pkgs).overrideToolchain (_: selfpkgs.build-toolchain);
|
||||
# the checks require more rust toolchain components, hence we have this separate instance of the crane library
|
||||
craneLibForChecks = (inputs.crane.mkLib pkgs).overrideToolchain (_: selfpkgs.dev-toolchain);
|
||||
|
||||
# meta information (name, version, etc) of the rust crate based on the Cargo.toml
|
||||
crateInfo = craneLib.crateNameFromCargoToml { cargoToml = "${inputs.self}/Cargo.toml"; };
|
||||
|
||||
src =
|
||||
let
|
||||
# see https://crane.dev/API.html#cranelibfiltercargosources
|
||||
#
|
||||
# we need to keep the `web` directory which would be filtered out by the regular source filtering function
|
||||
#
|
||||
# https://crane.dev/API.html#cranelibcleancargosource
|
||||
isWebTemplate = path: _type: builtins.match ".*(src/(web|service)|docs).*" path != null;
|
||||
isRust = craneLib.filterCargoSources;
|
||||
isNix = path: _type: builtins.match ".+/nix.*" path != null;
|
||||
webOrRustNotNix = p: t: !(isNix p t) && (isWebTemplate p t || isRust p t);
|
||||
in
|
||||
lib.cleanSourceWith {
|
||||
src = inputs.self;
|
||||
filter = webOrRustNotNix;
|
||||
name = "source";
|
||||
};
|
||||
|
||||
# common attrs that are shared between building continuwuity's deps and the package itself
|
||||
commonAttrs =
|
||||
{
|
||||
profile ? "dev",
|
||||
...
|
||||
}:
|
||||
{
|
||||
inherit (crateInfo)
|
||||
pname
|
||||
version
|
||||
;
|
||||
inherit src;
|
||||
|
||||
# this prevents unnecessary rebuilds
|
||||
strictDeps = true;
|
||||
|
||||
dontStrip = profile == "dev" || profile == "test";
|
||||
dontPatchELF = profile == "dev" || profile == "test";
|
||||
|
||||
doCheck = true;
|
||||
|
||||
nativeBuildInputs = [
|
||||
# bindgen needs the build platform's libclang. Apparently due to "splicing
|
||||
# weirdness", pkgs.rustPlatform.bindgenHook on its own doesn't quite do the
|
||||
# right thing here.
|
||||
pkgs.rustPlatform.bindgenHook
|
||||
];
|
||||
};
|
||||
|
||||
makeRocksDBEnv =
|
||||
{ rocksdb }:
|
||||
{
|
||||
ROCKSDB_INCLUDE_DIR = "${rocksdb}/include";
|
||||
ROCKSDB_LIB_DIR = "${rocksdb}/lib";
|
||||
};
|
||||
|
||||
# function that builds the continuwuity dependencies derivation
|
||||
buildDeps =
|
||||
{
|
||||
rocksdb,
|
||||
features,
|
||||
commonAttrsArgs,
|
||||
}:
|
||||
craneLib.buildDepsOnly (
|
||||
(commonAttrs commonAttrsArgs)
|
||||
// {
|
||||
env = uwuenv.buildDepsOnlyEnv
|
||||
// (makeRocksDBEnv { inherit rocksdb; })
|
||||
// {
|
||||
# required since we started using unstable reqwest apparently ... otherwise the all-features build will fail
|
||||
RUSTFLAGS = "--cfg reqwest_unstable";
|
||||
};
|
||||
inherit (features) cargoExtraArgs;
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
# function that builds the continuwuity package
|
||||
buildPackage =
|
||||
{
|
||||
deps,
|
||||
rocksdb,
|
||||
features,
|
||||
commonAttrsArgs,
|
||||
}:
|
||||
let
|
||||
rocksdbEnv = makeRocksDBEnv { inherit rocksdb; };
|
||||
in
|
||||
craneLib.buildPackage (
|
||||
(commonAttrs commonAttrsArgs)
|
||||
// {
|
||||
postFixup = ''
|
||||
patchelf --set-rpath "$(${pkgs.patchelf}/bin/patchelf --print-rpath $out/bin/${crateInfo.pname}):${rocksdb}/lib" $out/bin/${crateInfo.pname}
|
||||
'';
|
||||
cargoArtifacts = deps;
|
||||
doCheck = true;
|
||||
env =
|
||||
uwuenv.buildPackageEnv
|
||||
// rocksdbEnv
|
||||
// {
|
||||
# required since we started using unstable reqwest apparently ... otherwise the all-features build will fail
|
||||
RUSTFLAGS = "--cfg reqwest_unstable";
|
||||
};
|
||||
passthru.env = uwuenv.buildPackageEnv // rocksdbEnv;
|
||||
meta.mainProgram = crateInfo.pname;
|
||||
inherit (features) cargoExtraArgs;
|
||||
}
|
||||
);
|
||||
}
|
||||
10
nix/packages/uwulib/default.nix
Normal file
10
nix/packages/uwulib/default.nix
Normal file
@@ -0,0 +1,10 @@
|
||||
{ inputs, ... }:
|
||||
{
|
||||
flake.uwulib = {
|
||||
init = pkgs: {
|
||||
features = import ./features.nix { inherit pkgs inputs; };
|
||||
environment = import ./environment.nix { inherit pkgs inputs; };
|
||||
build = import ./build.nix { inherit pkgs inputs; };
|
||||
};
|
||||
};
|
||||
}
|
||||
18
nix/packages/uwulib/environment.nix
Normal file
18
nix/packages/uwulib/environment.nix
Normal file
@@ -0,0 +1,18 @@
|
||||
args@{ pkgs, inputs, ... }:
|
||||
let
|
||||
uwubuild = import ./build.nix args;
|
||||
in
|
||||
rec {
|
||||
buildDepsOnlyEnv = {
|
||||
# https://crane.dev/faq/rebuilds-bindgen.html
|
||||
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
|
||||
CARGO_PROFILE = "release";
|
||||
}
|
||||
// uwubuild.craneLib.mkCrossToolchainEnv (p: pkgs.clangStdenv);
|
||||
|
||||
buildPackageEnv = {
|
||||
GIT_COMMIT_HASH = inputs.self.rev or inputs.self.dirtyRev or "";
|
||||
GIT_COMMIT_HASH_SHORT = inputs.self.shortRev or inputs.self.dirtyShortRev or "";
|
||||
}
|
||||
// buildDepsOnlyEnv;
|
||||
}
|
||||
77
nix/packages/uwulib/features.nix
Normal file
77
nix/packages/uwulib/features.nix
Normal file
@@ -0,0 +1,77 @@
|
||||
{ pkgs, inputs, ... }:
|
||||
let
|
||||
inherit (pkgs) lib;
|
||||
in
|
||||
rec {
|
||||
defaultDisabledFeatures = [
|
||||
# dont include experimental features
|
||||
"experimental"
|
||||
# jemalloc profiling/stats features are expensive and shouldn't
|
||||
# be expected on non-debug builds.
|
||||
"jemalloc_prof"
|
||||
"jemalloc_stats"
|
||||
# this is non-functional on nix for some reason
|
||||
"hardened_malloc"
|
||||
# conduwuit_mods is a development-only hot reload feature
|
||||
"conduwuit_mods"
|
||||
# we don't want to enable this feature set by default but be more specific about it
|
||||
"full"
|
||||
];
|
||||
# We perform default-feature unification in nix, because some of the dependencies
|
||||
# on the nix side depend on feature values.
|
||||
calcFeatures =
|
||||
{
|
||||
tomlPath ? "${inputs.self}/src/main",
|
||||
# either a list of feature names or a string "all" which enables all non-default features
|
||||
enabledFeatures ? [ ],
|
||||
disabledFeatures ? defaultDisabledFeatures,
|
||||
default_features ? true,
|
||||
disable_release_max_log_level ? false,
|
||||
}:
|
||||
let
|
||||
# simple helper to get the contents of a Cargo.toml file in a nix format
|
||||
getToml = path: lib.importTOML "${path}/Cargo.toml";
|
||||
|
||||
# get all the features except for the default features
|
||||
allFeatures = lib.pipe tomlPath [
|
||||
getToml
|
||||
(manifest: manifest.features)
|
||||
lib.attrNames
|
||||
(lib.remove "default")
|
||||
];
|
||||
|
||||
# get just the default enabled features
|
||||
allDefaultFeatures = lib.pipe tomlPath [
|
||||
getToml
|
||||
(manifest: manifest.features.default)
|
||||
];
|
||||
|
||||
# depending on the value of enabledFeatures choose just a set or all non-default features
|
||||
#
|
||||
# - [ list of features ] -> choose exactly the features listed
|
||||
# - "all" -> choose all non-default features
|
||||
additionalFeatures = if enabledFeatures == "all" then allFeatures else enabledFeatures;
|
||||
|
||||
# unification with default features (if enabled)
|
||||
features = lib.unique (additionalFeatures ++ lib.optionals default_features allDefaultFeatures);
|
||||
|
||||
# prepare the features that are subtracted from the set
|
||||
disabledFeatures' =
|
||||
disabledFeatures ++ lib.optionals disable_release_max_log_level [ "release_max_log_level" ];
|
||||
|
||||
# construct the final feature set
|
||||
finalFeatures = lib.subtractLists disabledFeatures' features;
|
||||
in
|
||||
{
|
||||
# final feature set, useful for querying it
|
||||
features = finalFeatures;
|
||||
|
||||
# crane flag with the relevant features
|
||||
cargoExtraArgs = builtins.concatStringsSep " " [
|
||||
"--no-default-features"
|
||||
"--locked"
|
||||
(lib.optionalString (finalFeatures != [ ]) "--features")
|
||||
(builtins.concatStringsSep "," finalFeatures)
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
perSystem =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
apps.update-rocksdb = {
|
||||
type = "app";
|
||||
program = pkgs.writeShellApplication {
|
||||
name = "update-rocksdb";
|
||||
runtimeInputs = [ pkgs.nix-update ];
|
||||
text = "nix-update rocksdb -F --version branch";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
28
nix/shells/default.nix
Normal file
28
nix/shells/default.nix
Normal file
@@ -0,0 +1,28 @@
|
||||
{ inputs, ... }:
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
self',
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
uwulib = inputs.self.uwulib.init pkgs;
|
||||
rocksdbAllFeatures = self'.packages.rocksdb.override {
|
||||
enableJemalloc = true;
|
||||
};
|
||||
in
|
||||
{
|
||||
# basic nix shell containing all things necessary to build continuwuity in all flavors manually (on x86_64-linux)
|
||||
devShells.default = uwulib.build.craneLib.devShell {
|
||||
packages = [
|
||||
pkgs.pkg-config
|
||||
pkgs.liburing
|
||||
pkgs.rust-jemalloc-sys-unprefixed
|
||||
rocksdbAllFeatures
|
||||
];
|
||||
env.LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.llvmPackages.libclang.lib ];
|
||||
};
|
||||
};
|
||||
}
|
||||
150
nix/tests/default.nix
Normal file
150
nix/tests/default.nix
Normal file
@@ -0,0 +1,150 @@
|
||||
{
|
||||
perSystem =
|
||||
{
|
||||
self',
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
baseTestScript =
|
||||
pkgs.writers.writePython3Bin "do_test" { libraries = [ pkgs.python3Packages.matrix-nio ]; }
|
||||
''
|
||||
import asyncio
|
||||
import nio
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# Connect to continuwuity
|
||||
client = nio.AsyncClient("http://continuwuity:6167", "alice")
|
||||
|
||||
# Register as user alice
|
||||
response = await client.register("alice", "my-secret-password")
|
||||
|
||||
# Log in as user alice
|
||||
response = await client.login("my-secret-password")
|
||||
|
||||
# Create a new room
|
||||
response = await client.room_create(federate=False)
|
||||
print("Matrix room create response:", response)
|
||||
assert isinstance(response, nio.RoomCreateResponse)
|
||||
room_id = response.room_id
|
||||
|
||||
# Join the room
|
||||
response = await client.join(room_id)
|
||||
print("Matrix join response:", response)
|
||||
assert isinstance(response, nio.JoinResponse)
|
||||
|
||||
# Send a message to the room
|
||||
response = await client.room_send(
|
||||
room_id=room_id,
|
||||
message_type="m.room.message",
|
||||
content={
|
||||
"msgtype": "m.text",
|
||||
"body": "Hello continuwuity!"
|
||||
}
|
||||
)
|
||||
print("Matrix room send response:", response)
|
||||
assert isinstance(response, nio.RoomSendResponse)
|
||||
|
||||
# Sync responses
|
||||
response = await client.sync(timeout=30000)
|
||||
print("Matrix sync response:", response)
|
||||
assert isinstance(response, nio.SyncResponse)
|
||||
|
||||
# Check the message was received by continuwuity
|
||||
last_message = response.rooms.join[room_id].timeline.events[-1].body
|
||||
assert last_message == "Hello continuwuity!"
|
||||
|
||||
# Leave the room
|
||||
response = await client.room_leave(room_id)
|
||||
print("Matrix room leave response:", response)
|
||||
assert isinstance(response, nio.RoomLeaveResponse)
|
||||
|
||||
# Close the client
|
||||
await client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
'';
|
||||
in
|
||||
{
|
||||
# run some nixos tests as checks
|
||||
checks = lib.pipe self'.packages [
|
||||
# we take all packages (names)
|
||||
builtins.attrNames
|
||||
# we filter out all packages that end with `-bin` (which we are interested in for testing)
|
||||
(builtins.filter (lib.hasSuffix "-bin"))
|
||||
# for each of these binaries we built the basic nixos test
|
||||
#
|
||||
# this test was initially yoinked from
|
||||
#
|
||||
# https://github.com/NixOS/nixpkgs/blob/960ce26339661b1b69c6f12b9063ca51b688615f/nixos/tests/matrix/continuwuity.nix
|
||||
(builtins.concatMap (
|
||||
name:
|
||||
builtins.map
|
||||
(
|
||||
{ config, suffix }:
|
||||
{
|
||||
name = "test-${name}-${suffix}";
|
||||
value = pkgs.testers.runNixOSTest {
|
||||
inherit name;
|
||||
|
||||
nodes = {
|
||||
continuwuity = {
|
||||
services.matrix-continuwuity = {
|
||||
enable = true;
|
||||
package = self'.packages.${name};
|
||||
settings = config;
|
||||
extraEnvironment.RUST_BACKTRACE = "yes";
|
||||
};
|
||||
networking.firewall.allowedTCPPorts = [ 6167 ];
|
||||
};
|
||||
client.environment.systemPackages = [ baseTestScript ];
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
with subtest("start continuwuity"):
|
||||
continuwuity.wait_for_unit("continuwuity.service")
|
||||
continuwuity.wait_for_open_port(6167)
|
||||
|
||||
with subtest("ensure messages can be exchanged"):
|
||||
client.succeed("${lib.getExe baseTestScript} >&2")
|
||||
'';
|
||||
|
||||
};
|
||||
}
|
||||
)
|
||||
[
|
||||
{
|
||||
suffix = "base";
|
||||
config = {
|
||||
global = {
|
||||
server_name = name;
|
||||
address = [ "0.0.0.0" ];
|
||||
allow_registration = true;
|
||||
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
suffix = "with-room-version";
|
||||
config = {
|
||||
global = {
|
||||
server_name = name;
|
||||
address = [ "0.0.0.0" ];
|
||||
allow_registration = true;
|
||||
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true;
|
||||
default_room_version = "12";
|
||||
};
|
||||
};
|
||||
}
|
||||
]
|
||||
))
|
||||
builtins.listToAttrs
|
||||
];
|
||||
};
|
||||
}
|
||||
702
package-lock.json
generated
702
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended", "replacements:all", ":semanticCommitTypeAll(chore)", "helpers:pinGitHubActionDigests"],
|
||||
"extends": ["config:recommended", "replacements:all", ":semanticCommitTypeAll(chore)"],
|
||||
"dependencyDashboard": true,
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"lockFileMaintenance": {
|
||||
@@ -95,16 +95,16 @@
|
||||
}
|
||||
],
|
||||
"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-zA-Z0-9-._]+?) depName=(?<depName>[^\\s]+?)(?: (lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?(?: registryUrl=(?<registryUrl>[^\\s]+?))?\\s+(?:ENV\\s+|ARG\\s+)?[A-Za-z0-9_]+?_VERSION[ =][\"']?(?<currentValue>.+?)[\"']?\\s+(?:(?:ENV\\s+|ARG\\s+)?[A-Za-z0-9_]+?_CHECKSUM[ =][\"']?(?<currentDigest>.+?)[\"']?\\s)?"
|
||||
]
|
||||
}
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Update _VERSION variables in Dockerfiles",
|
||||
"managerFilePatterns": [
|
||||
"/(^|/)([Dd]ocker|[Cc]ontainer)file[^/]*$/",
|
||||
"/(^|/|\\.)([Dd]ocker|[Cc]ontainer)file$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"# renovate: datasource=(?<datasource>[a-zA-Z0-9-._]+?) depName=(?<depName>[^\\s]+?)(?: (lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?(?: registryUrl=(?<registryUrl>[^\\s]+?))?\\s+(?:ENV\\s+|ARG\\s+)?[A-Za-z0-9_]+?_VERSION[ =][\"']?(?<currentValue>.+?)[\"']?\\s+(?:(?:ENV\\s+|ARG\\s+)?[A-Za-z0-9_]+?_CHECKSUM[ =][\"']?(?<currentDigest>.+?)[\"']?\\s)?"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name = "conduwuit_admin"
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
@@ -80,9 +79,7 @@ conduwuit-database.workspace = true
|
||||
conduwuit-macros.workspace = true
|
||||
conduwuit-service.workspace = true
|
||||
const-str.workspace = true
|
||||
ctor.workspace = true
|
||||
futures.workspace = true
|
||||
lettre.workspace = true
|
||||
log.workspace = true
|
||||
ruma.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
warn,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||
use lettre::message::Mailbox;
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
|
||||
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId,
|
||||
@@ -877,31 +876,3 @@ pub(super) async fn trim_memory(&self) -> Result {
|
||||
|
||||
writeln!(self, "done").await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn send_test_email(&self) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
let mailer = self.services.mailer.expect_mailer()?;
|
||||
let Some(sender) = self.sender else {
|
||||
return Err!("No sender user provided in context");
|
||||
};
|
||||
|
||||
let Some(email) = self
|
||||
.services
|
||||
.threepid
|
||||
.get_email_for_localpart(sender.localpart())
|
||||
.await
|
||||
else {
|
||||
return Err!("{} has no associated email address", sender);
|
||||
};
|
||||
|
||||
mailer
|
||||
.send(Mailbox::new(None, email.clone()), service::mailer::messages::Test)
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!("Test email successfully sent to {email}"))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -225,9 +225,6 @@ pub enum DebugCommand {
|
||||
level: Option<i32>,
|
||||
},
|
||||
|
||||
/// Send a test email to the invoking admin's email address
|
||||
SendTestEmail,
|
||||
|
||||
/// Developer test stubs
|
||||
#[command(subcommand)]
|
||||
#[allow(non_snake_case)]
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
#![allow(clippy::enum_glob_use)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
conduwuit_macros::introspect_crate! {}
|
||||
|
||||
pub(crate) mod admin;
|
||||
pub(crate) mod context;
|
||||
pub(crate) mod processor;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
use clap::Subcommand;
|
||||
use conduwuit::{
|
||||
Result,
|
||||
utils::{IterStream, stream::BroadbandExt},
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruma::{OwnedDeviceId, OwnedUserId};
|
||||
use conduwuit::Result;
|
||||
use ruma::OwnedUserId;
|
||||
|
||||
use crate::Context;
|
||||
|
||||
@@ -15,23 +11,6 @@ pub enum PusherCommand {
|
||||
/// Full user ID
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
/// Deletes a specific pusher by ID
|
||||
DeletePusher {
|
||||
user_id: OwnedUserId,
|
||||
pusher_id: String,
|
||||
},
|
||||
|
||||
/// Deletes all pushers for a user
|
||||
DeleteAllUser {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
/// Deletes all pushers associated with a device ID
|
||||
DeleteAllDevice {
|
||||
user_id: OwnedUserId,
|
||||
device_id: OwnedDeviceId,
|
||||
},
|
||||
}
|
||||
|
||||
pub(super) async fn process(subcommand: PusherCommand, context: &Context<'_>) -> Result {
|
||||
@@ -45,51 +24,6 @@ pub(super) async fn process(subcommand: PusherCommand, context: &Context<'_>) ->
|
||||
|
||||
write!(context, "Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")
|
||||
},
|
||||
| PusherCommand::DeletePusher { user_id, pusher_id } => {
|
||||
services.pusher.delete_pusher(&user_id, &pusher_id).await;
|
||||
write!(context, "Deleted pusher {pusher_id} for {user_id}.")
|
||||
},
|
||||
| PusherCommand::DeleteAllUser { user_id } => {
|
||||
let pushers = services
|
||||
.pusher
|
||||
.get_pushkeys(&user_id)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let pusher_count = pushers.len();
|
||||
pushers
|
||||
.stream()
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(&user_id, pushkey).await;
|
||||
})
|
||||
.await;
|
||||
write!(context, "Deleted {pusher_count} pushers for {user_id}.")
|
||||
},
|
||||
| PusherCommand::DeleteAllDevice { user_id, device_id } => {
|
||||
let pushers = services
|
||||
.pusher
|
||||
.get_pushkeys(&user_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.broad_filter_map(async |pushkey| {
|
||||
services
|
||||
.pusher
|
||||
.get_pusher_device(&pushkey)
|
||||
.await
|
||||
.ok()
|
||||
.as_ref()
|
||||
.is_some_and(|pusher_device| pusher_device == &device_id)
|
||||
.then_some(pushkey)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let pusher_count = pushers.len();
|
||||
pushers
|
||||
.stream()
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(&user_id, &pushkey).await;
|
||||
})
|
||||
.await;
|
||||
write!(context, "Deleted {pusher_count} pushers for {device_id}.")
|
||||
},
|
||||
}
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{fmt::Write, path::PathBuf, sync::Arc};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use conduwuit::{
|
||||
Err, Result,
|
||||
@@ -153,97 +153,3 @@ pub(super) async fn shutdown(&self) -> Result {
|
||||
|
||||
self.write_str("Shutting down server...").await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn list_features(&self) -> Result {
|
||||
let mut enabled_features = conduwuit::info::introspection::ENABLED_FEATURES
|
||||
.lock()
|
||||
.expect("locked")
|
||||
.iter()
|
||||
.flat_map(|(_, f)| f.iter())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
enabled_features.sort_unstable();
|
||||
enabled_features.dedup();
|
||||
|
||||
let mut available_features = conduwuit::build_metadata::WORKSPACE_FEATURES
|
||||
.iter()
|
||||
.flat_map(|(_, f)| f.iter())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
available_features.sort_unstable();
|
||||
available_features.dedup();
|
||||
|
||||
let mut features = String::new();
|
||||
|
||||
for feature in available_features {
|
||||
let active = enabled_features.contains(&feature);
|
||||
let emoji = if active { "✅" } else { "❌" };
|
||||
let remark = if active { "[enabled]" } else { "" };
|
||||
writeln!(features, "{emoji} {feature} {remark}")?;
|
||||
}
|
||||
|
||||
self.write_str(&features).await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn build_info(&self) -> Result {
|
||||
use conduwuit::build_metadata::built;
|
||||
|
||||
let mut info = String::new();
|
||||
|
||||
// Version information
|
||||
writeln!(info, "# Build Information\n")?;
|
||||
writeln!(info, "**Version:** {}", built::PKG_VERSION)?;
|
||||
writeln!(info, "**Package:** {}", built::PKG_NAME)?;
|
||||
writeln!(info, "**Description:** {}", built::PKG_DESCRIPTION)?;
|
||||
|
||||
// Git information
|
||||
writeln!(info, "\n## Git Information\n")?;
|
||||
if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH {
|
||||
writeln!(info, "**Commit Hash:** {hash}")?;
|
||||
}
|
||||
if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH_SHORT {
|
||||
writeln!(info, "**Commit Hash (short):** {hash}")?;
|
||||
}
|
||||
if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_WEB_URL {
|
||||
writeln!(info, "**Repository:** {url}")?;
|
||||
}
|
||||
if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_COMMIT_URL {
|
||||
writeln!(info, "**Commit URL:** {url}")?;
|
||||
}
|
||||
|
||||
// Build environment
|
||||
writeln!(info, "\n## Build Environment\n")?;
|
||||
writeln!(info, "**Profile:** {}", built::PROFILE)?;
|
||||
writeln!(info, "**Optimization Level:** {}", built::OPT_LEVEL)?;
|
||||
writeln!(info, "**Debug:** {}", built::DEBUG)?;
|
||||
writeln!(info, "**Target:** {}", built::TARGET)?;
|
||||
writeln!(info, "**Host:** {}", built::HOST)?;
|
||||
|
||||
// Rust compiler information
|
||||
writeln!(info, "\n## Compiler Information\n")?;
|
||||
writeln!(info, "**Rustc Version:** {}", built::RUSTC_VERSION)?;
|
||||
if !built::RUSTDOC_VERSION.is_empty() {
|
||||
writeln!(info, "**Rustdoc Version:** {}", built::RUSTDOC_VERSION)?;
|
||||
}
|
||||
|
||||
// Target configuration
|
||||
writeln!(info, "\n## Target Configuration\n")?;
|
||||
writeln!(info, "**Architecture:** {}", built::CFG_TARGET_ARCH)?;
|
||||
writeln!(info, "**OS:** {}", built::CFG_OS)?;
|
||||
writeln!(info, "**Family:** {}", built::CFG_FAMILY)?;
|
||||
writeln!(info, "**Endianness:** {}", built::CFG_ENDIAN)?;
|
||||
writeln!(info, "**Pointer Width:** {} bits", built::CFG_POINTER_WIDTH)?;
|
||||
if !built::CFG_ENV.is_empty() {
|
||||
writeln!(info, "**Environment:** {}", built::CFG_ENV)?;
|
||||
}
|
||||
|
||||
// CI information
|
||||
if let Some(ci) = built::CI_PLATFORM {
|
||||
writeln!(info, "\n## CI Platform\n")?;
|
||||
writeln!(info, "**Platform:** {ci}")?;
|
||||
}
|
||||
|
||||
self.write_str(&info).await
|
||||
}
|
||||
|
||||
@@ -52,10 +52,4 @@ pub enum ServerCommand {
|
||||
|
||||
/// Shutdown the server
|
||||
Shutdown,
|
||||
|
||||
/// List features built into the server
|
||||
ListFeatures,
|
||||
|
||||
/// Build information
|
||||
BuildInfo,
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
fmt::Write as _,
|
||||
};
|
||||
|
||||
use api::client::{
|
||||
full_user_deactivate, join_room_by_id_helper, leave_room, recreate_push_rules_and_return,
|
||||
remote_leave_room,
|
||||
};
|
||||
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room, remote_leave_room};
|
||||
use conduwuit::{
|
||||
Err, Result, debug_warn, error, info,
|
||||
matrix::{Event, pdu::PduBuilder},
|
||||
@@ -14,7 +11,6 @@
|
||||
warn,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use lettre::Address;
|
||||
use ruma::{
|
||||
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, UserId,
|
||||
events::{
|
||||
@@ -1098,117 +1094,3 @@ pub(super) async fn enable_login(&self, user_id: String) -> Result {
|
||||
|
||||
self.write_str(&format!("{user_id} can now log in.")).await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn get_email(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
|
||||
match self
|
||||
.services
|
||||
.threepid
|
||||
.get_email_for_localpart(user_id.localpart())
|
||||
.await
|
||||
{
|
||||
| Some(email) =>
|
||||
self.write_str(&format!("{user_id} has the associated email address {email}."))
|
||||
.await,
|
||||
| None =>
|
||||
self.write_str(&format!("{user_id} has no associated email address."))
|
||||
.await,
|
||||
}
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn get_user_by_email(&self, email: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
let Ok(email) = Address::try_from(email) else {
|
||||
return Err!("Invalid email address.");
|
||||
};
|
||||
|
||||
match self.services.threepid.get_localpart_for_email(&email).await {
|
||||
| Some(localpart) => {
|
||||
let user_id = OwnedUserId::parse(format!(
|
||||
"@{localpart}:{}",
|
||||
self.services.globals.server_name()
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
self.write_str(&format!("{email} belongs to {user_id}."))
|
||||
.await
|
||||
},
|
||||
| None =>
|
||||
self.write_str(&format!("No user has {email} as their email address."))
|
||||
.await,
|
||||
}
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn change_email(&self, user_id: String, email: Option<String>) -> Result {
|
||||
self.bail_restricted()?;
|
||||
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
let Ok(new_email) = email.map(Address::try_from).transpose() else {
|
||||
return Err!("Invalid email address.");
|
||||
};
|
||||
|
||||
if self.services.mailer.mailer().is_none() {
|
||||
warn!("SMTP has not been configured on this server, emails cannot be sent.");
|
||||
}
|
||||
|
||||
let current_email = self
|
||||
.services
|
||||
.threepid
|
||||
.get_email_for_localpart(user_id.localpart())
|
||||
.await;
|
||||
|
||||
match (current_email, new_email) {
|
||||
| (None, None) =>
|
||||
self.write_str(&format!(
|
||||
"{user_id} already had no associated email. No changes have been made."
|
||||
))
|
||||
.await,
|
||||
| (current_email, Some(new_email)) => {
|
||||
self.services
|
||||
.threepid
|
||||
.associate_localpart_email(user_id.localpart(), &new_email)
|
||||
.await?;
|
||||
|
||||
if let Some(current_email) = current_email {
|
||||
self.write_str(&format!(
|
||||
"The associated email of {user_id} has been changed from {current_email} to \
|
||||
{new_email}."
|
||||
))
|
||||
.await
|
||||
} else {
|
||||
self.write_str(&format!(
|
||||
"{user_id} has been associated with the email {new_email}."
|
||||
))
|
||||
.await
|
||||
}
|
||||
},
|
||||
| (Some(current_email), None) => {
|
||||
self.services
|
||||
.threepid
|
||||
.disassociate_localpart_email(user_id.localpart())
|
||||
.await;
|
||||
|
||||
self.write_str(&format!(
|
||||
"The associated email of {user_id} has been removed (it was {current_email})."
|
||||
))
|
||||
.await
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn reset_push_rules(&self, user_id: String) -> Result {
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
if !self.services.users.is_active(&user_id).await {
|
||||
return Err!("User is not active.");
|
||||
}
|
||||
recreate_push_rules_and_return(self.services, &user_id).await?;
|
||||
self.write_str("Reset user's push rules to the server default.")
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -35,24 +35,6 @@ pub enum UserCommand {
|
||||
username: String,
|
||||
},
|
||||
|
||||
/// Get a user's associated email address.
|
||||
GetEmail {
|
||||
user_id: String,
|
||||
},
|
||||
|
||||
/// Get the user with the given email address.
|
||||
GetUserByEmail {
|
||||
email: String,
|
||||
},
|
||||
|
||||
/// Update or remove a user's email address.
|
||||
///
|
||||
/// If `email` is not supplied, the user's existing address will be removed.
|
||||
ChangeEmail {
|
||||
user_id: String,
|
||||
email: Option<String>,
|
||||
},
|
||||
|
||||
/// Deactivate a user
|
||||
///
|
||||
/// User will be removed from all rooms by default.
|
||||
@@ -257,10 +239,4 @@ pub enum UserCommand {
|
||||
#[arg(long)]
|
||||
yes_i_want_to_do_this: bool,
|
||||
},
|
||||
|
||||
/// Resets the push-rules (notification settings) of the target user to the
|
||||
/// server defaults.
|
||||
ResetPushRules {
|
||||
user_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
name = "conduwuit_api"
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
@@ -77,10 +76,8 @@ axum.workspace = true
|
||||
base64.workspace = true
|
||||
bytes.workspace = true
|
||||
conduwuit-core.workspace = true
|
||||
conduwuit-macros.workspace = true
|
||||
conduwuit-service.workspace = true
|
||||
const-str.workspace = true
|
||||
ctor.workspace = true
|
||||
futures.workspace = true
|
||||
hmac.workspace = true
|
||||
http.workspace = true
|
||||
@@ -88,7 +85,6 @@ http-body-util.workspace = true
|
||||
hyper.workspace = true
|
||||
ipaddress.workspace = true
|
||||
itertools.workspace = true
|
||||
lettre.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
reqwest.workspace = true
|
||||
|
||||
980
src/api/client/account.rs
Normal file
980
src/api/client/account.rs
Normal file
@@ -0,0 +1,980 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Error, Event, Result, debug_info, err, error, info,
|
||||
matrix::pdu::PduBuilder,
|
||||
utils::{self, ReadyExt, stream::BroadbandExt},
|
||||
warn,
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use register::RegistrationKind;
|
||||
use ruma::{
|
||||
OwnedRoomId, UserId,
|
||||
api::client::{
|
||||
account::{
|
||||
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
||||
deactivate, get_3pids, get_username_availability,
|
||||
register::{self, LoginType},
|
||||
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
||||
whoami,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
events::{
|
||||
GlobalAccountDataEventType, StateEventType,
|
||||
room::{
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
message::RoomMessageEventContent,
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
},
|
||||
push,
|
||||
};
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
||||
use crate::Ruma;
|
||||
|
||||
const RANDOM_USER_ID_LENGTH: usize = 10;
|
||||
|
||||
/// # `GET /_matrix/client/v3/register/available`
|
||||
///
|
||||
/// Checks if a username is valid and available on this server.
|
||||
///
|
||||
/// Conditions for returning true:
|
||||
/// - The user id is not historical
|
||||
/// - The server name of the user id matches this server
|
||||
/// - No user or appservice on this server already claimed this username
|
||||
///
|
||||
/// Note: This will not reserve the username, so the username might become
|
||||
/// invalid when trying to register
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "register_available", level = "info")]
|
||||
pub(crate) async fn get_register_available_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_username_availability::v3::Request>,
|
||||
) -> Result<get_username_availability::v3::Response> {
|
||||
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
|
||||
let is_matrix_appservice_irc = body.appservice_info.as_ref().is_some_and(|appservice| {
|
||||
appservice.registration.id == "irc"
|
||||
|| appservice.registration.id.contains("matrix-appservice-irc")
|
||||
|| appservice.registration.id.contains("matrix_appservice_irc")
|
||||
});
|
||||
|
||||
if services
|
||||
.globals
|
||||
.forbidden_usernames()
|
||||
.is_match(&body.username)
|
||||
{
|
||||
return Err!(Request(Forbidden("Username is forbidden")));
|
||||
}
|
||||
|
||||
// don't force the username lowercase if it's from matrix-appservice-irc
|
||||
let body_username = if is_matrix_appservice_irc {
|
||||
body.username.clone()
|
||||
} else {
|
||||
body.username.to_lowercase()
|
||||
};
|
||||
|
||||
// Validate user id
|
||||
let user_id =
|
||||
match UserId::parse_with_server_name(&body_username, services.globals.server_name()) {
|
||||
| Ok(user_id) => {
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
// unless the username is from the broken matrix appservice IRC bridge, we
|
||||
// should follow synapse's behaviour on not allowing things like spaces
|
||||
// and UTF-8 characters in usernames
|
||||
if !is_matrix_appservice_irc {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {body_username} contains disallowed characters or spaces: \
|
||||
{e}"
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {body_username} is not valid: {e}"
|
||||
))));
|
||||
},
|
||||
};
|
||||
|
||||
// Check if username is creative enough
|
||||
if services.users.exists(&user_id).await {
|
||||
return Err!(Request(UserInUse("User ID is not available.")));
|
||||
}
|
||||
|
||||
if let Some(ref info) = body.appservice_info {
|
||||
if !info.is_user_match(&user_id) {
|
||||
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
|
||||
}
|
||||
}
|
||||
|
||||
if services.appservice.is_exclusive_user_id(&user_id).await {
|
||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||
}
|
||||
|
||||
Ok(get_username_availability::v3::Response { available: true })
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/register`
|
||||
///
|
||||
/// Register an account on this homeserver.
|
||||
///
|
||||
/// You can use [`GET
|
||||
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
|
||||
/// html) to check if the user id is valid and available.
|
||||
///
|
||||
/// - Only works if registration is enabled
|
||||
/// - If type is guest: ignores all parameters except
|
||||
/// initial_device_display_name
|
||||
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
|
||||
/// - If type is not guest and no username is given: Always fails after UIAA
|
||||
/// check
|
||||
/// - Creates a new account and populates it with default account data
|
||||
/// - If `inhibit_login` is false: Creates a device and returns device id and
|
||||
/// access_token
|
||||
#[allow(clippy::doc_markdown)]
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
|
||||
pub(crate) async fn register_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<register::v3::Request>,
|
||||
) -> Result<register::v3::Response> {
|
||||
let is_guest = body.kind == RegistrationKind::Guest;
|
||||
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||
|
||||
// Allow registration if it's enabled in the config file or if this is the first
|
||||
// run (so the first user account can be created)
|
||||
let allow_registration =
|
||||
services.config.allow_registration || services.firstrun.is_first_run();
|
||||
|
||||
if !allow_registration && body.appservice_info.is_none() {
|
||||
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
|
||||
| (Some(username), Some(device_display_name)) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
user = %username,
|
||||
device_name = %device_display_name,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (Some(username), _) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
user = %username,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (_, Some(device_display_name)) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
device_name = %device_display_name,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (None, _) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
if is_guest && !services.config.allow_guest_registration {
|
||||
info!(
|
||||
"Guest registration disabled, rejecting guest registration attempt, initial device \
|
||||
name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
|
||||
}
|
||||
|
||||
// forbid guests from registering if there is not a real admin user yet. give
|
||||
// generic user error.
|
||||
if is_guest && services.users.count().await < 2 {
|
||||
warn!(
|
||||
"Guest account attempted to register before a real admin user has been registered, \
|
||||
rejecting registration. Guest's initial device name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
let user_id = match (body.username.as_ref(), is_guest) {
|
||||
| (Some(username), false) => {
|
||||
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
|
||||
let is_matrix_appservice_irc =
|
||||
body.appservice_info.as_ref().is_some_and(|appservice| {
|
||||
appservice.registration.id == "irc"
|
||||
|| appservice.registration.id.contains("matrix-appservice-irc")
|
||||
|| appservice.registration.id.contains("matrix_appservice_irc")
|
||||
});
|
||||
|
||||
if services.globals.forbidden_usernames().is_match(username)
|
||||
&& !emergency_mode_enabled
|
||||
{
|
||||
return Err!(Request(Forbidden("Username is forbidden")));
|
||||
}
|
||||
|
||||
// don't force the username lowercase if it's from matrix-appservice-irc
|
||||
let body_username = if is_matrix_appservice_irc {
|
||||
username.clone()
|
||||
} else {
|
||||
username.to_lowercase()
|
||||
};
|
||||
|
||||
let proposed_user_id = match UserId::parse_with_server_name(
|
||||
&body_username,
|
||||
services.globals.server_name(),
|
||||
) {
|
||||
| Ok(user_id) => {
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
// unless the username is from the broken matrix appservice IRC bridge, or
|
||||
// we are in emergency mode, we should follow synapse's behaviour on
|
||||
// not allowing things like spaces and UTF-8 characters in usernames
|
||||
if !is_matrix_appservice_irc && !emergency_mode_enabled {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {body_username} contains disallowed characters or \
|
||||
spaces: {e}"
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow registration with user IDs that aren't local
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
return Err!(Request(InvalidUsername(
|
||||
"Username {body_username} is not local to this server"
|
||||
)));
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {body_username} is not valid: {e}"
|
||||
))));
|
||||
},
|
||||
};
|
||||
|
||||
if services.users.exists(&proposed_user_id).await {
|
||||
return Err!(Request(UserInUse("User ID is not available.")));
|
||||
}
|
||||
|
||||
proposed_user_id
|
||||
},
|
||||
| _ => loop {
|
||||
let proposed_user_id = UserId::parse_with_server_name(
|
||||
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
.unwrap();
|
||||
if !services.users.exists(&proposed_user_id).await {
|
||||
break proposed_user_id;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if body.body.login_type == Some(LoginType::ApplicationService) {
|
||||
match body.appservice_info {
|
||||
| Some(ref info) =>
|
||||
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
|
||||
return Err!(Request(Exclusive(
|
||||
"Username is not in an appservice namespace."
|
||||
)));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(MissingToken("Missing appservice token.")));
|
||||
},
|
||||
}
|
||||
} else if services.appservice.is_exclusive_user_id(&user_id).await && !emergency_mode_enabled
|
||||
{
|
||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||
}
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: Vec::new(),
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
let skip_auth = body.appservice_info.is_some() || is_guest;
|
||||
|
||||
// Populate required UIAA flows
|
||||
|
||||
if services.firstrun.is_first_run() {
|
||||
// Registration token forced while in first-run mode
|
||||
uiaainfo.flows.push(AuthFlow {
|
||||
stages: vec![AuthType::RegistrationToken],
|
||||
});
|
||||
} else {
|
||||
if services
|
||||
.registration_tokens
|
||||
.iterate_tokens()
|
||||
.next()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
// Registration token required
|
||||
uiaainfo.flows.push(AuthFlow {
|
||||
stages: vec![AuthType::RegistrationToken],
|
||||
});
|
||||
}
|
||||
|
||||
if services.config.recaptcha_private_site_key.is_some() {
|
||||
if let Some(pubkey) = &services.config.recaptcha_site_key {
|
||||
// ReCaptcha required
|
||||
uiaainfo
|
||||
.flows
|
||||
.push(AuthFlow { stages: vec![AuthType::ReCaptcha] });
|
||||
uiaainfo.params = serde_json::value::to_raw_value(&serde_json::json!({
|
||||
"m.login.recaptcha": {
|
||||
"public_key": pubkey,
|
||||
},
|
||||
}))
|
||||
.expect("Failed to serialize recaptcha params");
|
||||
}
|
||||
}
|
||||
|
||||
if uiaainfo.flows.is_empty() && !skip_auth {
|
||||
// Registration isn't _disabled_, but there's no captcha configured and no
|
||||
// registration tokens currently set. Bail out by default unless open
|
||||
// registration was explicitly enabled.
|
||||
if !services
|
||||
.config
|
||||
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
// We have open registration enabled (😧), provide a dummy stage
|
||||
uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if !skip_auth {
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(
|
||||
&UserId::parse_with_server_name("", services.globals.server_name())
|
||||
.unwrap(),
|
||||
"".into(),
|
||||
auth,
|
||||
&uiaainfo,
|
||||
)
|
||||
.await?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services.uiaa.create(
|
||||
&UserId::parse_with_server_name("", services.globals.server_name())
|
||||
.unwrap(),
|
||||
"".into(),
|
||||
&uiaainfo,
|
||||
json,
|
||||
);
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("JSON body is not valid")));
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let password = if is_guest { None } else { body.password.as_deref() };
|
||||
|
||||
// Create user
|
||||
services.users.create(&user_id, password, None).await?;
|
||||
|
||||
// Default to pretty displayname
|
||||
let mut displayname = user_id.localpart().to_owned();
|
||||
|
||||
// If `new_user_displayname_suffix` is set, registration will push whatever
|
||||
// content is set to the user's display name with a space before it
|
||||
if !services.globals.new_user_displayname_suffix().is_empty()
|
||||
&& body.appservice_info.is_none()
|
||||
{
|
||||
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_displayname(&user_id, Some(displayname.clone()));
|
||||
|
||||
// Initial account data
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
&user_id,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
||||
content: ruma::events::push_rules::PushRulesEventContent {
|
||||
global: push::Ruleset::server_default(&user_id),
|
||||
},
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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(|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 token for the device
|
||||
let new_token = utils::random_string(TOKEN_LENGTH);
|
||||
|
||||
// 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("");
|
||||
|
||||
// log in conduit admin channel if a non-guest user registered
|
||||
if body.appservice_info.is_none() && !is_guest {
|
||||
if !device_display_name.is_empty() {
|
||||
let notice = format!(
|
||||
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
||||
display name \"{device_display_name}\""
|
||||
);
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
} else {
|
||||
let notice = format!("New user \"{user_id}\" registered on this server.");
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log in conduit admin channel if a guest registered
|
||||
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
||||
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
||||
|
||||
if !device_display_name.is_empty() {
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with device display name \
|
||||
\"{device_display_name}\" registered on this server from IP {client}"
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with no device display name registered on \
|
||||
this server from IP {client}",
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !is_guest {
|
||||
// Make the first user to register an administrator and disable first-run mode.
|
||||
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
|
||||
|
||||
// If the registering user was not the first and we're suspending users on
|
||||
// register, suspend them.
|
||||
if !was_first_user && services.config.suspend_on_register {
|
||||
// Note that we can still do auto joins for suspended users
|
||||
services
|
||||
.users
|
||||
.suspend_account(&user_id, &services.globals.server_user)
|
||||
.await;
|
||||
// And send an @room notice to the admin room, to prompt admins to review the
|
||||
// new user and ideally unsuspend them if deemed appropriate.
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} has been suspended as they are not the first user on \
|
||||
this server. Please review and unsuspend them if appropriate."
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if body.appservice_info.is_none()
|
||||
&& !services.server.config.auto_join_rooms.is_empty()
|
||||
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
||||
{
|
||||
for room in &services.server.config.auto_join_rooms {
|
||||
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
||||
error!(
|
||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||
{room}, skipping"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Skipping room {room} to automatically join as we have never joined before."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_server_name) = room.server_name() {
|
||||
match join_room_by_id_helper(
|
||||
&services,
|
||||
&user_id,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||
&body.appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Err(e) => {
|
||||
// don't return this error so we don't fail registrations
|
||||
error!(
|
||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
||||
);
|
||||
},
|
||||
| _ => {
|
||||
info!("Automatically joined room {room} for user {user_id}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(register::v3::Response {
|
||||
access_token: token,
|
||||
user_id,
|
||||
device_id: device,
|
||||
refresh_token: None,
|
||||
expires_in: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/password`
|
||||
///
|
||||
/// Changes the password of this account.
|
||||
///
|
||||
/// - Requires UIAA to verify user password
|
||||
/// - Changes the password of the sender user
|
||||
/// - The password hash is calculated using argon2 with 32 character salt, the
|
||||
/// plain password is
|
||||
/// not saved
|
||||
///
|
||||
/// If logout_devices is true it does the following for each device except the
|
||||
/// sender device:
|
||||
/// - Invalidates access token
|
||||
/// - Deletes device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")]
|
||||
pub(crate) async fn change_password_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<change_password::v3::Request>,
|
||||
) -> Result<change_password::v3::Response> {
|
||||
// Authentication for this endpoint was made optional, but we need
|
||||
// authentication currently
|
||||
let sender_user = body
|
||||
.sender_user
|
||||
.as_ref()
|
||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, body.sender_device(), &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("JSON body is not valid")));
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_password(sender_user, Some(&body.new_password))
|
||||
.await?;
|
||||
|
||||
if body.logout_devices {
|
||||
// Logout all devices except the current one
|
||||
services
|
||||
.users
|
||||
.all_device_ids(sender_user)
|
||||
.ready_filter(|id| *id != body.sender_device())
|
||||
.for_each(|id| services.users.remove_device(sender_user, id))
|
||||
.await;
|
||||
|
||||
// Remove all pushers except the ones associated with this session
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(sender_user)
|
||||
.map(ToOwned::to_owned)
|
||||
.broad_filter_map(async |pushkey| {
|
||||
services
|
||||
.pusher
|
||||
.get_pusher_device(&pushkey)
|
||||
.await
|
||||
.ok()
|
||||
.filter(|pusher_device| pusher_device != body.sender_device())
|
||||
.is_some()
|
||||
.then_some(pushkey)
|
||||
})
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(sender_user, &pushkey).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
info!("User {sender_user} changed their password.");
|
||||
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("User {sender_user} changed their password."))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(change_password::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/account/whoami`
|
||||
///
|
||||
/// Get `user_id` of the sender user.
|
||||
///
|
||||
/// Note: Also works for Application Services
|
||||
pub(crate) async fn whoami_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<whoami::v3::Request>,
|
||||
) -> Result<whoami::v3::Response> {
|
||||
let is_guest = services
|
||||
.users
|
||||
.is_deactivated(body.sender_user())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
err!(Request(Forbidden("Application service has not registered this user.")))
|
||||
})? && body.appservice_info.is_none();
|
||||
Ok(whoami::v3::Response {
|
||||
user_id: body.sender_user().to_owned(),
|
||||
device_id: body.sender_device.clone(),
|
||||
is_guest,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/deactivate`
|
||||
///
|
||||
/// Deactivate sender user account.
|
||||
///
|
||||
/// - Leaves all rooms and rejects all invitations
|
||||
/// - Invalidates all access tokens
|
||||
/// - Deletes all device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets all to-device events
|
||||
/// - Triggers device list updates
|
||||
/// - Removes ability to log in again
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "deactivate", level = "info")]
|
||||
pub(crate) async fn deactivate_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<deactivate::v3::Request>,
|
||||
) -> Result<deactivate::v3::Response> {
|
||||
// Authentication for this endpoint was made optional, but we need
|
||||
// authentication currently
|
||||
let sender_user = body
|
||||
.sender_user
|
||||
.as_ref()
|
||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, body.sender_device(), &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("JSON body is not valid")));
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Remove profile pictures and display name
|
||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
full_user_deactivate(&services, sender_user, &all_joined_rooms)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
info!("User {sender_user} deactivated their account.");
|
||||
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("User {sender_user} deactivated their account."))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(deactivate::v3::Response {
|
||||
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET _matrix/client/v3/account/3pid`
|
||||
///
|
||||
/// Get a list of third party identifiers associated with this account.
|
||||
///
|
||||
/// - Currently always returns empty list
|
||||
pub(crate) async fn third_party_route(
|
||||
body: Ruma<get_3pids::v3::Request>,
|
||||
) -> Result<get_3pids::v3::Response> {
|
||||
let _sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
Ok(get_3pids::v3::Response::new(Vec::new()))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an email
|
||||
/// address to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||
/// as a contact option.
|
||||
pub(crate) async fn request_3pid_management_token_via_email_route(
|
||||
_body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
||||
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an phone
|
||||
/// number to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||
/// as a contact option.
|
||||
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
||||
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
|
||||
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
||||
///
|
||||
/// Checks if the provided registration token is valid at the time of checking.
|
||||
pub(crate) async fn check_registration_token_validity(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<check_registration_token_validity::v1::Request>,
|
||||
) -> Result<check_registration_token_validity::v1::Response> {
|
||||
// TODO: ratelimit this pretty heavily
|
||||
|
||||
let valid = services
|
||||
.registration_tokens
|
||||
.validate_token(body.token.clone())
|
||||
.await
|
||||
.is_some();
|
||||
|
||||
Ok(check_registration_token_validity::v1::Response { valid })
|
||||
}
|
||||
|
||||
/// Runs through all the deactivation steps:
|
||||
///
|
||||
/// - Mark as deactivated
|
||||
/// - Removing display name
|
||||
/// - Removing avatar URL and blurhash
|
||||
/// - Removing all profile data
|
||||
/// - Leaving all rooms (and forgets all of them)
|
||||
pub async fn full_user_deactivate(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
all_joined_rooms: &[OwnedRoomId],
|
||||
) -> Result<()> {
|
||||
services.users.deactivate_account(user_id).await.ok();
|
||||
|
||||
services
|
||||
.users
|
||||
.all_profile_keys(user_id)
|
||||
.ready_for_each(|(profile_key, _)| {
|
||||
services.users.set_profile_key(user_id, &profile_key, None);
|
||||
})
|
||||
.await;
|
||||
|
||||
// TODO: Rescind all user invites
|
||||
|
||||
let mut pdu_queue: Vec<(PduBuilder, &OwnedRoomId)> = Vec::new();
|
||||
|
||||
for room_id in all_joined_rooms {
|
||||
let room_power_levels = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<RoomPowerLevelsEventContent>(
|
||||
room_id,
|
||||
&StateEventType::RoomPowerLevels,
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let user_can_demote_self =
|
||||
room_power_levels
|
||||
.as_ref()
|
||||
.is_some_and(|power_levels_content| {
|
||||
RoomPowerLevels::from(power_levels_content.clone())
|
||||
.user_can_change_user_power_level(user_id, user_id)
|
||||
}) || services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.is_ok_and(|event| event.sender() == user_id);
|
||||
|
||||
if user_can_demote_self {
|
||||
let mut power_levels_content = room_power_levels.unwrap_or_default();
|
||||
power_levels_content.users.remove(user_id);
|
||||
let pl_evt = PduBuilder::state(String::new(), &power_levels_content);
|
||||
pdu_queue.push((pl_evt, room_id));
|
||||
}
|
||||
|
||||
// Leave the room
|
||||
pdu_queue.push((
|
||||
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
avatar_url: None,
|
||||
blurhash: None,
|
||||
membership: MembershipState::Leave,
|
||||
displayname: None,
|
||||
join_authorized_via_users_server: None,
|
||||
reason: None,
|
||||
is_direct: None,
|
||||
third_party_invite: None,
|
||||
redact_events: None,
|
||||
}),
|
||||
room_id,
|
||||
));
|
||||
|
||||
// TODO: Redact all messages sent by the user in the room
|
||||
}
|
||||
|
||||
super::update_all_rooms(services, pdu_queue, user_id).await;
|
||||
for room_id in all_joined_rooms {
|
||||
services.rooms.state_cache.forget(room_id, user_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Event, Result, err, info,
|
||||
pdu::PduBuilder,
|
||||
utils::{ReadyExt, stream::BroadbandExt},
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use lettre::{Address, message::Mailbox};
|
||||
use ruma::{
|
||||
OwnedRoomId, OwnedUserId, UserId,
|
||||
api::client::{
|
||||
account::{
|
||||
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
||||
deactivate, get_username_availability, request_password_change_token_via_email,
|
||||
whoami,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType},
|
||||
},
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
},
|
||||
};
|
||||
use service::{mailer::messages, uiaa::Identity};
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
||||
use crate::Ruma;
|
||||
|
||||
pub(crate) mod register;
|
||||
pub(crate) mod threepid;
|
||||
|
||||
/// # `GET /_matrix/client/v3/register/available`
|
||||
///
|
||||
/// Checks if a username is valid and available on this server.
|
||||
///
|
||||
/// Conditions for returning true:
|
||||
/// - The user id is not historical
|
||||
/// - The server name of the user id matches this server
|
||||
/// - No user or appservice on this server already claimed this username
|
||||
///
|
||||
/// Note: This will not reserve the username, so the username might become
|
||||
/// invalid when trying to register
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "register_available", level = "info")]
|
||||
pub(crate) async fn get_register_available_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_username_availability::v3::Request>,
|
||||
) -> Result<get_username_availability::v3::Response> {
|
||||
// Validate user id
|
||||
let user_id =
|
||||
match UserId::parse_with_server_name(&body.username, services.globals.server_name()) {
|
||||
| Ok(user_id) => {
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {} contains disallowed characters or spaces: {e}",
|
||||
body.username
|
||||
))));
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {} is not valid: {e}",
|
||||
body.username
|
||||
))));
|
||||
},
|
||||
};
|
||||
|
||||
// Check if username is creative enough
|
||||
if services.users.exists(&user_id).await {
|
||||
return Err!(Request(UserInUse("User ID is not available.")));
|
||||
}
|
||||
|
||||
if let Some(ref info) = body.appservice_info {
|
||||
if !info.is_user_match(&user_id) {
|
||||
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
|
||||
}
|
||||
}
|
||||
|
||||
if services.appservice.is_exclusive_user_id(&user_id).await {
|
||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||
}
|
||||
|
||||
Ok(get_username_availability::v3::Response { available: true })
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/password`
|
||||
///
|
||||
/// Changes the password of this account.
|
||||
///
|
||||
/// - Requires UIAA to verify user password
|
||||
/// - Changes the password of the sender user
|
||||
/// - The password hash is calculated using argon2 with 32 character salt, the
|
||||
/// plain password is
|
||||
/// not saved
|
||||
///
|
||||
/// If logout_devices is true it does the following for each device except the
|
||||
/// sender device:
|
||||
/// - Invalidates access token
|
||||
/// - Deletes device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")]
|
||||
pub(crate) async fn change_password_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<change_password::v3::Request>,
|
||||
) -> Result<change_password::v3::Response> {
|
||||
let identity = if let Some(ref user_id) = body.sender_user {
|
||||
// A signed-in user is trying to change their password, prompt them for their
|
||||
// existing one
|
||||
|
||||
services
|
||||
.uiaa
|
||||
.authenticate(
|
||||
&body.auth,
|
||||
vec![AuthFlow::new(vec![AuthType::Password])],
|
||||
Box::default(),
|
||||
Some(Identity::from_user_id(user_id)),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
// A signed-out user is trying to reset their password, prompt them for email
|
||||
// confirmation. Note that we do not _send_ an email here, their client should
|
||||
// have already hit `/account/password/requestToken` to send the email. We
|
||||
// just validate it.
|
||||
|
||||
services
|
||||
.uiaa
|
||||
.authenticate(
|
||||
&body.auth,
|
||||
vec![AuthFlow::new(vec![AuthType::EmailIdentity])],
|
||||
Box::default(),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
};
|
||||
|
||||
let sender_user = OwnedUserId::parse(format!(
|
||||
"@{}:{}",
|
||||
identity.localpart.expect("localpart should be known"),
|
||||
services.globals.server_name()
|
||||
))
|
||||
.expect("user ID should be valid");
|
||||
|
||||
services
|
||||
.users
|
||||
.set_password(&sender_user, Some(&body.new_password))
|
||||
.await?;
|
||||
|
||||
if body.logout_devices {
|
||||
// Logout all devices except the current one
|
||||
services
|
||||
.users
|
||||
.all_device_ids(&sender_user)
|
||||
.ready_filter(|id| *id != body.sender_device())
|
||||
.for_each(|id| services.users.remove_device(&sender_user, id))
|
||||
.await;
|
||||
|
||||
// Remove all pushers except the ones associated with this session
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(&sender_user)
|
||||
.map(ToOwned::to_owned)
|
||||
.broad_filter_map(async |pushkey| {
|
||||
services
|
||||
.pusher
|
||||
.get_pusher_device(&pushkey)
|
||||
.await
|
||||
.ok()
|
||||
.filter(|pusher_device| pusher_device != body.sender_device())
|
||||
.is_some()
|
||||
.then_some(pushkey)
|
||||
})
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(&sender_user, &pushkey).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
info!("User {} changed their password.", &sender_user);
|
||||
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("User {} changed their password.", &sender_user))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(change_password::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/password/email/requestToken`
|
||||
///
|
||||
/// Requests a validation email for the purpose of resetting a user's password.
|
||||
pub(crate) async fn request_password_change_token_via_email_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<request_password_change_token_via_email::v3::Request>,
|
||||
) -> Result<request_password_change_token_via_email::v3::Response> {
|
||||
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||
};
|
||||
|
||||
let Some(localpart) = services.threepid.get_localpart_for_email(&email).await else {
|
||||
return Err!(Request(ThreepidNotFound(
|
||||
"No account is associated with this email address"
|
||||
)));
|
||||
};
|
||||
|
||||
let user_id =
|
||||
OwnedUserId::parse(format!("@{localpart}:{}", services.globals.server_name())).unwrap();
|
||||
let display_name = services.users.displayname(&user_id).await.ok();
|
||||
|
||||
let session = services
|
||||
.threepid
|
||||
.send_validation_email(
|
||||
Mailbox::new(display_name.clone(), email),
|
||||
|verification_link| messages::PasswordReset {
|
||||
display_name: display_name.as_deref(),
|
||||
user_id: &user_id,
|
||||
verification_link,
|
||||
},
|
||||
&body.client_secret,
|
||||
body.send_attempt.try_into().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(request_password_change_token_via_email::v3::Response::new(session))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/account/whoami`
|
||||
///
|
||||
/// Get `user_id` of the sender user.
|
||||
///
|
||||
/// Note: Also works for Application Services
|
||||
pub(crate) async fn whoami_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<whoami::v3::Request>,
|
||||
) -> Result<whoami::v3::Response> {
|
||||
let is_guest = services
|
||||
.users
|
||||
.is_deactivated(body.sender_user())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
err!(Request(Forbidden("Application service has not registered this user.")))
|
||||
})? && body.appservice_info.is_none();
|
||||
Ok(whoami::v3::Response {
|
||||
user_id: body.sender_user().to_owned(),
|
||||
device_id: body.sender_device.clone(),
|
||||
is_guest,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/deactivate`
|
||||
///
|
||||
/// Deactivate sender user account.
|
||||
///
|
||||
/// - Leaves all rooms and rejects all invitations
|
||||
/// - Invalidates all access tokens
|
||||
/// - Deletes all device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets all to-device events
|
||||
/// - Triggers device list updates
|
||||
/// - Removes ability to log in again
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "deactivate", level = "info")]
|
||||
pub(crate) async fn deactivate_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<deactivate::v3::Request>,
|
||||
) -> Result<deactivate::v3::Response> {
|
||||
// Authentication for this endpoint is technically optional,
|
||||
// but we require the user to be logged in
|
||||
let sender_user = body
|
||||
.sender_user
|
||||
.as_ref()
|
||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
||||
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
|
||||
// Remove profile pictures and display name
|
||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
full_user_deactivate(&services, sender_user, &all_joined_rooms)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
info!("User {sender_user} deactivated their account.");
|
||||
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("User {sender_user} deactivated their account."))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(deactivate::v3::Response {
|
||||
id_server_unbind_result: ThirdPartyIdRemovalStatus::Success,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
||||
///
|
||||
/// Checks if the provided registration token is valid at the time of checking.
|
||||
pub(crate) async fn check_registration_token_validity(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<check_registration_token_validity::v1::Request>,
|
||||
) -> Result<check_registration_token_validity::v1::Response> {
|
||||
// TODO: ratelimit this pretty heavily
|
||||
|
||||
let valid = services
|
||||
.registration_tokens
|
||||
.validate_token(body.token.clone())
|
||||
.await
|
||||
.is_some();
|
||||
|
||||
Ok(check_registration_token_validity::v1::Response { valid })
|
||||
}
|
||||
|
||||
/// Runs through all the deactivation steps:
|
||||
///
|
||||
/// - Mark as deactivated
|
||||
/// - Removing display name
|
||||
/// - Removing avatar URL and blurhash
|
||||
/// - Removing all profile data
|
||||
/// - Leaving all rooms (and forgets all of them)
|
||||
pub async fn full_user_deactivate(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
all_joined_rooms: &[OwnedRoomId],
|
||||
) -> Result<()> {
|
||||
services.users.deactivate_account(user_id).await.ok();
|
||||
|
||||
if services.globals.user_is_local(user_id) {
|
||||
let _ = services
|
||||
.threepid
|
||||
.disassociate_localpart_email(user_id.localpart())
|
||||
.await;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.all_profile_keys(user_id)
|
||||
.ready_for_each(|(profile_key, _)| {
|
||||
services.users.set_profile_key(user_id, &profile_key, None);
|
||||
})
|
||||
.await;
|
||||
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(user_id)
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(user_id, pushkey).await;
|
||||
})
|
||||
.await;
|
||||
|
||||
// TODO: Rescind all user invites
|
||||
|
||||
let mut pdu_queue: Vec<(PduBuilder, &OwnedRoomId)> = Vec::new();
|
||||
|
||||
for room_id in all_joined_rooms {
|
||||
let room_power_levels = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<RoomPowerLevelsEventContent>(
|
||||
room_id,
|
||||
&StateEventType::RoomPowerLevels,
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let user_can_demote_self =
|
||||
room_power_levels
|
||||
.as_ref()
|
||||
.is_some_and(|power_levels_content| {
|
||||
RoomPowerLevels::from(power_levels_content.clone())
|
||||
.user_can_change_user_power_level(user_id, user_id)
|
||||
}) || services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.is_ok_and(|event| event.sender() == user_id);
|
||||
|
||||
if user_can_demote_self {
|
||||
let mut power_levels_content = room_power_levels.unwrap_or_default();
|
||||
power_levels_content.users.remove(user_id);
|
||||
let pl_evt = PduBuilder::state(String::new(), &power_levels_content);
|
||||
pdu_queue.push((pl_evt, room_id));
|
||||
}
|
||||
|
||||
// Leave the room
|
||||
pdu_queue.push((
|
||||
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
avatar_url: None,
|
||||
blurhash: None,
|
||||
membership: MembershipState::Leave,
|
||||
displayname: None,
|
||||
join_authorized_via_users_server: None,
|
||||
reason: None,
|
||||
is_direct: None,
|
||||
third_party_invite: None,
|
||||
redact_events: None,
|
||||
}),
|
||||
room_id,
|
||||
));
|
||||
|
||||
// TODO: Redact all messages sent by the user in the room
|
||||
}
|
||||
|
||||
super::update_all_rooms(services, pdu_queue, user_id)
|
||||
.boxed()
|
||||
.await;
|
||||
for room_id in all_joined_rooms {
|
||||
services.rooms.state_cache.forget(room_id, user_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,626 +0,0 @@
|
||||
use std::{collections::HashMap, fmt::Write};
|
||||
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, debug_info, error, info,
|
||||
utils::{self},
|
||||
warn,
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use lettre::{Address, message::Mailbox};
|
||||
use register::RegistrationKind;
|
||||
use ruma::{
|
||||
OwnedUserId, UserId,
|
||||
api::client::{
|
||||
account::{
|
||||
register::{self, LoginType},
|
||||
request_registration_token_via_email,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType},
|
||||
},
|
||||
events::{GlobalAccountDataEventType, room::message::RoomMessageEventContent},
|
||||
push,
|
||||
};
|
||||
use serde_json::value::RawValue;
|
||||
use service::mailer::messages;
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
||||
use crate::Ruma;
|
||||
|
||||
const RANDOM_USER_ID_LENGTH: usize = 10;
|
||||
|
||||
/// # `POST /_matrix/client/v3/register`
|
||||
///
|
||||
/// Register an account on this homeserver.
|
||||
///
|
||||
/// You can use [`GET
|
||||
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
|
||||
/// html) to check if the user id is valid and available.
|
||||
///
|
||||
/// - Only works if registration is enabled
|
||||
/// - If type is guest: ignores all parameters except
|
||||
/// initial_device_display_name
|
||||
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
|
||||
/// - If type is not guest and no username is given: Always fails after UIAA
|
||||
/// check
|
||||
/// - Creates a new account and populates it with default account data
|
||||
/// - If `inhibit_login` is false: Creates a device and returns device id and
|
||||
/// access_token
|
||||
#[allow(clippy::doc_markdown)]
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
|
||||
pub(crate) async fn register_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<register::v3::Request>,
|
||||
) -> Result<register::v3::Response> {
|
||||
let is_guest = body.kind == RegistrationKind::Guest;
|
||||
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||
|
||||
// Allow registration if it's enabled in the config file or if this is the first
|
||||
// run (so the first user account can be created)
|
||||
let allow_registration =
|
||||
services.config.allow_registration || services.firstrun.is_first_run();
|
||||
|
||||
if !allow_registration && body.appservice_info.is_none() {
|
||||
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
|
||||
| (Some(username), Some(device_display_name)) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
user = %username,
|
||||
device_name = %device_display_name,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (Some(username), _) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
user = %username,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (_, Some(device_display_name)) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
device_name = %device_display_name,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
| (None, _) => {
|
||||
info!(
|
||||
%is_guest,
|
||||
"Rejecting registration attempt as registration is disabled"
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
if is_guest && !services.config.allow_guest_registration {
|
||||
info!(
|
||||
"Guest registration disabled, rejecting guest registration attempt, initial device \
|
||||
name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
|
||||
}
|
||||
|
||||
// forbid guests from registering if there is not a real admin user yet. give
|
||||
// generic user error.
|
||||
if is_guest && services.firstrun.is_first_run() {
|
||||
warn!(
|
||||
"Guest account attempted to register before a real admin user has been registered, \
|
||||
rejecting registration. Guest's initial device name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
);
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
// Appeservices and guests get to skip auth
|
||||
let skip_auth = body.appservice_info.is_some() || is_guest;
|
||||
|
||||
let identity = if skip_auth {
|
||||
// Appservices and guests have no identity
|
||||
None
|
||||
} else {
|
||||
// Perform UIAA to determine the user's identity
|
||||
let (flows, params) = create_registration_uiaa_session(&services).await?;
|
||||
|
||||
Some(
|
||||
services
|
||||
.uiaa
|
||||
.authenticate(&body.auth, flows, params, None)
|
||||
.await?,
|
||||
)
|
||||
};
|
||||
|
||||
// If the user didn't supply a username but did supply an email, use
|
||||
// the email's user as their initial localpart to avoid falling back to
|
||||
// a randomly generated localpart
|
||||
let supplied_username = body.username.clone().or_else(|| {
|
||||
if let Some(identity) = &identity
|
||||
&& let Some(email) = &identity.email
|
||||
{
|
||||
Some(email.user().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let user_id = determine_registration_user_id(
|
||||
&services,
|
||||
supplied_username,
|
||||
is_guest,
|
||||
emergency_mode_enabled,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if body.body.login_type == Some(LoginType::ApplicationService) {
|
||||
// For appservice logins, make sure that the user ID is in the appservice's
|
||||
// namespace
|
||||
|
||||
match body.appservice_info {
|
||||
| Some(ref info) =>
|
||||
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
|
||||
return Err!(Request(Exclusive(
|
||||
"Username is not in an appservice namespace."
|
||||
)));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(MissingToken("Missing appservice token.")));
|
||||
},
|
||||
}
|
||||
} else if services.appservice.is_exclusive_user_id(&user_id).await && !emergency_mode_enabled
|
||||
{
|
||||
// For non-appservice logins, ban user IDs which are in an appservice's
|
||||
// namespace (unless emergency mode is enabled)
|
||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||
}
|
||||
|
||||
let password = if is_guest { None } else { body.password.as_deref() };
|
||||
|
||||
// Create user
|
||||
services.users.create(&user_id, password, None).await?;
|
||||
|
||||
// Set an initial display name
|
||||
let mut displayname = user_id.localpart().to_owned();
|
||||
|
||||
// Apply the new user displayname suffix, if it's set
|
||||
if !services.globals.new_user_displayname_suffix().is_empty()
|
||||
&& body.appservice_info.is_none()
|
||||
{
|
||||
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_displayname(&user_id, Some(displayname.clone()));
|
||||
|
||||
// Initial account data
|
||||
services
|
||||
.account_data
|
||||
.update(
|
||||
None,
|
||||
&user_id,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
||||
content: ruma::events::push_rules::PushRulesEventContent {
|
||||
global: push::Ruleset::server_default(&user_id),
|
||||
},
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 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(|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 token for the device
|
||||
let new_token = utils::random_string(TOKEN_LENGTH);
|
||||
|
||||
// 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)
|
||||
};
|
||||
|
||||
// If the user registered with an email, associate it with their account.
|
||||
if let Some(identity) = identity
|
||||
&& let Some(email) = identity.email
|
||||
{
|
||||
// This may fail if the email is already in use, but we already check for that
|
||||
// in `/requestToken`, so ignoring the error is acceptable here in the rare case
|
||||
// that an email is sniped by another user between the `/requestToken` request
|
||||
// and the `/register` request.
|
||||
let _ = services
|
||||
.threepid
|
||||
.associate_localpart_email(user_id.localpart(), &email)
|
||||
.await;
|
||||
}
|
||||
|
||||
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
|
||||
|
||||
// log in conduit admin channel if a non-guest user registered
|
||||
if body.appservice_info.is_none() && !is_guest {
|
||||
if !device_display_name.is_empty() {
|
||||
let notice = format!(
|
||||
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
||||
display name \"{device_display_name}\""
|
||||
);
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
} else {
|
||||
let notice = format!("New user \"{user_id}\" registered on this server.");
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log in conduit admin channel if a guest registered
|
||||
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
||||
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
||||
|
||||
if !device_display_name.is_empty() {
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with device display name \
|
||||
\"{device_display_name}\" registered on this server from IP {client}"
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with no device display name registered on \
|
||||
this server from IP {client}",
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !is_guest {
|
||||
// Make the first user to register an administrator and disable first-run mode.
|
||||
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
|
||||
|
||||
// If the registering user was not the first and we're suspending users on
|
||||
// register, suspend them.
|
||||
if !was_first_user && services.config.suspend_on_register {
|
||||
// Note that we can still do auto joins for suspended users
|
||||
services
|
||||
.users
|
||||
.suspend_account(&user_id, &services.globals.server_user)
|
||||
.await;
|
||||
// And send an @room notice to the admin room, to prompt admins to review the
|
||||
// new user and ideally unsuspend them if deemed appropriate.
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} has been suspended as they are not the first user on \
|
||||
this server. Please review and unsuspend them if appropriate."
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if body.appservice_info.is_none()
|
||||
&& !services.server.config.auto_join_rooms.is_empty()
|
||||
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
||||
{
|
||||
for room in &services.server.config.auto_join_rooms {
|
||||
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
||||
error!(
|
||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||
{room}, skipping"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Skipping room {room} to automatically join as we have never joined before."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_server_name) = room.server_name() {
|
||||
match join_room_by_id_helper(
|
||||
&services,
|
||||
&user_id,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||
&body.appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Err(e) => {
|
||||
// don't return this error so we don't fail registrations
|
||||
error!(
|
||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
||||
);
|
||||
},
|
||||
| _ => {
|
||||
info!("Automatically joined room {room} for user {user_id}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(register::v3::Response {
|
||||
access_token: token,
|
||||
user_id,
|
||||
device_id: device,
|
||||
refresh_token: None,
|
||||
expires_in: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Determine which flows and parameters should be presented when
|
||||
/// registering a new account.
|
||||
async fn create_registration_uiaa_session(
|
||||
services: &Services,
|
||||
) -> Result<(Vec<AuthFlow>, Box<RawValue>)> {
|
||||
let mut params = HashMap::<String, serde_json::Value>::new();
|
||||
|
||||
let flows = if services.firstrun.is_first_run() {
|
||||
// Registration token forced while in first-run mode
|
||||
vec![AuthFlow::new(vec![AuthType::RegistrationToken])]
|
||||
} else {
|
||||
let mut flows = vec![];
|
||||
|
||||
if services
|
||||
.registration_tokens
|
||||
.iterate_tokens()
|
||||
.next()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
// Trusted registration flow with a token is available
|
||||
let mut token_flow = AuthFlow::new(vec![AuthType::RegistrationToken]);
|
||||
|
||||
if let Some(smtp) = &services.config.smtp
|
||||
&& smtp.require_email_for_token_registration
|
||||
{
|
||||
// Email is required for token registrations
|
||||
token_flow.stages.push(AuthType::EmailIdentity);
|
||||
}
|
||||
|
||||
flows.push(token_flow);
|
||||
}
|
||||
|
||||
let mut untrusted_flow = AuthFlow::default();
|
||||
|
||||
if services.config.recaptcha_private_site_key.is_some() {
|
||||
if let Some(pubkey) = &services.config.recaptcha_site_key {
|
||||
// ReCaptcha is configured for untrusted registrations
|
||||
untrusted_flow.stages.push(AuthType::ReCaptcha);
|
||||
|
||||
params.insert(
|
||||
AuthType::ReCaptcha.as_str().to_owned(),
|
||||
serde_json::json!({
|
||||
"public_key": pubkey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(smtp) = &services.config.smtp
|
||||
&& smtp.require_email_for_registration
|
||||
{
|
||||
// Email is required for untrusted registrations
|
||||
untrusted_flow.stages.push(AuthType::EmailIdentity);
|
||||
}
|
||||
|
||||
if !untrusted_flow.stages.is_empty() {
|
||||
flows.push(untrusted_flow);
|
||||
}
|
||||
|
||||
// Require all users to agree to the terms and conditions, if configured
|
||||
let terms = &services.config.registration_terms;
|
||||
if !terms.is_empty() {
|
||||
let mut terms =
|
||||
serde_json::to_value(terms.clone()).expect("failed to serialize terms");
|
||||
|
||||
// Insert a dummy `version` field
|
||||
for (_, documents) in terms.as_object_mut().unwrap() {
|
||||
let documents = documents.as_object_mut().unwrap();
|
||||
|
||||
documents.insert("version".to_owned(), "latest".into());
|
||||
}
|
||||
|
||||
params.insert(
|
||||
AuthType::Terms.as_str().to_owned(),
|
||||
serde_json::json!({
|
||||
"policies": terms,
|
||||
}),
|
||||
);
|
||||
|
||||
for flow in &mut flows {
|
||||
flow.stages.insert(0, AuthType::Terms);
|
||||
}
|
||||
}
|
||||
|
||||
if flows.is_empty() {
|
||||
// No flows are configured. Bail out by default
|
||||
// unless open registration was explicitly enabled.
|
||||
if !services
|
||||
.config
|
||||
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
// We have open registration enabled (😧), provide a dummy flow
|
||||
flows.push(AuthFlow::new(vec![AuthType::Dummy]));
|
||||
}
|
||||
|
||||
flows
|
||||
};
|
||||
|
||||
let params = serde_json::value::to_raw_value(¶ms).expect("params should be valid JSON");
|
||||
|
||||
Ok((flows, params))
|
||||
}
|
||||
|
||||
async fn determine_registration_user_id(
|
||||
services: &Services,
|
||||
supplied_username: Option<String>,
|
||||
is_guest: bool,
|
||||
emergency_mode_enabled: bool,
|
||||
) -> Result<OwnedUserId> {
|
||||
if let Some(supplied_username) = supplied_username
|
||||
&& !is_guest
|
||||
{
|
||||
// The user gets to pick their username. Do some validation to make sure it's
|
||||
// acceptable.
|
||||
|
||||
// Don't allow registration with forbidden usernames.
|
||||
if services
|
||||
.globals
|
||||
.forbidden_usernames()
|
||||
.is_match(&supplied_username)
|
||||
&& !emergency_mode_enabled
|
||||
{
|
||||
return Err!(Request(Forbidden("Username is forbidden")));
|
||||
}
|
||||
|
||||
// Create and validate the user ID
|
||||
let user_id = match UserId::parse_with_server_name(
|
||||
&supplied_username,
|
||||
services.globals.server_name(),
|
||||
) {
|
||||
| Ok(user_id) => {
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
// Unless we are in emergency mode, we should follow synapse's behaviour on
|
||||
// not allowing things like spaces and UTF-8 characters in usernames
|
||||
if !emergency_mode_enabled {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {supplied_username} contains disallowed characters or \
|
||||
spaces: {e}"
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow registration with user IDs that aren't local
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
return Err!(Request(InvalidUsername(
|
||||
"Username {supplied_username} is not local to this server"
|
||||
)));
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {supplied_username} is not valid: {e}"
|
||||
))));
|
||||
},
|
||||
};
|
||||
|
||||
if services.users.exists(&user_id).await {
|
||||
return Err!(Request(UserInUse("User ID is not available.")));
|
||||
}
|
||||
|
||||
Ok(user_id)
|
||||
} else {
|
||||
// The user is a guest or didn't specify a username. Generate a username for
|
||||
// them.
|
||||
|
||||
loop {
|
||||
let user_id = UserId::parse_with_server_name(
|
||||
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if !services.users.exists(&user_id).await {
|
||||
break Ok(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/register/email/requestToken`
|
||||
///
|
||||
/// Requests a validation email for the purpose of registering a new account.
|
||||
pub(crate) async fn request_registration_token_via_email_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<request_registration_token_via_email::v3::Request>,
|
||||
) -> Result<request_registration_token_via_email::v3::Response> {
|
||||
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||
};
|
||||
|
||||
if services
|
||||
.threepid
|
||||
.get_localpart_for_email(&email)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
return Err!(Request(ThreepidInUse("This email address is already in use.")));
|
||||
}
|
||||
|
||||
let session = services
|
||||
.threepid
|
||||
.send_validation_email(
|
||||
Mailbox::new(None, email),
|
||||
|verification_link| messages::NewAccount {
|
||||
server_name: services.config.server_name.as_ref(),
|
||||
verification_link,
|
||||
},
|
||||
&body.client_secret,
|
||||
body.send_attempt.try_into().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(request_registration_token_via_email::v3::Response::new(session))
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, err};
|
||||
use lettre::{Address, message::Mailbox};
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
api::client::account::{
|
||||
ThirdPartyIdRemovalStatus, add_3pid, delete_3pid, get_3pids,
|
||||
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
||||
},
|
||||
thirdparty::{Medium, ThirdPartyIdentifierInit},
|
||||
};
|
||||
use service::{mailer::messages, uiaa::Identity};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET _matrix/client/v3/account/3pid`
|
||||
///
|
||||
/// Get a list of third party identifiers associated with this account.
|
||||
pub(crate) async fn third_party_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_3pids::v3::Request>,
|
||||
) -> Result<get_3pids::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
let mut threepids = vec![];
|
||||
|
||||
if let Some(email) = services
|
||||
.threepid
|
||||
.get_email_for_localpart(sender_user.localpart())
|
||||
.await
|
||||
{
|
||||
threepids.push(
|
||||
ThirdPartyIdentifierInit {
|
||||
address: email.to_string(),
|
||||
medium: Medium::Email,
|
||||
// We don't currently track these, and they aren't used for much
|
||||
validated_at: MilliSecondsSinceUnixEpoch::now(),
|
||||
added_at: MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::UNIX_EPOCH)
|
||||
.unwrap(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(get_3pids::v3::Response::new(threepids))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
|
||||
///
|
||||
/// Requests a validation email for the purpose of changing an account's email.
|
||||
pub(crate) async fn request_3pid_management_token_via_email_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
||||
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||
};
|
||||
|
||||
if services
|
||||
.threepid
|
||||
.get_localpart_for_email(&email)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
return Err!(Request(ThreepidInUse("This email address is already in use.")));
|
||||
}
|
||||
|
||||
let session = services
|
||||
.threepid
|
||||
.send_validation_email(
|
||||
Mailbox::new(None, email),
|
||||
|verification_link| messages::ChangeEmail {
|
||||
server_name: services.config.server_name.as_str(),
|
||||
user_id: body.sender_user.as_deref(),
|
||||
verification_link,
|
||||
},
|
||||
&body.client_secret,
|
||||
body.send_attempt.try_into().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(request_3pid_management_token_via_email::v3::Response::new(session))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an email
|
||||
/// address to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||
/// as a contact option.
|
||||
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
||||
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
|
||||
Err!(Request(ThreepidMediumNotSupported(
|
||||
"MSISDN third-party identifiers are not supported."
|
||||
)))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/add`
|
||||
pub(crate) async fn add_3pid_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<add_3pid::v3::Request>,
|
||||
) -> Result<add_3pid::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
// Require password auth to add an email
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
|
||||
let email = services
|
||||
.threepid
|
||||
.consume_valid_session(&body.sid, &body.client_secret)
|
||||
.await
|
||||
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?;
|
||||
|
||||
services
|
||||
.threepid
|
||||
.associate_localpart_email(sender_user.localpart(), &email)
|
||||
.await?;
|
||||
|
||||
Ok(add_3pid::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/delete`
|
||||
pub(crate) async fn delete_3pid_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<delete_3pid::v3::Request>,
|
||||
) -> Result<delete_3pid::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
if body.medium != Medium::Email {
|
||||
return Ok(delete_3pid::v3::Response {
|
||||
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
|
||||
});
|
||||
}
|
||||
|
||||
if services
|
||||
.threepid
|
||||
.disassociate_localpart_email(sender_user.localpart())
|
||||
.await
|
||||
.is_none()
|
||||
{
|
||||
return Err!(Request(ThreepidNotFound("Your account has no associated email.")));
|
||||
}
|
||||
|
||||
Ok(delete_3pid::v3::Response {
|
||||
id_server_unbind_result: ThirdPartyIdRemovalStatus::Success,
|
||||
})
|
||||
}
|
||||
@@ -30,10 +30,8 @@ pub(crate) async fn get_capabilities_route(
|
||||
default: services.server.config.default_room_version.clone(),
|
||||
};
|
||||
|
||||
// Only allow 3pid changes if SMTP is configured
|
||||
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability {
|
||||
enabled: services.mailer.mailer().is_some(),
|
||||
};
|
||||
// we do not implement 3PID stuff
|
||||
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability { enabled: false };
|
||||
|
||||
capabilities.get_login_token = GetLoginTokenCapability {
|
||||
enabled: services.server.config.login_via_existing_session,
|
||||
@@ -53,7 +51,7 @@ pub(crate) async fn get_capabilities_route(
|
||||
.await
|
||||
{
|
||||
// Advertise suspension API
|
||||
capabilities.set("uk.timedout.msc4323", json!({"suspend": true, "lock": false}))?;
|
||||
capabilities.set("uk.timedout.msc4323", json!({"suspend":true, "lock": false}))?;
|
||||
}
|
||||
|
||||
Ok(get_capabilities::v3::Response { capabilities })
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{Err, Result, debug, err, utils};
|
||||
use conduwuit::{Err, Error, Result, debug, err, utils};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch, OwnedDeviceId,
|
||||
api::client::device::{
|
||||
self, delete_device, delete_devices, get_device, get_devices, update_device,
|
||||
api::client::{
|
||||
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
|
||||
error::ErrorKind,
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
};
|
||||
use service::uiaa::Identity;
|
||||
|
||||
use super::SESSION_ID_LENGTH;
|
||||
use crate::{Ruma, client::DEVICE_ID_LENGTH};
|
||||
|
||||
/// # `GET /_matrix/client/r0/devices`
|
||||
@@ -121,7 +123,7 @@ pub(crate) async fn delete_device_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<delete_device::v3::Request>,
|
||||
) -> Result<delete_device::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
let appservice = body.appservice_info.as_ref();
|
||||
|
||||
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
|
||||
@@ -137,11 +139,41 @@ pub(crate) async fn delete_device_route(
|
||||
return Ok(delete_device::v3::Response {});
|
||||
}
|
||||
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err!(Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, json);
|
||||
|
||||
return Err!(Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("Not json.")));
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
@@ -168,7 +200,7 @@ pub(crate) async fn delete_devices_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<delete_devices::v3::Request>,
|
||||
) -> Result<delete_devices::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
let appservice = body.appservice_info.as_ref();
|
||||
|
||||
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
|
||||
@@ -183,11 +215,41 @@ pub(crate) async fn delete_devices_route(
|
||||
return Ok(delete_devices::v3::Response {});
|
||||
}
|
||||
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for device_id in &body.devices {
|
||||
services.users.remove_device(sender_user, device_id).await;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use std::iter::once;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Event, Result, RoomVersion, err, info,
|
||||
Err, Event, Result, err, info,
|
||||
utils::{
|
||||
TryFutureExtExt,
|
||||
math::Expected,
|
||||
@@ -32,14 +30,12 @@
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
create::RoomCreateEventContent,
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
},
|
||||
uint,
|
||||
};
|
||||
use tokio::join;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -343,63 +339,36 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks whether the given user ID is allowed to publish the target room to
|
||||
/// the server's public room directory. Users are allowed to publish rooms if
|
||||
/// they are server admins, room creators (in v12), or have the power level to
|
||||
/// send `m.room.canonical_alias`.
|
||||
/// Check whether the user can publish to the room directory via power levels of
|
||||
/// room history visibility event or room creator
|
||||
async fn user_can_publish_room(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
) -> Result<bool> {
|
||||
if services.users.is_admin(user_id).await {
|
||||
// Server admins can always publish to their own room directory.
|
||||
return Ok(true);
|
||||
}
|
||||
let (create_event, room_version, power_levels_content) = join!(
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, ""),
|
||||
services.rooms.state.get_room_version(room_id),
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<RoomPowerLevelsEventContent>(
|
||||
room_id,
|
||||
&StateEventType::RoomPowerLevels,
|
||||
""
|
||||
)
|
||||
);
|
||||
let room_version = room_version
|
||||
.as_ref()
|
||||
.map_err(|_| err!(Request(NotFound("Unknown room"))))?;
|
||||
let create_event = create_event.map_err(|_| err!(Request(NotFound("Unknown room"))))?;
|
||||
if RoomVersion::new(room_version)
|
||||
.expect("room version must be supported")
|
||||
.explicitly_privilege_room_creators
|
||||
match services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
|
||||
.await
|
||||
{
|
||||
let create_content: RoomCreateEventContent =
|
||||
serde_json::from_str(create_event.content().get())
|
||||
.map_err(|_| err!(Database("Invalid event content for m.room.create")))?;
|
||||
let is_creator = create_content
|
||||
.additional_creators
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(once(create_event.sender().to_owned()))
|
||||
.any(|sender| sender == user_id);
|
||||
if is_creator {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
match power_levels_content.map(RoomPowerLevels::from) {
|
||||
| Ok(pl) => Ok(pl.user_can_send_state(user_id, StateEventType::RoomCanonicalAlias)),
|
||||
| Err(e) =>
|
||||
if e.is_not_found() {
|
||||
Ok(create_event.sender() == user_id)
|
||||
} else {
|
||||
Err!(Database("Invalid event content for m.room.power_levels: {e}"))
|
||||
},
|
||||
| Ok(event) => serde_json::from_str(event.content().get())
|
||||
.map_err(|_| err!(Database("Invalid event content for m.room.power_levels")))
|
||||
.map(|content: RoomPowerLevelsEventContent| {
|
||||
RoomPowerLevels::from(content)
|
||||
.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
|
||||
}),
|
||||
| _ => {
|
||||
match services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
{
|
||||
| Ok(event) => Ok(event.sender() == user_id),
|
||||
| _ => Err!(Request(Forbidden("User is not allowed to publish this room"))),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use conduwuit::{
|
||||
Err, Error, Result, debug, debug_warn, err,
|
||||
result::NotFound,
|
||||
utils,
|
||||
utils::{IterStream, stream::WidebandExt},
|
||||
};
|
||||
use conduwuit_service::{Services, users::parse_master_key};
|
||||
@@ -21,6 +22,7 @@
|
||||
upload_signatures::{self},
|
||||
upload_signing_keys,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
federation,
|
||||
},
|
||||
@@ -28,8 +30,8 @@
|
||||
serde::Raw,
|
||||
};
|
||||
use serde_json::json;
|
||||
use service::uiaa::Identity;
|
||||
|
||||
use super::SESSION_ID_LENGTH;
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/upload`
|
||||
@@ -172,7 +174,16 @@ pub(crate) async fn upload_signing_keys_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<upload_signing_keys::v3::Request>,
|
||||
) -> Result<upload_signing_keys::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match check_for_new_keys(
|
||||
services,
|
||||
@@ -196,10 +207,32 @@ pub(crate) async fn upload_signing_keys_route(
|
||||
// Some of the keys weren't found, so we let them upload
|
||||
},
|
||||
| _ => {
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body.as_ref() {
|
||||
| Some(json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -114,19 +114,7 @@ pub(crate) async fn get_content_thumbnail_route(
|
||||
content,
|
||||
content_type,
|
||||
content_disposition,
|
||||
} = match fetch_thumbnail(&services, &mxc, user, body.timeout_ms, &dim).await {
|
||||
| Ok(meta) => meta,
|
||||
| Err(conduwuit::Error::Io(e)) => match e.kind() {
|
||||
| std::io::ErrorKind::NotFound =>
|
||||
return Err!(Request(NotFound("Thumbnail not found."))),
|
||||
| std::io::ErrorKind::PermissionDenied => {
|
||||
error!("Permission denied when trying to read file: {e:?}");
|
||||
return Err!(Request(Unknown("Unknown error when fetching thumbnail.")));
|
||||
},
|
||||
| _ => return Err!(Request(Unknown("Unknown error when fetching thumbnail."))),
|
||||
},
|
||||
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching thumbnail."))),
|
||||
};
|
||||
} = fetch_thumbnail(&services, &mxc, user, body.timeout_ms, &dim).await?;
|
||||
|
||||
Ok(get_content_thumbnail::v1::Response {
|
||||
file: content.expect("entire file contents"),
|
||||
|
||||
@@ -785,15 +785,6 @@ async fn join_room_by_id_helper_local(
|
||||
};
|
||||
|
||||
if servers.is_empty() || servers.len() == 1 && services.globals.server_is_ours(&servers[0]) {
|
||||
if !services.rooms.metadata.exists(room_id).await {
|
||||
return Err!(Request(
|
||||
Unknown(
|
||||
"Room was not found locally and no servers were found to help us discover it"
|
||||
),
|
||||
NOT_FOUND
|
||||
));
|
||||
}
|
||||
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
pub(super) use presence::*;
|
||||
pub(super) use profile::*;
|
||||
pub use profile::{update_all_rooms, update_avatar_url, update_displayname};
|
||||
pub use push::recreate_push_rules_and_return;
|
||||
pub(super) use push::*;
|
||||
pub(super) use read_marker::*;
|
||||
pub(super) use redact::*;
|
||||
@@ -93,3 +92,6 @@
|
||||
|
||||
/// generated user access token length
|
||||
const TOKEN_LENGTH: usize = 32;
|
||||
|
||||
/// generated user session ID length
|
||||
const SESSION_ID_LENGTH: usize = service::uiaa::SESSION_ID_LENGTH;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
use futures::{
|
||||
FutureExt, StreamExt, TryStreamExt,
|
||||
StreamExt, TryStreamExt,
|
||||
future::{join, join3, join4},
|
||||
};
|
||||
use ruma::{
|
||||
@@ -51,7 +51,6 @@ pub(crate) async fn set_displayname_route(
|
||||
.await;
|
||||
|
||||
update_displayname(&services, &body.user_id, body.displayname.clone(), &all_joined_rooms)
|
||||
.boxed()
|
||||
.await;
|
||||
|
||||
if services.config.allow_local_presence {
|
||||
@@ -150,7 +149,6 @@ pub(crate) async fn set_avatar_url_route(
|
||||
body.blurhash.clone(),
|
||||
&all_joined_rooms,
|
||||
)
|
||||
.boxed()
|
||||
.await;
|
||||
|
||||
if services.config.allow_local_presence {
|
||||
@@ -346,9 +344,7 @@ pub async fn update_displayname(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
update_all_rooms(services, all_joined_rooms, user_id)
|
||||
.boxed()
|
||||
.await;
|
||||
update_all_rooms(services, all_joined_rooms, user_id).await;
|
||||
}
|
||||
|
||||
pub async fn update_avatar_url(
|
||||
@@ -398,9 +394,7 @@ pub async fn update_avatar_url(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
update_all_rooms(services, all_joined_rooms, user_id)
|
||||
.boxed()
|
||||
.await;
|
||||
update_all_rooms(services, all_joined_rooms, user_id).await;
|
||||
}
|
||||
|
||||
pub async fn update_all_rooms(
|
||||
|
||||
@@ -489,7 +489,7 @@ pub(crate) async fn set_pushers_route(
|
||||
|
||||
/// user somehow has bad push rules, these must always exist per spec.
|
||||
/// so recreate it and return server default silently
|
||||
pub async fn recreate_push_rules_and_return(
|
||||
async fn recreate_push_rules_and_return(
|
||||
services: &Services,
|
||||
sender_user: &ruma::UserId,
|
||||
) -> Result<get_pushrules_all::v3::Response> {
|
||||
|
||||
@@ -137,7 +137,6 @@ pub(crate) async fn upgrade_room_route(
|
||||
Some(&body.room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
// Change lock to replacement room
|
||||
drop(state_lock);
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Error, Result, debug, err, info,
|
||||
utils::{self, ReadyExt, hash, stream::BroadbandExt},
|
||||
utils::{self, ReadyExt, hash},
|
||||
warn,
|
||||
};
|
||||
use conduwuit_core::{debug_error, debug_warn};
|
||||
use conduwuit_service::Services;
|
||||
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
|
||||
use futures::StreamExt;
|
||||
use lettre::Address;
|
||||
use ruma::{
|
||||
OwnedUserId, UserId,
|
||||
api::client::{
|
||||
@@ -27,10 +26,9 @@
|
||||
},
|
||||
logout, logout_all,
|
||||
},
|
||||
uiaa::UserIdentifier,
|
||||
uiaa,
|
||||
},
|
||||
};
|
||||
use service::uiaa::Identity;
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||
use crate::Ruma;
|
||||
@@ -82,7 +80,7 @@ pub(crate) async fn password_login(
|
||||
.password_hash(lowercased_user_id)
|
||||
.await
|
||||
.map(|hash| (hash, lowercased_user_id))
|
||||
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?,
|
||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?,
|
||||
};
|
||||
|
||||
if hash.is_empty() {
|
||||
@@ -91,7 +89,7 @@ pub(crate) async fn password_login(
|
||||
|
||||
hash::verify_password(password, &hash)
|
||||
.inspect_err(|e| debug_error!("{e}"))
|
||||
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?;
|
||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
||||
|
||||
Ok(user_id.to_owned())
|
||||
}
|
||||
@@ -163,38 +161,28 @@ pub(super) async fn ldap_login(
|
||||
|
||||
pub(crate) async fn handle_login(
|
||||
services: &Services,
|
||||
identifier: Option<&UserIdentifier>,
|
||||
body: &Ruma<login::v3::Request>,
|
||||
identifier: Option<&uiaa::UserIdentifier>,
|
||||
password: &str,
|
||||
user: Option<&String>,
|
||||
) -> Result<OwnedUserId> {
|
||||
debug!("Got password login type");
|
||||
let user_id_or_localpart = match (identifier, user) {
|
||||
| (Some(UserIdentifier::UserIdOrLocalpart(localpart)), _) => localpart,
|
||||
| (Some(UserIdentifier::Email { address }), _) => {
|
||||
let email = Address::try_from(address.to_owned())
|
||||
.map_err(|_| err!(Request(InvalidParam("Email is malformed"))))?;
|
||||
|
||||
&services
|
||||
.threepid
|
||||
.get_localpart_for_email(&email)
|
||||
.await
|
||||
.ok_or_else(|| err!(Request(Forbidden("Invalid identifier or password"))))?
|
||||
},
|
||||
| (None, Some(user)) => user,
|
||||
| _ => {
|
||||
return Err!(Request(InvalidParam("Identifier type not recognized")));
|
||||
},
|
||||
};
|
||||
|
||||
let user_id =
|
||||
UserId::parse_with_server_name(user_id_or_localpart, &services.config.server_name)
|
||||
.map_err(|_| err!(Request(InvalidUsername("User ID is malformed"))))?;
|
||||
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
UserId::parse_with_server_name(user_id, &services.config.server_name)
|
||||
} else if let Some(user) = user {
|
||||
UserId::parse_with_server_name(user, &services.config.server_name)
|
||||
} else {
|
||||
return Err!(Request(Unknown(
|
||||
debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)")
|
||||
)));
|
||||
}
|
||||
.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
|
||||
|
||||
let lowercased_user_id = UserId::parse_with_server_name(
|
||||
user_id.localpart().to_lowercase(),
|
||||
&services.config.server_name,
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
|
||||
if !services.globals.user_is_local(&user_id)
|
||||
|| !services.globals.user_is_local(&lowercased_user_id)
|
||||
@@ -256,7 +244,7 @@ pub(crate) async fn login_route(
|
||||
password,
|
||||
user,
|
||||
..
|
||||
}) => handle_login(&services, identifier.as_ref(), password, user.as_ref()).await?,
|
||||
}) => handle_login(&services, &body, identifier.as_ref(), password, user.as_ref()).await?,
|
||||
| login::v3::LoginInfo::Token(login::v3::Token { token }) => {
|
||||
debug!("Got token login type");
|
||||
if !services.server.config.login_via_existing_session {
|
||||
@@ -276,7 +264,7 @@ pub(crate) async fn login_route(
|
||||
};
|
||||
|
||||
let user_id =
|
||||
if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
UserId::parse_with_server_name(user_id, &services.config.server_name)
|
||||
} else if let Some(user) = user {
|
||||
UserId::parse_with_server_name(user, &services.config.server_name)
|
||||
@@ -285,7 +273,7 @@ pub(crate) async fn login_route(
|
||||
debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)")
|
||||
)));
|
||||
}
|
||||
.map_err(|_| err!(Request(InvalidUsername(warn!("User ID is malformed")))))?;
|
||||
.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
|
||||
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
|
||||
@@ -382,13 +370,45 @@ pub(crate) async fn login_token_route(
|
||||
return Err!(Request(Forbidden("Login via an existing session is not enabled")));
|
||||
}
|
||||
|
||||
let sender_user = body.sender_user();
|
||||
// This route SHOULD have UIA
|
||||
// TODO: How do we make only UIA sessions that have not been used before valid?
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
|
||||
// Prompt the user to confirm with their password using UIAA
|
||||
let _ = services
|
||||
.uiaa
|
||||
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||
.await?;
|
||||
let mut uiaainfo = uiaa::UiaaInfo {
|
||||
flows: vec![uiaa::AuthFlow { stages: vec![uiaa::AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body.as_ref() {
|
||||
| Some(json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("No JSON body was sent when required.")));
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let login_token = utils::random_string(TOKEN_LENGTH);
|
||||
let expires_in = services.users.create_login_token(sender_user, &login_token);
|
||||
@@ -414,28 +434,9 @@ pub(crate) async fn logout_route(
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<logout::v3::Request>,
|
||||
) -> Result<logout::v3::Response> {
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
services
|
||||
.users
|
||||
.remove_device(sender_user, sender_device)
|
||||
.await;
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(sender_user)
|
||||
.map(ToOwned::to_owned)
|
||||
.broad_filter_map(async |pushkey| {
|
||||
services
|
||||
.pusher
|
||||
.get_pusher_device(&pushkey)
|
||||
.await
|
||||
.ok()
|
||||
.as_ref()
|
||||
.is_some_and(|pusher_device| pusher_device == sender_device)
|
||||
.then_some(pushkey)
|
||||
})
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(sender_user, &pushkey).await;
|
||||
})
|
||||
.remove_device(body.sender_user(), body.sender_device())
|
||||
.await;
|
||||
|
||||
Ok(logout::v3::Response::new())
|
||||
@@ -460,18 +461,10 @@ pub(crate) async fn logout_all_route(
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<logout_all::v3::Request>,
|
||||
) -> Result<logout_all::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
services
|
||||
.users
|
||||
.all_device_ids(sender_user)
|
||||
.for_each(|device_id| services.users.remove_device(sender_user, device_id))
|
||||
.await;
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(sender_user)
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(sender_user, pushkey).await;
|
||||
})
|
||||
.all_device_ids(body.sender_user())
|
||||
.for_each(|device_id| services.users.remove_device(body.sender_user(), device_id))
|
||||
.await;
|
||||
|
||||
Ok(logout_all::v3::Response::new())
|
||||
|
||||
@@ -60,7 +60,6 @@ pub(crate) async fn send_state_event_for_key_route(
|
||||
None
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
OwnedRoomId,
|
||||
api::{
|
||||
@@ -112,7 +112,6 @@ pub(crate) async fn set_profile_key_route(
|
||||
Some(display_name.to_owned()),
|
||||
&all_joined_rooms,
|
||||
)
|
||||
.boxed()
|
||||
.await;
|
||||
} else if body.key_name == "avatar_url" {
|
||||
let Some(avatar_url) = profile_key_value.as_str() else {
|
||||
@@ -128,9 +127,7 @@ pub(crate) async fn set_profile_key_route(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
update_avatar_url(&services, &body.user_id, Some(mxc), None, &all_joined_rooms)
|
||||
.boxed()
|
||||
.await;
|
||||
update_avatar_url(&services, &body.user_id, Some(mxc), None, &all_joined_rooms).await;
|
||||
} else {
|
||||
services.users.set_profile_key(
|
||||
&body.user_id,
|
||||
@@ -181,9 +178,7 @@ pub(crate) async fn delete_profile_key_route(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
update_displayname(&services, &body.user_id, None, &all_joined_rooms)
|
||||
.boxed()
|
||||
.await;
|
||||
update_displayname(&services, &body.user_id, None, &all_joined_rooms).await;
|
||||
} else if body.key_name == "avatar_url" {
|
||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
@@ -193,9 +188,7 @@ pub(crate) async fn delete_profile_key_route(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
update_avatar_url(&services, &body.user_id, None, None, &all_joined_rooms)
|
||||
.boxed()
|
||||
.await;
|
||||
update_avatar_url(&services, &body.user_id, None, None, &all_joined_rooms).await;
|
||||
} else {
|
||||
services
|
||||
.users
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use axum::extract::State;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use conduwuit::{Err, Result, utils};
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use hmac::{Hmac, Mac};
|
||||
use ruma::{SecondsSinceUnixEpoch, UserId, api::client::voip::get_turn_server_info};
|
||||
use sha1::Sha1;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user