Compare commits

..

107 Commits

Author SHA1 Message Date
Renovate Bot
ab1fc060a7 chore(deps): lock file maintenance 2026-04-10 10:56:43 +00:00
Renovate Bot
ddc9e795d8 chore(deps): update rust crate serde-saphyr to 0.0.23 2026-04-10 08:30:04 +00:00
Renovate Bot
87892a9739 chore(deps): update https://github.com/actions/github-script action to v9 2026-04-10 08:21:00 +00:00
Renovate Bot
3e2d454989 chore(deps): update dependency cargo-bins/cargo-binstall to v1.17.9 2026-04-10 08:20:12 +00:00
Henry-Hiles
a79e7a01a8 fix: indentation in nixos file 2026-04-10 08:11:44 +00:00
Henry-Hiles
b378cb8c5d fix: multiple top-level headers in generic file 2026-04-10 08:11:44 +00:00
Henry-Hiles
68e31282ef chore: remove nix hardened profile docs as the hardened profile will was removed from nixpkgs.
See https://github.com/NixOS/nixpkgs/pull/501199
2026-04-10 08:11:44 +00:00
Henry-Hiles
f40e0c7773 feat: more specific docs on how to use the flake nix package 2026-04-10 08:11:44 +00:00
Henry-Hiles
fbb855a404 feat: update build docs 2026-04-10 08:11:44 +00:00
Jade Ellis
2325e8fa4c chore: Update generated docs 2026-04-09 17:24:45 +01:00
Jade Ellis
6906d63013 docs: Changelog 2026-04-09 17:24:44 +01:00
Jade Ellis
16de2a2cc0 feat: Add ability to inspect build information and features at runtime
Also re-adds ability to inspect used features
2026-04-09 17:24:44 +01:00
Jade Ellis
108a4fe336 ci: Remove caching of /target directory
This directory seemed to grow exponentially, with incremental
compilation reaching 11GB+ and dependencies not finishing
2026-04-09 17:17:03 +01:00
Renovate Bot
83396db5de chore(deps): update https://github.com/samueldr/lix-gha-installer-action digest to f5e9419 2026-04-09 05:02:05 +00:00
timedout
839138c02e chore: Add news frag 2026-04-08 20:49:59 +00:00
timedout
e03c90c2ac fix: Sign restricted joins when we're the authorising server 2026-04-08 20:49:59 +00:00
Henry-Hiles
379ef5014c fix: only run patchelf on linux 2026-04-08 20:14:36 +00:00
Henry-Hiles
2ab177f100 fix: fix continuwuity build on nix-darwin 2026-04-08 20:14:36 +00:00
Henry-Hiles
a818f51396 fix: devshell on darwin
Co-authored-by: thetayloredman <nutdriver716@gmail.com>
2026-04-08 20:14:36 +00:00
timedout
09bfe79a44 perf: Don't needlessly sign and re-hash events in send_join 2026-04-08 17:17:15 +00:00
timedout
d041adadc8 style: Fix large future clippy errors 2026-04-08 17:17:15 +00:00
timedout
189ed1c394 style: Fix large future clippy error 2026-04-08 17:17:15 +00:00
timedout
36c32938ae fix: Don't try to sign events that don't originate from us 2026-04-08 17:17:15 +00:00
Henry-Hiles
915643c965 feat: overridable rocksdb 2026-04-07 20:41:19 +00:00
Henry-Hiles
4063b2c7da fix: various issues with continuwuity build 2026-04-07 20:41:19 +00:00
Henry-Hiles
943bd81ce9 fix: fix typo in continuwuity build 2026-04-07 20:41:19 +00:00
Henry-Hiles
2942d9133e chore: remove old newline 2026-04-07 20:41:19 +00:00
Henry-Hiles
18a7a85fe4 chore: remove outdated comments 2026-04-07 20:41:19 +00:00
Henry-Hiles
0fdb1be938 feat: add customizable cargoExtraArgs 2026-04-07 20:41:19 +00:00
Henry-Hiles
867a3ac376 chore: Write news fragment 2026-04-07 20:41:19 +00:00
Henry-Hiles
7a6eff091a chore: Pin Lix installer to specific commit 2026-04-07 20:41:19 +00:00
Henry-Hiles
c278663f65 fix: devshell fixes
Co-authored-by: kraem <
me@kraem.xyz>
2026-04-07 20:41:19 +00:00
Henry-Hiles
c822c945e7 fix: make fmt run on correct toolchain 2026-04-07 20:41:19 +00:00
Henry-Hiles
6eb3dc1f9d fix: postPatch issue due to version override 2026-04-07 20:41:19 +00:00
Henry-Hiles
789ec71b75 fix: fix update flake hashes workflow 2026-04-07 20:41:19 +00:00
Henry-Hiles
1cfa3ff10b feat: add rocksdb updater nix app 2026-04-07 20:41:19 +00:00
Henry-Hiles
02cf6b5695 fix: use correct versioning for rocksdb 2026-04-07 20:41:19 +00:00
Henry-Hiles
4cc4893376 chore: remove now incorrect liburing comment in rocksdb nix build override 2026-04-07 20:41:19 +00:00
Henry-Hiles
7643b64f60 fix: patchelf binary to link to correct rocksdb 2026-04-07 20:41:19 +00:00
Henry-Hiles
3d9fd34012 feat: add meta to continuwuity build 2026-04-07 20:41:19 +00:00
Henry-Hiles
630963d6e1 fix: add bindgen hook to build 2026-04-07 20:41:19 +00:00
Henry-Hiles
36da6f5bf3 fix: recursively merge build configuration 2026-04-07 20:41:19 +00:00
Henry-Hiles
462ef63945 fix: bump rocksdb 2026-04-07 20:41:19 +00:00
Henry-Hiles
46bcfe5605 chore: rename toolchain packages 2026-04-07 20:41:19 +00:00
Henry-Hiles
16321cf467 fix: fix crane name in package build 2026-04-07 20:41:19 +00:00
Henry-Hiles
4d59e07006 chore: rewrite devshell, remove checks 2026-04-07 20:41:19 +00:00
Henry-Hiles
ec5f50c68e chore: rewrite continuwuity build 2026-04-07 20:41:19 +00:00
Henry-Hiles
db1b08532e chore: reorganize nix files 2026-04-07 20:41:19 +00:00
Henry-Hiles
d8f67e3b46 chore: simplify rocksdb build 2026-04-07 20:41:19 +00:00
ginger
2124fcf325 fix: Keep rustdoc from trying to run my TOML as a doctest 2026-04-07 18:40:43 +00:00
ezera
38b4065270 fix: use cfg to fix compiler warning for opts
Fixes #1621.
2026-04-07 12:58:23 +00:00
Ginger
2e62ca93a8 fix: Fix registration_terms default in example config 2026-04-07 12:55:56 +00:00
Ginger
b7a6c819b7 chore: News fragment 2026-04-07 12:55:56 +00:00
Ginger
eccc878ee9 feat: Add support for terms and conditions when registering 2026-04-07 12:55:56 +00:00
Tulir Asokan
8b762cf2e6 fix: Server name caching for SRV remotes 2026-04-06 19:57:05 +00:00
timedout
1ce9ae2cbf chore: Update example configuration file 2026-04-06 17:45:04 +00:00
thetayloredman
6a3370005e doc: remove reference to MSC unstable prefix 2026-04-06 17:45:04 +00:00
Logan Devine
675cfb964a feat: add support for MSC4439 PGP key URIs in wk-support
This commit introduces support for MSC4439, Encryption Key URIs
in `.well-known/matrix/support`. ([MSC](https://github.com/matrix-org/matrix-spec-proposals/pull/4439),
[Rendered](https://github.com/thetayloredman/matrix-spec-proposals/blob/msc4439/proposals/4439-support-contact-encryption.md))
via an additional config option.
2026-04-06 17:45:04 +00:00
Tulir Asokan
09312791a7 fix(ci): Add wget to fix llvm.sh in dockerfile
Reviewed-on: https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1616
Reviewed-by: nex <me@nexy7574.co.uk>
Co-authored-by: Tulir Asokan <tulir@maunium.net>
Co-committed-by: Tulir Asokan <tulir@maunium.net>
2026-04-06 15:44:18 +00:00
Ginger
087d8b1016 fix: Remove sliding sync proxy from .well-known/client response 2026-04-06 10:36:30 -04:00
Renovate Bot
6155dd2726 chore(deps): update node-patch-updates to v2.0.8 2026-04-06 13:04:13 +00:00
timedout
688cd8f46a fix: Forbid creating events sent by remote users 2026-04-05 22:34:11 +01:00
timedout
3ab1f102dd fix: Switch lettre to ring backend 2026-04-05 21:07:45 +00:00
timedout
480a32e4d4 chore: Add newsfrag 2026-04-05 21:04:27 +01:00
timedout
fadd559837 feat: Add admin commands to delete pushers 2026-04-05 20:58:11 +01:00
timedout
79c63c17fc feat: Delete pushers when a user logs out 2026-04-05 20:48:03 +01:00
timedout
cdc772ba10 feat: Delete all pushers for a user during deactivation 2026-04-05 20:42:08 +01:00
timedout
5f1b80a47c chore: Add newsfrag 2026-04-05 20:15:52 +01:00
timedout
0f8b56f521 feat: Add admin command to reset user push rules 2026-04-05 20:12:21 +01:00
éźera
67d8d72506 fix: return 404 when joining non-existent room
Fixes #1443.
2026-04-05 11:40:53 -05:00
Renovate Bot
fcfa7b8bef chore(deps): update pre-commit hook crate-ci/typos to v1.45.0 2026-04-02 05:02:08 +00:00
timedout
0cc1e4685c style: Make main green again 2026-03-31 18:07:44 +01:00
ginger
3d2915093c Update LICENSE 2026-03-31 02:26:22 +00:00
Ginger
e1c54f4dec fix: Don't allow UIAA stages to be completed if no flow includes them 2026-03-31 02:20:59 +00:00
ginger
0c9fa3b7e5 feat: Add a notice about email to the first-run banner 2026-03-31 02:20:59 +00:00
Ginger
a95b488e6a chore: Update admin command docs 2026-03-31 02:20:59 +00:00
Ginger
4f8833e937 fix: Update connection_uri docs 2026-03-31 02:20:59 +00:00
Ginger
f32599e030 feat: Supply more informative error message if email is disabled 2026-03-31 02:20:59 +00:00
Ginger
b6f0b41d3d feat: Ratelimit sending threepid validation emails 2026-03-31 02:20:59 +00:00
Ginger
d5675b85cf fix: Release session lock before sending threepid validation email 2026-03-31 02:20:59 +00:00
Ginger
951b5abe19 refactor: Remove UiaaStatus enum 2026-03-31 02:20:59 +00:00
Ginger
a325ad16f1 feat: Fall back to email when registering a user who didn't provide a username 2026-03-31 02:20:59 +00:00
Ginger
f93a1cc506 fix: Don't bail out on email association failures when registering a new user 2026-03-31 02:20:59 +00:00
Ginger
6e8dbcbfab refactor: Remove workarounds for matrix-appservice-irc 2026-03-31 02:20:59 +00:00
ginger
97458207e5 chore: Update news fragment 2026-03-31 02:20:59 +00:00
Ginger
ab8929e2fa chore: Fix typo 2026-03-31 02:20:59 +00:00
Ginger
166d7d0f63 fix: Remove associated email on account deactivation 2026-03-31 02:20:59 +00:00
Ginger
20a6f0c6fb chore: News fragment 2026-03-31 02:20:59 +00:00
Ginger
3885e43b5d feat: Add support for 3pid management 2026-03-31 02:20:59 +00:00
Ginger
ef7ad6082c feat: Add support for registering a new account with an email address 2026-03-31 02:20:59 +00:00
Ginger
717d319708 feat: Add support for logging in with an email address 2026-03-31 02:20:59 +00:00
Ginger
0b04757bef feat: Add support for password resets via email 2026-03-31 02:20:59 +00:00
Ginger
f2b7dd6519 feat: Add a webpage for threepid validation links 2026-03-31 02:20:59 +00:00
Ginger
9d06208a7a feat: Store threepid validation sessions in memory instead of the database 2026-03-31 02:20:59 +00:00
Ginger
955da3a74f feat: Add admin commands for managing users' email addresses 2026-03-31 02:20:59 +00:00
Ginger
7e79a544cf refactor: Split account routes into multiple files 2026-03-31 02:20:59 +00:00
Ginger
f5db4d17d6 feat: Refactor UIAA service, add support for email stage 2026-03-31 02:20:59 +00:00
Ginger
54fd1d313f feat: Implement threepid service 2026-03-31 02:20:59 +00:00
Ginger
bb7fd9efc1 feat: Implement mailer service for sending emails 2026-03-31 02:20:59 +00:00
Jade Ellis
aa79072411 docs: Revert duplicate link 2026-03-29 19:34:56 +01:00
Jade Ellis
8b72c5eb11 docs: Fix email link 2026-03-29 19:25:24 +01:00
Jade Ellis
e5cfc503d8 docs: Delete unused book.toml 2026-03-29 19:21:02 +01:00
Jade Ellis
07d5081008 docs: Apply feedback 2026-03-29 19:20:05 +01:00
Jade Ellis
dba7f47972 docs: Link MatrixRTC room 2026-03-29 19:15:42 +01:00
Jade Ellis
0a2d4e1cb2 docs: Replace Contributor Covenant with community guidelines 2026-03-29 19:15:42 +01:00
Jade Ellis
f45857acd4 docs: Update community guidelines 2026-03-29 19:15:41 +01:00
norm
9209b847f6 docs: Mention systemd's ReadWritePaths setting for the backup dir
The systemd unit file uses `ProtectSystem=strict`, which makes almost
every directory read-only. This can cause backups to not work, even if
the directory is granted the correct permissions and ownership to the
`conduwuit` user.

The `ReadWritePaths` setting lets you specify which directories are
exempt from being made read-only by `ProtectSystem=strict`.
2026-03-27 19:25:26 +00:00
135 changed files with 4094 additions and 3090 deletions

2
.envrc
View File

@@ -2,7 +2,7 @@
dotenv_if_exists
if [ -f /etc/os-release ] && grep -q '^ID=nixos' /etc/os-release; then
if command -v nix >/dev/null 2>&1; then
use flake ".#${DIRENV_DEVSHELL:-default}"
fi

View File

@@ -9,7 +9,7 @@ runs:
- name: Install sccache
uses: https://git.tomfos.tr/tom/sccache-action@v1
- name: Configure sccache
uses: https://github.com/actions/github-script@v8
uses: https://github.com/actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');

View File

@@ -149,37 +149,6 @@ 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::"

View File

@@ -55,7 +55,7 @@ jobs:
fi
- name: Manage PR Comment
uses: https://github.com/actions/github-script@v8
uses: https://github.com/actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
HAS_CHANGELOG: ${{ steps.check_files.outputs.has_changelog }}
SRC_CHANGED: ${{ steps.check_files.outputs.src_changed }}

View File

@@ -16,48 +16,19 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: false
fetch-single-branch: true
submodules: false
persist-credentials: true
token: ${{ secrets.FORGEJO_TOKEN }}
- uses: https://github.com/cachix/install-nix-action@19effe9fe722874e6d46dd7182e4b8b7a43c4a99 # v31.10.0
- name: Install Lix
uses: https://github.com/samueldr/lix-gha-installer-action@f5e94192f565f53d84f41a056956dc0d3183b343
with:
nix_path: nixpkgs=channel:nixos-unstable
# We can skip getting a toolchain hash if this was ran as a dispatch with the intent
# to update just the rocksdb hash. If this was ran as a dispatch and the toolchain
# files are changed, we still update them, as well as the rocksdb import.
- name: Detect changed files
id: changes
run: |
git fetch origin ${{ github.base_ref }} --depth=1 || true
if [ -n "${{ github.event.pull_request.base.sha }}" ]; then
base=${{ github.event.pull_request.base.sha }}
else
base=$(git rev-parse HEAD~1)
fi
echo "Base: $base"
echo "HEAD: $(git rev-parse HEAD)"
git diff --name-only $base HEAD > changed_files.txt
echo "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 }}"
extra_nix_config: experimental-features = nix-command flakes flake-self-attrs
- 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/packages/rust.nix > temp.nix
mv temp.nix nix/packages/rust.nix
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
# Build continuwuity and filter for the new hash
# We do `|| true` because we want this to fail without stopping the workflow
@@ -65,36 +36,17 @@ 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/packages/rust.nix
sed -i "s|lib.fakeSha256|\"$new_hash\"|" nix/rust.nix
echo "New hash:"
awk -F'"' '/fromToolchainFile/{found=1; next} found && /sha256 =/{print $2; found=0}' nix/packages/rust.nix
awk -F'"' '/fromToolchainFile/{found=1; next} found && /sha256 =/{print $2; found=0}' nix/rust.nix
echo "Expected new hash:"
cat new_toolchain_hash.txt
rm new_toolchain_hash.txt
- 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: Update rocksdb
run: nix run .#update-rocksdb
- name: Show diff
run: git diff flake.nix nix

View File

@@ -24,7 +24,7 @@ repos:
- id: check-added-large-files
- repo: https://github.com/crate-ci/typos
rev: v1.44.0
rev: v1.45.0
hooks:
- id: typos
- id: typos

View File

@@ -1,131 +1 @@
# 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
Contributors are expected to follow the [Continuwuity Community Guidelines](continuwuity.org/community/guidelines).

608
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -159,7 +159,7 @@ features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde-saphyr]
version = "0.0.21"
version = "0.0.23"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
@@ -344,7 +344,7 @@ version = "0.1.2"
[workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
#branch = "conduwuit-changes"
rev = "a97b91adcc012ef04991d823b8b5a79c6686ae48"
rev = "1415caf8a32af4d943580c5ea4e12be1974593c2"
features = [
"compat",
"rand",
@@ -383,7 +383,8 @@ features = [
"unstable-pdu",
"unstable-msc4155",
"unstable-msc4143", # livekit well_known response
"unstable-msc4284"
"unstable-msc4284",
"unstable-msc4439", # pgp_key in .well_known/matrix/support
]
[workspace.dependencies.rust-rocksdb]
@@ -556,6 +557,19 @@ 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
#
@@ -916,7 +930,6 @@ 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"

View File

@@ -1,4 +1,3 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -187,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2023 June
Copyright 2023 Continuwuity Team and contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -1,23 +0,0 @@
[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

View File

@@ -0,0 +1 @@
Added support for associating email addresses with accounts, requiring email addresses for registration, and resetting passwords via email. Contributed by @ginger

View File

@@ -0,0 +1 @@
Added support for requiring users to accept terms and conditions when registering.

1
changelog.d/1579.bugfix Normal file
View File

@@ -0,0 +1 @@
Fixed error 500 when joining non-existent rooms. Contributed by @ezera.

1
changelog.d/1596.bugfix Normal file
View File

@@ -0,0 +1 @@
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).

View File

@@ -0,0 +1,2 @@
Add new config option for [MSC4439](https://github.com/matrix-org/matrix-spec-proposals/pull/4439)
PGP key URIs. Contributed by LogN.

1
changelog.d/1613.feature Normal file
View File

@@ -0,0 +1 @@
Added `!admin users reset-push-rules` command to reset the notification settings of users. Contributed by @nex.

1
changelog.d/1614.feature Normal file
View File

@@ -0,0 +1 @@
Notification pushers are now automatically removed when their associated device is. Admin commands now exist for manual cleanup too. Contributed by @nex.

1
changelog.d/1615.bugfix Normal file
View File

@@ -0,0 +1 @@
Fixed resolving IP of servers that only use SRV delegation. Contributed by @tulir.

1
changelog.d/1620.misc Normal file
View File

@@ -0,0 +1 @@
Fixed compiler warning in cf_opts.rs when building in release. Contributed by @ezera.

1
changelog.d/1623.bugfix Normal file
View File

@@ -0,0 +1 @@
Fixed "Sender must be a local user" error for make_join, make_knock, and make_leave federation routes. Contributed by @nex.

View File

@@ -0,0 +1 @@
Added admin commands to get build information and features. Contributed by @Jade

1
changelog.d/1630.bugfix Normal file
View File

@@ -0,0 +1 @@
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).

View File

@@ -95,6 +95,10 @@
# 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
#
@@ -519,6 +523,18 @@
#
#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
@@ -1865,6 +1881,11 @@
#
#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
@@ -2037,3 +2058,41 @@
# 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

View File

@@ -15,13 +15,13 @@ ARG LLVM_VERSION=21
# Install repo tools
# Line one: compiler tools
# Line two: curl, for downloading binaries
# Line two: curl, for downloading binaries and wget because llvm.sh is broken with curl
# 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 \
curl git software-properties-common \
wget 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.17.8
ENV BINSTALL_VERSION=1.17.9
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree

View File

@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.17.8
ENV BINSTALL_VERSION=1.17.9
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree

View File

@@ -5,7 +5,7 @@ # Matrix RTC/Element Call Setup
:::
:::tip
You can find help setting up Matrix RTC in our dedicated room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
You can find help setting up MatrixRTC in our dedicated room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
:::
## Instructions

View File

@@ -1,17 +1,12 @@
# Continuwuity Community Guidelines
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.
Welcome to the Continuwuity commuwunity! We're excited to have you here.
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.
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.
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.
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.
## Our Values and Expected Behaviors
@@ -29,17 +24,21 @@ ## 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. Avoid placing the burden of education on
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
marginalized groups; please make an effort to look into your questions before asking others for
detailed explanations.
4. **Be Open to Improving Inclusivity**: Actively participate in making our community more inclusive.
5. **Be Engaged and Open-Minded**: 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.
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
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
and action from all members.
## Unacceptable Behaviors
@@ -72,36 +71,6 @@ ## 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
@@ -114,6 +83,7 @@ ## 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.

View File

@@ -14,6 +14,7 @@ ### 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)
@@ -42,32 +43,36 @@ #### 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.
### Building with the Rust toolchain
#### Using Docker
If wanting to build using standard Rust toolchains, make sure you install:
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.
- (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
#### 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
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
@@ -128,13 +133,11 @@ ## 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"
```
@@ -202,23 +205,27 @@ ### 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
@@ -232,6 +239,7 @@ ### 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;`
@@ -271,17 +279,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).

View File

@@ -6,9 +6,10 @@ ## Installation methods
You can acquire Continuwuity with Nix (or [Lix][lix]) from these sources:
* 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
- Directly from Nixpkgs using the official package (`pkgs.matrix-continuwuity`)
- Or, using `continuwuity.packages.${system}.default` from:
- The `flake.nix` at the root of the Continuwuity repo
- The `default.nix` at the root of the Continuwuity repo
## NixOS module
@@ -22,6 +23,8 @@ ## NixOS module
{
services.matrix-continuwuity = {
enable = true;
# Optionally, override the package to be from our flake, for faster updates or unstable versions.
# package = inputs.continuwuity.packages.${pkgs.stdenv.hostPlatform.system}.default;
settings = {
global = {
server_name = "example.com";
@@ -77,20 +80,6 @@ ### RocksDB database
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:
@@ -127,4 +116,3 @@ ## Reverse proxy configuration
```
[lix]: https://lix.systems/
[hardened.nix]: https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/profiles/hardened.nix

View File

@@ -130,6 +130,10 @@ ## `!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

View File

@@ -133,6 +133,18 @@ ### `!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

View File

@@ -47,3 +47,11 @@ ## `!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

View File

@@ -12,6 +12,24 @@ ## `!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
@@ -139,3 +157,7 @@ ## `!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
View File

@@ -3,11 +3,11 @@
"advisory-db": {
"flake": false,
"locked": {
"lastModified": 1773786698,
"narHash": "sha256-o/J7ZculgwSs1L4H4UFlFZENOXTJzq1X0n71x6oNNvY=",
"lastModified": 1775764778,
"narHash": "sha256-ehYI2+TnkdX86YRk+38K8ulZTDyifBbUTFub7IQ9vSk=",
"owner": "rustsec",
"repo": "advisory-db",
"rev": "99e9de91bb8b61f06ef234ff84e11f758ecd5384",
"rev": "77cb4096516c6ac5cd0d0ddeb03fb2bbdc1ec989",
"type": "github"
},
"original": {
@@ -18,11 +18,11 @@
},
"crane": {
"locked": {
"lastModified": 1773189535,
"narHash": "sha256-E1G/Or6MWeP+L6mpQ0iTFLpzSzlpGrITfU2220Gq47g=",
"lastModified": 1775790182,
"narHash": "sha256-pG2RWVQY0Pe+rmmXJx+Jpyi+JcgjWzS18m7fcD1B64Q=",
"owner": "ipetkov",
"repo": "crane",
"rev": "6fa2fb4cf4a89ba49fc9dd5a3eb6cde99d388269",
"rev": "534982f1c41834b101e381b07b1121a4f065a374",
"type": "github"
},
"original": {
@@ -39,11 +39,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1773732206,
"narHash": "sha256-HKibxaUXyWd4Hs+ZUnwo6XslvaFqFqJh66uL9tphU4Q=",
"lastModified": 1775807984,
"narHash": "sha256-Redoe3D9zGN5I9QPHWL9vfMVQBehY1fKsMiRXQ83X3w=",
"owner": "nix-community",
"repo": "fenix",
"rev": "0aa13c1b54063a8d8679b28a5cd357ba98f4a56b",
"rev": "fcf90c0c4d368b2ca917a7afa6d08e98a397e5fd",
"type": "github"
},
"original": {
@@ -74,11 +74,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1772408722,
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
"lastModified": 1775087534,
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
"type": "github"
},
"original": {
@@ -89,11 +89,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1773734432,
"narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=",
"lastModified": 1775710090,
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "cda48547b432e8d3b18b4180ba07473762ec8558",
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
"type": "github"
},
"original": {
@@ -105,11 +105,11 @@
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1772328832,
"narHash": "sha256-e+/T/pmEkLP6BHhYjx6GmwP5ivonQQn0bJdH9YrRB+Q=",
"lastModified": 1774748309,
"narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "c185c7a5e5dd8f9add5b2f8ebeff00888b070742",
"rev": "333c4e0545a6da976206c74db8773a1645b5870a",
"type": "github"
},
"original": {
@@ -132,11 +132,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1773697963,
"narHash": "sha256-xdKI77It9PM6eNrCcDZsnP4SKulZwk8VkDgBRVMnCb8=",
"lastModified": 1775745684,
"narHash": "sha256-8MbfLwd60FNa8dRFkjE+G3TT/x21G3Rsplm1bMBQUtU=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "2993637174252ff60a582fd1f55b9ab52c39db6d",
"rev": "64ddb549bc9a70d011328746fa46a8883f937b6b",
"type": "github"
},
"original": {
@@ -153,11 +153,11 @@
]
},
"locked": {
"lastModified": 1773297127,
"narHash": "sha256-6E/yhXP7Oy/NbXtf1ktzmU8SdVqJQ09HC/48ebEGBpk=",
"lastModified": 1775636079,
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "71b125cd05fbfd78cab3e070b73544abe24c5016",
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
"type": "github"
},
"original": {

View File

@@ -29,7 +29,6 @@
url = "github:edolstra/flake-compat?ref=master";
flake = false;
};
};
outputs =
@@ -37,10 +36,10 @@
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [ ./nix ];
systems = [
# good support
"x86_64-linux"
# support untested but theoretically there
"aarch64-linux"
# support untested but theoretically there
"aarch64-darwin"
];
};
}

View File

@@ -1,107 +0,0 @@
{ 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";
}
);
};
};
}

14
nix/crane.nix Normal file
View File

@@ -0,0 +1,14 @@
{ inputs, ... }:
{
perSystem =
{
pkgs,
self',
...
}:
{
_module.args.craneLib = (inputs.crane.mkLib pkgs).overrideToolchain (
pkgs: self'.packages.stable-toolchain
);
};
}

View File

@@ -1,11 +1,10 @@
{
imports = [
./checks
./rust.nix
./crane.nix
./packages
./shells
./tests
./hydra.nix
./devshell.nix
./fmt.nix
./rocksdb-updater.nix
];
}

42
nix/devshell.nix Normal file
View File

@@ -0,0 +1,42 @@
{
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
];
};
};
};
}

View File

@@ -1,9 +0,0 @@
{ inputs, ... }:
let
lib = inputs.nixpkgs.lib;
in
{
flake.hydraJobs.packages = builtins.mapAttrs (
_name: lib.hydraJob
) inputs.self.packages.x86_64-linux;
}

View File

@@ -0,0 +1,65 @@
{
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 ];
};
}
)

View File

@@ -1,59 +0,0 @@
{ 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
];
};
}

View File

@@ -1,14 +1,18 @@
{
imports = [
./continuwuity
./rocksdb
./rust.nix
./uwulib
];
self,
...
}:
{
perSystem =
{ self', ... }:
{
packages.default = self'.packages.continuwuity-default-bin;
pkgs,
craneLib,
...
}:
{
packages = {
rocksdb = pkgs.callPackage ./rocksdb.nix { };
default = pkgs.callPackage ./continuwuity.nix { inherit self craneLib; };
};
};
}

34
nix/packages/rocksdb.nix Normal file
View File

@@ -0,0 +1,34 @@
{
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 = "";
})

View File

@@ -1,12 +0,0 @@
{
perSystem =
{
pkgs,
...
}:
{
packages = {
rocksdb = pkgs.callPackage ./package.nix { };
};
};
}

View File

@@ -1,87 +0,0 @@
{
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 = [ ];
})

View File

@@ -1,122 +0,0 @@
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;
}
);
}

View File

@@ -1,10 +0,0 @@
{ 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; };
};
};
}

View File

@@ -1,18 +0,0 @@
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;
}

View File

@@ -1,77 +0,0 @@
{ 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)
];
};
}

14
nix/rocksdb-updater.nix Normal file
View File

@@ -0,0 +1,14 @@
{
perSystem =
{ pkgs, ... }:
{
apps.update-rocksdb = {
type = "app";
program = pkgs.writeShellApplication {
name = "update-rocksdb";
runtimeInputs = [ pkgs.nix-update ];
text = "nix-update rocksdb -F --version branch";
};
};
};
}

View File

@@ -4,6 +4,7 @@
{
system,
lib,
pkgs,
...
}:
{
@@ -11,7 +12,7 @@
let
fnx = inputs.fenix.packages.${system};
stable = fnx.fromToolchainFile {
stable-toolchain = fnx.fromToolchainFile {
file = inputs.self + "/rust-toolchain.toml";
# See also `rust-toolchain.toml`
@@ -19,11 +20,10 @@
};
in
{
# used for building nix stuff (doesn't include rustfmt overhead)
build-toolchain = stable;
# used for dev shells
inherit stable-toolchain;
dev-toolchain = fnx.combine [
stable
stable-toolchain
# use the nightly rustfmt because we use nightly features
fnx.complete.rustfmt
];

View File

@@ -1,29 +0,0 @@
{ 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.nodejs
pkgs.pkg-config
pkgs.liburing
pkgs.rust-jemalloc-sys-unprefixed
rocksdbAllFeatures
];
env.LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.llvmPackages.libclang.lib ];
};
};
}

View File

@@ -1,150 +0,0 @@
{
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
];
};
}

266
package-lock.json generated
View File

@@ -16,21 +16,21 @@
}
},
"node_modules/@emnapi/core": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
"integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.0",
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -39,9 +39,9 @@
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -123,14 +123,14 @@
}
},
"node_modules/@rsbuild/core": {
"version": "2.0.0-beta.10",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.0-beta.10.tgz",
"integrity": "sha512-6xalOGzWjamJQvC+qnAipo6azfW3cn9JSRSkTMBz/hiXFzcfy54GX31gCDhRY0TooEisyJ2wbGWjGcT8zPwwxg==",
"version": "2.0.0-beta.11",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.0-beta.11.tgz",
"integrity": "sha512-IBbQx7SrnSpD7j2p2qyq3qDxoqmG4E6lcflTpbBitX6iUrzpVRQbP4rktXZ2iuY7ph9+FtUK/SVAVA+Ocm3Nig==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/core": "2.0.0-beta.8",
"@swc/helpers": "^0.5.19"
"@rspack/core": "2.0.0-beta.9",
"@swc/helpers": "^0.5.20"
},
"bin": {
"rsbuild": "bin/rsbuild.js"
@@ -167,28 +167,28 @@
}
},
"node_modules/@rspack/binding": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.0-beta.8.tgz",
"integrity": "sha512-6tG/yYhUIF1zcEF7qw9GPA1Bwj5gq+Hqy4OzVzIBUWOn/2bKsFTWuorEJh8Yx1LwOnjNO7O+NbsATvk5zEOGKQ==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.0-beta.9.tgz",
"integrity": "sha512-QgkOvzl6BJc4Vg5eaY9r7MkHNfXvVZPgTIeYkdBEOYPowdyCLhlG9vH7QltqLKP9KDNel70YIeMyUrpTqez01w==",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"@rspack/binding-darwin-arm64": "2.0.0-beta.8",
"@rspack/binding-darwin-x64": "2.0.0-beta.8",
"@rspack/binding-linux-arm64-gnu": "2.0.0-beta.8",
"@rspack/binding-linux-arm64-musl": "2.0.0-beta.8",
"@rspack/binding-linux-x64-gnu": "2.0.0-beta.8",
"@rspack/binding-linux-x64-musl": "2.0.0-beta.8",
"@rspack/binding-wasm32-wasi": "2.0.0-beta.8",
"@rspack/binding-win32-arm64-msvc": "2.0.0-beta.8",
"@rspack/binding-win32-ia32-msvc": "2.0.0-beta.8",
"@rspack/binding-win32-x64-msvc": "2.0.0-beta.8"
"@rspack/binding-darwin-arm64": "2.0.0-beta.9",
"@rspack/binding-darwin-x64": "2.0.0-beta.9",
"@rspack/binding-linux-arm64-gnu": "2.0.0-beta.9",
"@rspack/binding-linux-arm64-musl": "2.0.0-beta.9",
"@rspack/binding-linux-x64-gnu": "2.0.0-beta.9",
"@rspack/binding-linux-x64-musl": "2.0.0-beta.9",
"@rspack/binding-wasm32-wasi": "2.0.0-beta.9",
"@rspack/binding-win32-arm64-msvc": "2.0.0-beta.9",
"@rspack/binding-win32-ia32-msvc": "2.0.0-beta.9",
"@rspack/binding-win32-x64-msvc": "2.0.0-beta.9"
}
},
"node_modules/@rspack/binding-darwin-arm64": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.0-beta.8.tgz",
"integrity": "sha512-h3x2GreEh8J36A3cWFeHZGTuz4vjUArk9dBDq8fZSyaUQQQox/lp8bUOGa/2YuYUOXk0gei2GN+/BVi2R5p39A==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.0-beta.9.tgz",
"integrity": "sha512-9Aao24b+lrVGG25itl2c7e6HK6eNH5J5ao1Uq5UoSwSJZOxRPuY+QlHIvE2tyt833Ly9qcT1J7os2AIUNlF6Vw==",
"cpu": [
"arm64"
],
@@ -200,9 +200,9 @@
]
},
"node_modules/@rspack/binding-darwin-x64": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.0-beta.8.tgz",
"integrity": "sha512-+XTA37+FZjXgwxkNX94T/EqspFO8Q3Km4CklQ3nOQzieMi31w+TLBB0uTsnT1ugp0UTN5PHLd4DFK1SQB7Ckbg==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.0-beta.9.tgz",
"integrity": "sha512-sP6gusMsxm3W4aHpRsmVaBQU09n1p/1+XpLHT/gZy6nJ7Wy3nqfNKNoybNBORwCuFcGUon6cVRcieN9AEm6iJA==",
"cpu": [
"x64"
],
@@ -214,9 +214,9 @@
]
},
"node_modules/@rspack/binding-linux-arm64-gnu": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.0-beta.8.tgz",
"integrity": "sha512-vD2+ztbMmeBR65jBlwUZCNIjUzO0exp/LaPSMIhLlqPlk670gMCQ7fmKo3tSgQ9tobfizEA/Atdy3/lW1Rl64A==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.0-beta.9.tgz",
"integrity": "sha512-k2DPN3B2qaz4L/h/R+l7rbDk/lLwbR/sayfsHZ8sLdZ3f6pvaSI9ejrsFv0nU4OmKCQsz4zYuoKTVFPtDfbGjA==",
"cpu": [
"arm64"
],
@@ -231,9 +231,9 @@
]
},
"node_modules/@rspack/binding-linux-arm64-musl": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.0-beta.8.tgz",
"integrity": "sha512-jJ1XB7Yz9YdPRA6MJ35S9/mb+3jeI4p9v78E3dexzCPA3G4X7WXbyOcRbUlYcyOlE5MtX5O19rDexqWlkD9tVw==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.0-beta.9.tgz",
"integrity": "sha512-7+XwAsqhfc2rIHMc9mY6RMBTP76RRqmUm1UjidqYdJl5hYBa5apffjeZfJYgAhVbSwKB/tUffzPpEffGUuc5kw==",
"cpu": [
"arm64"
],
@@ -248,9 +248,9 @@
]
},
"node_modules/@rspack/binding-linux-x64-gnu": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.0-beta.8.tgz",
"integrity": "sha512-qy+fK/tiYw3KvGjTGGMu/mWOdvBYrMO8xva/ouiaRTrx64PPZ6vyqFXOUfHj9rhY5L6aU2NTObpV6HZHcBtmhQ==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.0-beta.9.tgz",
"integrity": "sha512-z/EOUKEq5rq4sYsVSFL9uzdPtTPVA82x3gsRJlDTfEcruZZI7Y6JKUkpDYkC0LivXqyOnoOz8slAFd2/dByRtA==",
"cpu": [
"x64"
],
@@ -265,9 +265,9 @@
]
},
"node_modules/@rspack/binding-linux-x64-musl": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.0-beta.8.tgz",
"integrity": "sha512-eJF1IsayHhsURu5Dp6fzdr5jYGeJmoREOZAc9UV3aEqY6zNAcWgZT1RwKCCujJylmHgCTCOuxqdK/VdFJqWDyw==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.0-beta.9.tgz",
"integrity": "sha512-LVIXrqtAOy/DowIB04jyUyYy+5kHtZNJ0W5EJd39OwY/9gGvhgAEVvSWu7JrRAvKW1kQsV7GnRT5ninbDrRw1A==",
"cpu": [
"x64"
],
@@ -282,9 +282,9 @@
]
},
"node_modules/@rspack/binding-wasm32-wasi": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.0-beta.8.tgz",
"integrity": "sha512-HssdOQE8i+nUWoK+NDeD5OSyNxf80k3elKCl/due3WunoNn0h6tUTSZ8QB+bhcT4tjH9vTbibWZIT91avtvUNw==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.0-beta.9.tgz",
"integrity": "sha512-Vl7aDAt7DCqtZ/RJd8hLFjQqufX+efL/XZG3qADsagl/SspH1ItJ7N6X1S8o50eKoshy27Jr7mQYZEdufX9qhQ==",
"cpu": [
"wasm32"
],
@@ -296,9 +296,9 @@
}
},
"node_modules/@rspack/binding-win32-arm64-msvc": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.0-beta.8.tgz",
"integrity": "sha512-RuHbXuIMJr0ANMFoGXIb3sUZE5VwIsJw70u3TKPwfoaOFiJjgW7Pi2JTLPoTYfOlE+CNcu2ldX8VJRBbktR4NA==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.0-beta.9.tgz",
"integrity": "sha512-g4Fc3JjfibuHt5ltoV64eK0bs6NKlh8kgHA8Go3ETwEGO6OBck877e+5CqPtjTH8c1/KQPbnCoccGR1OScoZGg==",
"cpu": [
"arm64"
],
@@ -310,9 +310,9 @@
]
},
"node_modules/@rspack/binding-win32-ia32-msvc": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.0-beta.8.tgz",
"integrity": "sha512-ajzIOk30zjTKPiay+d6oV7lqzzqdgIXQhDD5YtcOqPn7NTh7949EB1NZX5l3Ueh1m8k4DSe7n07qFLjHDhZ8jw==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.0-beta.9.tgz",
"integrity": "sha512-Oii4HpCEH3CBDKSXcS6EVlV9nGYVKAV/uBLSsuZ0RNdEG0i+OHvEiicqHAwuIYZNlH4Ea/Vwc+Dl5PM2twCZ4Q==",
"cpu": [
"ia32"
],
@@ -324,9 +324,9 @@
]
},
"node_modules/@rspack/binding-win32-x64-msvc": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.0-beta.8.tgz",
"integrity": "sha512-MqPuHCbxyLSEjavbhYapHs7cvs2zSA9GKd8nJtDuSMmDTVHFzwHfUXTffUMFB4JTCAvdpMn8XtOG/UOW5QVRCA==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.0-beta.9.tgz",
"integrity": "sha512-7UFjyy7QMtWvf1CBEVQkHL6bJBKaVY9yq9+Qxb7ggtxvpBbkoYykdsrhMTvr/f5TBjBqHmyeb0/oYXqo5pWFBQ==",
"cpu": [
"x64"
],
@@ -338,13 +338,13 @@
]
},
"node_modules/@rspack/core": {
"version": "2.0.0-beta.8",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.0-beta.8.tgz",
"integrity": "sha512-GHiMNhcxfzJV3DqxIYYjiBGzhFkwwt+jSJl8+aVFRbQM0AYRdZJSfQDH4G5rHD1gO2yc3ktOOMHYnZWNtXCwdA==",
"version": "2.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.0-beta.9.tgz",
"integrity": "sha512-4sN3f72l4cj8n/dSCdWn6FkSjfHiDxHWrO1Kmqd0Bk0MmgyW+ldHitsSWPETCAxjTJGXY34r5sou5sYzb0DRww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/binding": "2.0.0-beta.8"
"@rspack/binding": "2.0.0-beta.9"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
@@ -363,14 +363,13 @@
}
},
"node_modules/@rspack/plugin-react-refresh": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.6.1.tgz",
"integrity": "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.6.2.tgz",
"integrity": "sha512-k+/VrfTNgo+KirjI6V+8CWRj6y+DH9jOUWv8JorYY4vKf/9xfnZ8xHzuB4iqCwTtoZl9YnxOaOuoyjJipc2tiQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"error-stack-parser": "^2.1.4",
"html-entities": "^2.6.0"
"error-stack-parser": "^2.1.4"
},
"peerDependencies": {
"react-refresh": ">=0.10.0 <1.0.0",
@@ -383,17 +382,17 @@
}
},
"node_modules/@rspress/core": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.7.tgz",
"integrity": "sha512-+HH6EVSs1SVvm+6l78lluK8u70ihKVX26VHEqYJzTBHLtipMXllmX2mfkjCoATEP2uqU+es4xSPurss+AztHWg==",
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.8.tgz",
"integrity": "sha512-MDkpm6fO0+NoW+Lx0KVL/n9DSRGQcoggeXY+EtlC+ySqF9VxQk4hu87fQhD8q2ikMOd7lbVsWmKspd3rIFD88g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@rsbuild/core": "2.0.0-beta.10",
"@rsbuild/core": "2.0.0-beta.11",
"@rsbuild/plugin-react": "~1.4.6",
"@rspress/shared": "2.0.7",
"@rspress/shared": "2.0.8",
"@shikijs/rehype": "^4.0.2",
"@types/unist": "^3.0.3",
"@unhead/react": "^2.1.12",
@@ -417,7 +416,7 @@
"react-lazy-with-preload": "^2.2.1",
"react-reconciler": "0.33.0",
"react-render-to-markdown": "19.0.1",
"react-router-dom": "^7.13.1",
"react-router-dom": "^7.13.2",
"rehype-external-links": "^3.0.0",
"rehype-raw": "^7.0.0",
"remark-cjk-friendly": "^2.0.1",
@@ -443,39 +442,39 @@
}
},
"node_modules/@rspress/plugin-client-redirects": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.7.tgz",
"integrity": "sha512-fH8HMUktt5ar6alSpGSDE/+dgY+TU/h0bW+4ntysUApKodNF1vg4ta60qju+5guWeRXD/35nH6rMV2Z0nW9BfQ==",
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.8.tgz",
"integrity": "sha512-6/+CYf4u2PGOmuQkqvzLeUKTdOlj+Fnt3D/6IgjZmbXcSDweLvHhC+dHgdZw7T4paiqIxeCqU0duYX8W5agAug==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rspress/core": "^2.0.7"
"@rspress/core": "^2.0.8"
}
},
"node_modules/@rspress/plugin-sitemap": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.7.tgz",
"integrity": "sha512-K4Y8yhpiQkF+cbpfgVhdj/sfwq0K/12AusT06gfVSdJV7R4GvJO28y1GxfrsHlki8nopWu8Zqfqb3dcrNlccfA==",
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.8.tgz",
"integrity": "sha512-V3u+wRvzmJmC+GkigvQYDQfEo43xUlya9OVLqSyRcB/crQ0U99oto6v73isZB/qS/pTb2wbFY+CbzOqrD1uBsA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rspress/core": "^2.0.7"
"@rspress/core": "^2.0.8"
}
},
"node_modules/@rspress/shared": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.7.tgz",
"integrity": "sha512-x7OqCGP5Ir1/X+6fvhtApw/ObHfwVIdWne6LlX3GGUHyHF+01yci6vrUzEP2R6PainnqySzW2+345C6zZ3dZuA==",
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.8.tgz",
"integrity": "sha512-kvfBUvMvWcn/7PJHqZxPeu1yblzvAuB1/gk/1orp5KsYu3wbZ7X3Hsm9smDJVs5Plw1iPt67t9fOYNSM0+VjUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rsbuild/core": "2.0.0-beta.10",
"@rsbuild/core": "2.0.0-beta.11",
"@shikijs/rehype": "^4.0.2",
"gray-matter": "4.0.3",
"lodash-es": "^4.17.23",
@@ -609,9 +608,9 @@
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz",
"integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -630,9 +629,9 @@
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
"integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -716,13 +715,13 @@
"license": "ISC"
},
"node_modules/@unhead/react": {
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.12.tgz",
"integrity": "sha512-1xXFrxyw29f+kScXfEb0GxjlgtnHxoYau0qpW9k8sgWhQUNnE5gNaH3u+rNhd5IqhyvbdDRJpQ25zoz0HIyGaw==",
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.13.tgz",
"integrity": "sha512-gC48tNJ0UtbithkiKCc2WUlxbVVk5o171EtruS2w2hQUblfYFHzCPu2hljjT1e0tUHXXqN8EMv7mpxHddMB2sg==",
"dev": true,
"license": "MIT",
"dependencies": {
"unhead": "2.1.12"
"unhead": "2.1.13"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
@@ -1600,23 +1599,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/html-entities": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/mdevils"
},
{
"type": "patreon",
"url": "https://patreon.com/mdevils"
}
],
"license": "MIT"
},
"node_modules/html-void-elements": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
@@ -1790,9 +1772,9 @@
}
},
"node_modules/lodash-es": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"dev": true,
"license": "MIT"
},
@@ -3058,9 +3040,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3082,9 +3064,9 @@
}
},
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3092,16 +3074,16 @@
}
},
"node_modules/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
"dev": true,
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.4"
"react": "^19.2.5"
}
},
"node_modules/react-lazy-with-preload": {
@@ -3151,9 +3133,9 @@
}
},
"node_modules/react-router": {
"version": "7.13.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
"integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz",
"integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3174,13 +3156,13 @@
}
},
"node_modules/react-router-dom": {
"version": "7.13.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
"integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz",
"integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"react-router": "7.13.1"
"react-router": "7.14.0"
},
"engines": {
"node": ">=20.0.0"
@@ -3620,14 +3602,14 @@
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
@@ -3655,9 +3637,9 @@
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3741,9 +3723,9 @@
}
},
"node_modules/unhead": {
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.12.tgz",
"integrity": "sha512-iTHdWD9ztTunOErtfUFk6Wr11BxvzumcYJ0CzaSCBUOEtg+DUZ9+gnE99i8QkLFT2q1rZD48BYYGXpOZVDLYkA==",
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.13.tgz",
"integrity": "sha512-jO9M1sI6b2h/1KpIu4Jeu+ptumLmUKboRRLxys5pYHFeT+lqTzfNHbYUX9bxVDhC1FBszAGuWcUVlmvIPsah8Q==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -2,6 +2,7 @@
name = "conduwuit_admin"
description.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
@@ -79,7 +80,9 @@ 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

View File

@@ -19,6 +19,7 @@
warn,
};
use futures::{FutureExt, StreamExt, TryStreamExt};
use lettre::message::Mailbox;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId,
@@ -876,3 +877,31 @@ 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(())
}

View File

@@ -225,6 +225,9 @@ 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)]

View File

@@ -3,6 +3,8 @@
#![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;

View File

@@ -1,6 +1,10 @@
use clap::Subcommand;
use conduwuit::Result;
use ruma::OwnedUserId;
use conduwuit::{
Result,
utils::{IterStream, stream::BroadbandExt},
};
use futures::StreamExt;
use ruma::{OwnedDeviceId, OwnedUserId};
use crate::Context;
@@ -11,6 +15,23 @@ 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 {
@@ -24,6 +45,51 @@ 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
}

View File

@@ -1,4 +1,4 @@
use std::{path::PathBuf, sync::Arc};
use std::{fmt::Write, path::PathBuf, sync::Arc};
use conduwuit::{
Err, Result,
@@ -153,3 +153,97 @@ 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
}

View File

@@ -52,4 +52,10 @@ pub enum ServerCommand {
/// Shutdown the server
Shutdown,
/// List features built into the server
ListFeatures,
/// Build information
BuildInfo,
}

View File

@@ -3,7 +3,10 @@
fmt::Write as _,
};
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room, remote_leave_room};
use api::client::{
full_user_deactivate, join_room_by_id_helper, leave_room, recreate_push_rules_and_return,
remote_leave_room,
};
use conduwuit::{
Err, Result, debug_warn, error, info,
matrix::{Event, pdu::PduBuilder},
@@ -11,6 +14,7 @@
warn,
};
use futures::{FutureExt, StreamExt};
use lettre::Address;
use ruma::{
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, UserId,
events::{
@@ -1094,3 +1098,117 @@ 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
}

View File

@@ -35,6 +35,24 @@ 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.
@@ -239,4 +257,10 @@ 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,
},
}

View File

@@ -2,6 +2,7 @@
name = "conduwuit_api"
description.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
@@ -76,8 +77,10 @@ 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
@@ -85,6 +88,7 @@ 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

View File

@@ -1,980 +0,0 @@
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(&notice).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(&notice).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(())
}

View File

@@ -0,0 +1,436 @@
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(())
}

View File

@@ -0,0 +1,626 @@
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(&notice).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(&notice).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(&params).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))
}

View File

@@ -0,0 +1,153 @@
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,
})
}

View File

@@ -30,8 +30,10 @@ pub(crate) async fn get_capabilities_route(
default: services.server.config.default_room_version.clone(),
};
// we do not implement 3PID stuff
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability { enabled: false };
// Only allow 3pid changes if SMTP is configured
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability {
enabled: services.mailer.mailer().is_some(),
};
capabilities.get_login_token = GetLoginTokenCapability {
enabled: services.server.config.login_via_existing_session,
@@ -51,7 +53,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 })

View File

@@ -1,17 +1,15 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Error, Result, debug, err, utils};
use conduwuit::{Err, 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},
error::ErrorKind,
uiaa::{AuthFlow, AuthType, UiaaInfo},
api::client::device::{
self, delete_device, delete_devices, get_device, get_devices, update_device,
},
};
use service::uiaa::Identity;
use super::SESSION_ID_LENGTH;
use crate::{Ruma, client::DEVICE_ID_LENGTH};
/// # `GET /_matrix/client/r0/devices`
@@ -123,7 +121,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, sender_device) = body.sender();
let sender_user = body.sender_user();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
@@ -139,41 +137,11 @@ pub(crate) async fn delete_device_route(
return Ok(delete_device::v3::Response {});
}
// 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.")));
},
},
}
// 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?;
services
.users
@@ -200,7 +168,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, sender_device) = body.sender();
let sender_user = body.sender_user();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
@@ -215,41 +183,11 @@ pub(crate) async fn delete_devices_route(
return Ok(delete_devices::v3::Response {});
}
// 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."));
},
},
}
// 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?;
for device_id in &body.devices {
services.users.remove_device(sender_user, device_id).await;

View File

@@ -7,7 +7,6 @@
use conduwuit::{
Err, Error, Result, debug, debug_warn, err,
result::NotFound,
utils,
utils::{IterStream, stream::WidebandExt},
};
use conduwuit_service::{Services, users::parse_master_key};
@@ -22,7 +21,6 @@
upload_signatures::{self},
upload_signing_keys,
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
federation,
},
@@ -30,8 +28,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`
@@ -174,16 +172,7 @@ 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, 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,
};
let sender_user = body.sender_user();
match check_for_new_keys(
services,
@@ -207,32 +196,10 @@ pub(crate) async fn upload_signing_keys_route(
// Some of the keys weren't found, so we let them upload
},
| _ => {
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."));
},
},
}
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
},
}

View File

@@ -785,6 +785,15 @@ 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);
}

View File

@@ -64,6 +64,7 @@
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::*;
@@ -92,6 +93,3 @@
/// 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;

View File

@@ -7,7 +7,7 @@
};
use conduwuit_service::Services;
use futures::{
StreamExt, TryStreamExt,
FutureExt, StreamExt, TryStreamExt,
future::{join, join3, join4},
};
use ruma::{
@@ -51,6 +51,7 @@ 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 {
@@ -149,6 +150,7 @@ pub(crate) async fn set_avatar_url_route(
body.blurhash.clone(),
&all_joined_rooms,
)
.boxed()
.await;
if services.config.allow_local_presence {
@@ -344,7 +346,9 @@ pub async fn update_displayname(
.collect()
.await;
update_all_rooms(services, all_joined_rooms, user_id).await;
update_all_rooms(services, all_joined_rooms, user_id)
.boxed()
.await;
}
pub async fn update_avatar_url(
@@ -394,7 +398,9 @@ pub async fn update_avatar_url(
.collect()
.await;
update_all_rooms(services, all_joined_rooms, user_id).await;
update_all_rooms(services, all_joined_rooms, user_id)
.boxed()
.await;
}
pub async fn update_all_rooms(

View File

@@ -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
async fn recreate_push_rules_and_return(
pub async fn recreate_push_rules_and_return(
services: &Services,
sender_user: &ruma::UserId,
) -> Result<get_pushrules_all::v3::Response> {

View File

@@ -137,6 +137,7 @@ pub(crate) async fn upgrade_room_route(
Some(&body.room_id),
&state_lock,
)
.boxed()
.await?;
// Change lock to replacement room
drop(state_lock);

View File

@@ -4,12 +4,13 @@
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Error, Result, debug, err, info,
utils::{self, ReadyExt, hash},
utils::{self, ReadyExt, hash, stream::BroadbandExt},
warn,
};
use conduwuit_core::{debug_error, debug_warn};
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
use conduwuit_service::Services;
use futures::StreamExt;
use lettre::Address;
use ruma::{
OwnedUserId, UserId,
api::client::{
@@ -26,9 +27,10 @@
},
logout, logout_all,
},
uiaa,
uiaa::UserIdentifier,
},
};
use service::uiaa::Identity;
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::Ruma;
@@ -80,7 +82,7 @@ pub(crate) async fn password_login(
.password_hash(lowercased_user_id)
.await
.map(|hash| (hash, lowercased_user_id))
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?,
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?,
};
if hash.is_empty() {
@@ -89,7 +91,7 @@ pub(crate) async fn password_login(
hash::verify_password(password, &hash)
.inspect_err(|e| debug_error!("{e}"))
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?;
Ok(user_id.to_owned())
}
@@ -161,28 +163,38 @@ pub(super) async fn ldap_login(
pub(crate) async fn handle_login(
services: &Services,
body: &Ruma<login::v3::Request>,
identifier: Option<&uiaa::UserIdentifier>,
identifier: Option<&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 =
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}")))))?;
UserId::parse_with_server_name(user_id_or_localpart, &services.config.server_name)
.map_err(|_| err!(Request(InvalidUsername("User ID is malformed"))))?;
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)
@@ -244,7 +256,7 @@ pub(crate) async fn login_route(
password,
user,
..
}) => handle_login(&services, &body, identifier.as_ref(), password, user.as_ref()).await?,
}) => handle_login(&services, 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 {
@@ -264,7 +276,7 @@ pub(crate) async fn login_route(
};
let user_id =
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
if let Some(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)
@@ -273,7 +285,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(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
.map_err(|_| err!(Request(InvalidUsername(warn!("User ID is malformed")))))?;
if !services.globals.user_is_local(&user_id) {
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
@@ -370,45 +382,13 @@ pub(crate) async fn login_token_route(
return Err!(Request(Forbidden("Login via an existing session is not enabled")));
}
// 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();
let sender_user = body.sender_user();
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.")));
},
},
}
// 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 login_token = utils::random_string(TOKEN_LENGTH);
let expires_in = services.users.create_login_token(sender_user, &login_token);
@@ -434,9 +414,28 @@ 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(body.sender_user(), body.sender_device())
.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;
})
.await;
Ok(logout::v3::Response::new())
@@ -461,10 +460,18 @@ 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(body.sender_user())
.for_each(|device_id| services.users.remove_device(body.sender_user(), device_id))
.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;
})
.await;
Ok(logout_all::v3::Response::new())

View File

@@ -60,6 +60,7 @@ pub(crate) async fn send_state_event_for_key_route(
None
},
)
.boxed()
.await?,
})
}

View File

@@ -3,7 +3,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Result};
use futures::StreamExt;
use futures::{FutureExt, StreamExt};
use ruma::{
OwnedRoomId,
api::{
@@ -112,6 +112,7 @@ 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 {
@@ -127,7 +128,9 @@ pub(crate) async fn set_profile_key_route(
.collect()
.await;
update_avatar_url(&services, &body.user_id, Some(mxc), None, &all_joined_rooms).await;
update_avatar_url(&services, &body.user_id, Some(mxc), None, &all_joined_rooms)
.boxed()
.await;
} else {
services.users.set_profile_key(
&body.user_id,
@@ -178,7 +181,9 @@ pub(crate) async fn delete_profile_key_route(
.collect()
.await;
update_displayname(&services, &body.user_id, None, &all_joined_rooms).await;
update_displayname(&services, &body.user_id, None, &all_joined_rooms)
.boxed()
.await;
} else if body.key_name == "avatar_url" {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
@@ -188,7 +193,9 @@ pub(crate) async fn delete_profile_key_route(
.collect()
.await;
update_avatar_url(&services, &body.user_id, None, None, &all_joined_rooms).await;
update_avatar_url(&services, &body.user_id, None, None, &all_joined_rooms)
.boxed()
.await;
} else {
services
.users

View File

@@ -2,7 +2,7 @@
use conduwuit::{Error, Result};
use ruma::api::client::{
discovery::{
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
discover_homeserver::{self, HomeserverInfo},
discover_support::{self, Contact},
},
error::ErrorKind,
@@ -23,9 +23,9 @@ pub(crate) async fn well_known_client(
};
Ok(discover_homeserver::Response {
homeserver: HomeserverInfo { base_url: client_url.clone() },
homeserver: HomeserverInfo { base_url: client_url },
identity_server: None,
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
sliding_sync_proxy: None,
tile_server: None,
rtc_foci: services
.config
@@ -71,6 +71,7 @@ pub(crate) async fn well_known_support(
let email_address = services.config.well_known.support_email.clone();
let matrix_id = services.config.well_known.support_mxid.clone();
let pgp_key = services.config.well_known.support_pgp_key.clone();
// TODO: support defining multiple contacts in the config
let mut contacts: Vec<Contact> = vec![];
@@ -88,6 +89,7 @@ pub(crate) async fn well_known_support(
role: role_value.clone(),
email_address: email_address.clone(),
matrix_id: matrix_id.clone(),
pgp_key: pgp_key.clone(),
});
}
@@ -104,6 +106,7 @@ pub(crate) async fn well_known_support(
role: role_value.clone(),
email_address: None,
matrix_id: Some(user_id.to_owned()),
pgp_key: None,
});
}
}

View File

@@ -3,6 +3,9 @@
extern crate conduwuit_core as conduwuit;
extern crate conduwuit_service as service;
conduwuit_macros::introspect_crate! {}
pub mod client;
pub mod router;
pub mod server;

View File

@@ -28,7 +28,8 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::appservice_ping)
.ruma_route(&client::get_supported_versions_route)
.ruma_route(&client::get_register_available_route)
.ruma_route(&client::register_route)
.ruma_route(&client::register::register_route)
.ruma_route(&client::register::request_registration_token_via_email_route)
.ruma_route(&client::get_login_types_route)
.ruma_route(&client::login_route)
.ruma_route(&client::login_token_route)
@@ -36,10 +37,13 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::logout_route)
.ruma_route(&client::logout_all_route)
.ruma_route(&client::change_password_route)
.ruma_route(&client::request_password_change_token_via_email_route)
.ruma_route(&client::deactivate_route)
.ruma_route(&client::third_party_route)
.ruma_route(&client::request_3pid_management_token_via_email_route)
.ruma_route(&client::request_3pid_management_token_via_msisdn_route)
.ruma_route(&client::threepid::third_party_route)
.ruma_route(&client::threepid::request_3pid_management_token_via_email_route)
.ruma_route(&client::threepid::request_3pid_management_token_via_msisdn_route)
.ruma_route(&client::threepid::add_3pid_route)
.ruma_route(&client::threepid::delete_3pid_route)
.ruma_route(&client::check_registration_token_validity)
.ruma_route(&client::get_capabilities_route)
.ruma_route(&client::get_pushrules_all_route)

View File

@@ -2,14 +2,13 @@
use axum::{body::Body, extract::FromRequest};
use bytes::{BufMut, Bytes, BytesMut};
use conduwuit::{Error, Result, debug, debug_warn, err, trace, utils::string::EMPTY};
use conduwuit::{Error, Result, debug, debug_warn, err, trace};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
OwnedUserId, ServerName, UserId, api::IncomingRequest,
};
use service::Services;
use super::{auth, auth::Auth, request, request::Request};
use super::{auth, request, request::Request};
use crate::{State, service::appservice::RegistrationInfo};
/// Extractor for Ruma request structs
@@ -108,7 +107,7 @@ async fn from_request(
}
let auth = auth::auth(services, &mut request, json_body.as_ref(), &T::METADATA).await?;
Ok(Self {
body: make_body::<T>(services, &mut request, json_body.as_mut(), &auth)?,
body: make_body::<T>(&mut request, json_body.as_mut())?,
origin: auth.origin,
sender_user: auth.sender_user,
sender_device: auth.sender_device,
@@ -118,16 +117,11 @@ async fn from_request(
}
}
fn make_body<T>(
services: &Services,
request: &mut Request,
json_body: Option<&mut CanonicalJsonValue>,
auth: &Auth,
) -> Result<T>
fn make_body<T>(request: &mut Request, json_body: Option<&mut CanonicalJsonValue>) -> Result<T>
where
T: IncomingRequest,
{
let body = take_body(services, request, json_body, auth);
let body = take_body(request, json_body);
let http_request = into_http_request(request, body);
T::try_from_http_request(http_request, &request.path)
.map_err(|e| err!(Request(BadJson(debug_warn!("{e}")))))
@@ -151,38 +145,11 @@ fn into_http_request(request: &Request, body: Bytes) -> hyper::Request<Bytes> {
}
#[allow(clippy::needless_pass_by_value)]
fn take_body(
services: &Services,
request: &mut Request,
json_body: Option<&mut CanonicalJsonValue>,
auth: &Auth,
) -> Bytes {
fn take_body(request: &mut Request, json_body: Option<&mut CanonicalJsonValue>) -> Bytes {
let Some(CanonicalJsonValue::Object(json_body)) = json_body else {
return mem::take(&mut request.body);
};
let user_id = auth.sender_user.clone().unwrap_or_else(|| {
let server_name = services.globals.server_name();
UserId::parse_with_server_name(EMPTY, server_name).expect("valid user_id")
});
let uiaa_request = json_body
.get("auth")
.and_then(CanonicalJsonValue::as_object)
.and_then(|auth| auth.get("session"))
.and_then(CanonicalJsonValue::as_str)
.and_then(|session| {
services
.uiaa
.get_uiaa_request(&user_id, auth.sender_device.as_deref(), session)
});
if let Some(CanonicalJsonValue::Object(initial_request)) = uiaa_request {
for (key, value) in initial_request {
json_body.entry(key).or_insert(value);
}
}
let mut buf = BytesMut::new().writer();
serde_json::to_writer(&mut buf, &json_body).expect("value serialization can't fail");
buf.into_inner().freeze()

View File

@@ -1,11 +1,13 @@
use std::borrow::ToOwned;
use axum::extract::State;
use conduwuit::{Err, Error, Result, debug, debug_info, info, matrix::pdu::PduBuilder, warn};
use conduwuit::{
Err, Error, Result, debug, debug_info, info, matrix::pdu::PduBuilder, utils, warn,
};
use conduwuit_service::Services;
use futures::StreamExt;
use ruma::{
CanonicalJsonObject, OwnedUserId, RoomId, RoomVersionId, UserId,
OwnedUserId, RoomId, RoomVersionId, UserId,
api::{client::error::ErrorKind, federation::membership::prepare_join_event},
events::{
StateEventType,
@@ -40,6 +42,7 @@ pub(crate) async fn create_join_event_template_route(
{
info!(
origin = body.origin().as_str(),
room_id = %body.room_id,
"Refusing to serve make_join for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
@@ -133,10 +136,10 @@ pub(crate) async fn create_join_event_template_route(
}
}
let (_pdu, mut pdu_json) = services
let (pdu, _) = services
.rooms
.timeline
.create_hash_and_sign_event(
.create_event(
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
join_authorized_via_users_server,
..RoomMemberEventContent::new(MembershipState::Join)
@@ -147,6 +150,8 @@ pub(crate) async fn create_join_event_template_route(
)
.await?;
drop(state_lock);
let mut pdu_json = utils::to_canonical_object(&pdu)
.expect("Barebones PDU should be convertible to canonical JSON");
pdu_json.remove("event_id");
Ok(prepare_join_event::v1::Response {
@@ -297,18 +302,3 @@ pub(crate) async fn user_can_perform_restricted_join(
)))
}
}
pub(crate) fn maybe_strip_event_id(
pdu_json: &mut CanonicalJsonObject,
room_version_id: &RoomVersionId,
) -> Result {
use RoomVersionId::*;
match room_version_id {
| V1 | V2 => Ok(()),
| _ => {
pdu_json.remove("event_id");
Ok(())
},
}
}

View File

@@ -1,6 +1,6 @@
use RoomVersionId::*;
use axum::extract::State;
use conduwuit::{Err, Error, Result, debug_warn, info, matrix::pdu::PduBuilder, warn};
use conduwuit::{Err, Error, Result, debug_warn, info, matrix::pdu::PduBuilder, utils, warn};
use ruma::{
RoomVersionId,
api::{client::error::ErrorKind, federation::knock::create_knock_event_template},
@@ -28,6 +28,7 @@ pub(crate) async fn create_knock_event_template_route(
{
info!(
origin = body.origin().as_str(),
room_id = %body.room_id,
"Refusing to serve make_knock for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
@@ -98,10 +99,10 @@ pub(crate) async fn create_knock_event_template_route(
}
}
let (_pdu, mut pdu_json) = services
let (pdu, _) = services
.rooms
.timeline
.create_hash_and_sign_event(
.create_event(
PduBuilder::state(
body.user_id.to_string(),
&RoomMemberEventContent::new(MembershipState::Knock),
@@ -113,9 +114,9 @@ pub(crate) async fn create_knock_event_template_route(
.await?;
drop(state_lock);
// room v3 and above removed the "event_id" field from remote PDU format
super::maybe_strip_event_id(&mut pdu_json, &room_version_id)?;
let mut pdu_json = utils::to_canonical_object(&pdu)
.expect("Barebones PDU should be convertible to canonical JSON");
pdu_json.remove("event_id");
Ok(create_knock_event_template::v1::Response {
room_version: room_version_id,

View File

@@ -1,12 +1,11 @@
use axum::extract::State;
use conduwuit::{Err, Result, info, matrix::pdu::PduBuilder};
use conduwuit::{Err, Result, info, matrix::pdu::PduBuilder, utils};
use ruma::{
api::federation::membership::prepare_leave_event,
events::room::member::{MembershipState, RoomMemberEventContent},
};
use serde_json::value::to_raw_value;
use super::make_join::maybe_strip_event_id;
use crate::Ruma;
/// # `GET /_matrix/federation/v1/make_leave/{roomId}/{eventId}`
@@ -49,10 +48,10 @@ pub(crate) async fn create_leave_event_template_route(
let room_version_id = services.rooms.state.get_room_version(&body.room_id).await?;
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let (_pdu, mut pdu_json) = services
let (pdu, _) = services
.rooms
.timeline
.create_hash_and_sign_event(
.create_event(
PduBuilder::state(
body.user_id.to_string(),
&RoomMemberEventContent::new(MembershipState::Leave),
@@ -64,9 +63,9 @@ pub(crate) async fn create_leave_event_template_route(
.await?;
drop(state_lock);
// room v3 and above removed the "event_id" field from remote PDU format
maybe_strip_event_id(&mut pdu_json, &room_version_id)?;
let mut pdu_json = utils::to_canonical_object(&pdu)
.expect("Barebones PDU should be convertible to canonical JSON");
pdu_json.remove("event_id");
Ok(prepare_leave_event::v1::Response {
room_version: Some(room_version_id),

View File

@@ -187,13 +187,14 @@ async fn create_join_event(
"Joining user did not pass restricted room's rules."
)));
}
}
trace!("Signing send_join event");
services
.server_keys
.hash_and_sign_event(&mut value, &room_version_id)
.map_err(|e| err!(Request(InvalidParam(warn!("Failed to sign send_join event: {e}")))))?;
services
.server_keys
.hash_and_sign_event(&mut value, &room_version_id)
.map_err(|e| {
err!(Request(InvalidParam(warn!("Failed to sign send_join event: {e}"))))
})?;
}
let mutex_lock = services
.rooms

View File

@@ -2,6 +2,7 @@
name = "conduwuit_build_metadata"
description.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
@@ -27,6 +28,6 @@ crate-type = [
[build-dependencies]
built = { version = "0.8", features = [] }
cargo_metadata = { version = "0.23.1" }
[lints]
workspace = true

View File

@@ -1,5 +1,9 @@
use std::process::Command;
use std::{
collections::BTreeMap, env, fmt::Write as FmtWrite, fs, io::Write, path::Path,
process::Command,
};
use cargo_metadata::MetadataCommand;
fn run_git_command(args: &[&str]) -> Option<String> {
Command::new("git")
.args(args)
@@ -11,12 +15,60 @@ fn run_git_command(args: &[&str]) -> Option<String> {
.filter(|s| !s.is_empty())
}
fn get_env(env_var: &str) -> Option<String> {
match std::env::var(env_var) {
match env::var(env_var) {
| Ok(val) if !val.is_empty() => Some(val),
| _ => None,
}
}
fn main() {
println!("cargo:rerun-if-changed=Cargo.toml");
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); // Cargo.toml path
let manifest_path = Path::new(&manifest_dir).join("Cargo.toml");
let metadata = MetadataCommand::new()
.manifest_path(&manifest_path)
.no_deps()
.exec()
.expect("failed to parse `cargo metadata`");
let workspace_packages = metadata
.workspace_members
.iter()
.map(|package| {
let package = metadata.packages.iter().find(|p| p.id == *package).unwrap();
println!("cargo:rerun-if-changed={}", package.manifest_path.as_str());
package
})
.collect::<Vec<_>>();
// Extract available features from workspace packages
let mut available_features: BTreeMap<String, Vec<String>> = BTreeMap::new();
for package in &workspace_packages {
let crate_name = package
.name
.trim_start_matches("conduwuit-")
.replace('-', "_");
let features: Vec<String> = package.features.keys().cloned().collect();
if !features.is_empty() {
available_features.insert(crate_name, features);
}
}
// Generate Rust code for available features
let features_code = generate_features_code(&available_features);
let features_dst =
Path::new(&env::var("OUT_DIR").expect("OUT_DIR not set")).join("available_features.rs");
let mut features_file = fs::File::create(features_dst).unwrap();
features_file.write_all(features_code.as_bytes()).unwrap();
let dst = Path::new(&env::var("OUT_DIR").expect("OUT_DIR not set")).join("pkg.json");
let mut out_file = fs::File::create(dst).unwrap();
out_file
.write_all(format!("{workspace_packages:?}").as_bytes())
.unwrap();
// built gets the default crate from the workspace. Not sure if this is intended
// behavior, but it's what we want.
built::write_built_file().expect("Failed to acquire build-time information");
@@ -91,3 +143,30 @@ fn main() {
println!("cargo:rerun-if-env-changed=GIT_REMOTE_URL");
println!("cargo:rerun-if-env-changed=GIT_REMOTE_COMMIT_URL");
}
fn generate_features_code(features: &BTreeMap<String, Vec<String>>) -> String {
let mut code = String::from(
r#"
/// All available features for workspace crates
pub const WORKSPACE_FEATURES: &[(&str, &[&str])] = &[
"#,
);
for (crate_name, feature_list) in features {
write!(code, " (\"{crate_name}\", &[").unwrap();
for (i, feature) in feature_list.iter().enumerate() {
if i > 0 {
code.push_str(", ");
}
write!(code, "\"{feature}\"").unwrap();
}
code.push_str("]),\n");
}
code.push_str(
r#"];
"#,
);
code
}

View File

@@ -2,6 +2,10 @@ pub mod built {
include!(concat!(env!("OUT_DIR"), "/built.rs"));
}
// Include generated available features
// This provides: pub const WORKSPACE_FEATURES: &[(&str, &[&str])]
include!(concat!(env!("OUT_DIR"), "/available_features.rs"));
pub static GIT_COMMIT_HASH: Option<&str> = option_env!("GIT_COMMIT_HASH");
pub static GIT_COMMIT_HASH_SHORT: Option<&str> = option_env!("GIT_COMMIT_HASH_SHORT");

View File

@@ -2,6 +2,7 @@
name = "conduwuit_core"
description.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
@@ -84,6 +85,7 @@ libc.workspace = true
libloading.workspace = true
libloading.optional = true
log.workspace = true
lettre.workspace = true
num-traits.workspace = true
rand.workspace = true
rand_core = { version = "0.6.4", features = ["getrandom"] }

View File

@@ -4,7 +4,7 @@
pub mod proxy;
use std::{
collections::{BTreeMap, BTreeSet},
collections::{BTreeMap, BTreeSet, HashMap},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::PathBuf,
};
@@ -16,12 +16,13 @@
};
use figment::providers::{Env, Format, Toml};
pub use figment::{Figment, value::Value as FigmentValue};
use lettre::message::Mailbox;
use regex::RegexSet;
use ruma::{
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
api::client::discovery::{discover_homeserver::RtcFocusInfo, discover_support::ContactRole},
};
use serde::{Deserialize, de::IgnoredAny};
use serde::{Deserialize, Serialize, de::IgnoredAny};
use url::Url;
use self::proxy::ProxyConfig;
@@ -145,6 +146,10 @@ pub struct Config {
/// 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
///
@@ -650,6 +655,20 @@ pub struct Config {
/// even if `recaptcha_site_key` is set.
pub recaptcha_private_site_key: Option<String>,
/// 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" }
/// ```
///
/// default: {}
#[serde(default)]
pub registration_terms: HashMap<String, HashMap<String, TermsDocument>>,
/// Controls whether encrypted rooms and events are allowed.
#[serde(default = "true_fn")]
pub allow_encryption: bool,
@@ -756,6 +775,9 @@ pub struct Config {
#[serde(default)]
pub well_known: WellKnownConfig,
/// display: nested
pub smtp: Option<SmtpConfig>,
/// Enable OpenTelemetry OTLP tracing export. This replaces the deprecated
/// Jaeger exporter. Traces will be sent via OTLP to a collector (such as
/// Jaeger) that supports the OpenTelemetry Protocol.
@@ -2183,6 +2205,10 @@ pub struct WellKnownConfig {
/// listed.
pub support_mxid: Option<OwnedUserId>,
/// PGP key URI for server support contacts, to be served as part of the
/// MSC1929 server support endpoint.
pub support_pgp_key: Option<String>,
/// **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
///
/// A list of MatrixRTC foci URLs which will be served as part of the
@@ -2440,6 +2466,59 @@ pub struct DraupnirConfig {
pub secret: String,
}
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.smtp",
optional = "true"
)]
pub struct SmtpConfig {
/// 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
pub connection_uri: String,
/// 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
pub sender: Mailbox,
/// Whether to require that users provide an email address when they
/// register.
///
/// default: false
#[serde(default)]
pub require_email_for_registration: bool,
/// Whether to require that users who register with a registration token
/// provide an email address.
///
/// default: false
#[serde(default)]
pub require_email_for_token_registration: bool,
}
/// A policy document for use with a m.login.terms stage.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TermsDocument {
pub name: String,
pub url: String,
}
const DEPRECATED_KEYS: &[&str] = &[
"cache_capacity",
"conduit_cache_capacity_modifier",

View File

@@ -77,6 +77,14 @@ macro_rules! err {
)
};
(Request($variant:ident($($args:tt)+), $status_code:ident)) => {
$crate::error::Error::Request(
$crate::ruma::api::client::error::ErrorKind::$variant,
$crate::format_maybe!($($args)+),
$crate::http::StatusCode::$status_code,
)
};
(Config($item:literal, $($args:tt)+)) => {{
let mut buf = String::new();
$crate::error::Error::Config($item, $crate::err_log!(buf, error, config = %$item, $($args)+))

View File

@@ -0,0 +1,7 @@
//! Information about features the crates were compiled with.
//! Only available for crates that have called the `introspect_crate` macro
use std::collections::BTreeMap;
pub static ENABLED_FEATURES: std::sync::Mutex<BTreeMap<&str, &[&str]>> =
std::sync::Mutex::new(BTreeMap::new());

View File

@@ -1,3 +1,4 @@
pub mod introspection;
pub mod room_version;
pub mod version;

View File

@@ -19,6 +19,7 @@
pub use ::smallvec;
pub use ::toml;
pub use ::tracing;
pub use conduwuit_build_metadata as build_metadata;
pub use config::Config;
pub use error::Error;
pub use info::{
@@ -34,6 +35,8 @@
pub use crate as conduwuit_core;
conduwuit_macros::introspect_crate! {}
#[cfg(any(not(conduwuit_mods), not(feature = "conduwuit_mods")))]
pub mod mods {
#[macro_export]

View File

@@ -2,6 +2,7 @@
name = "conduwuit_database"
description.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
@@ -54,7 +55,9 @@ bindgen-runtime = [
[dependencies]
async-channel.workspace = true
conduwuit-core.workspace = true
conduwuit-macros.workspace = true
const-str.workspace = true
ctor.workspace = true
futures.workspace = true
log.workspace = true
minicbor.workspace = true

View File

@@ -415,13 +415,6 @@ fn deserialize_ignored_any<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Valu
tracing::instrument(level = "trace", skip_all, fields(?self.buf))
)]
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
debug_assert_eq!(
conduwuit::debug::type_name::<V>(),
"serde_json::value::de::<impl serde_core::de::Deserialize for \
serde_json::value::Value>::deserialize::ValueVisitor",
"deserialize_any: type not expected"
);
match self.record_peek_byte() {
| Some(b'{') => self.deserialize_map(visitor),
| Some(b'[') => serde_json::Deserializer::from_slice(self.record_next())

Some files were not shown because too many files have changed in this diff Show More