mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-04 23:21:37 +00:00
Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e713c7c9e1 | |||
| 5901efef71 | |||
| a5cb5d812b | |||
| fc93f79a0c | |||
| 8d1e6a167e | |||
| 3746f3c80b | |||
| d495ad2541 | |||
| be1c344787 | |||
| d1c7ee8769 | |||
| d6a2760903 | |||
| 8b762cf2e6 | |||
| 1ce9ae2cbf | |||
| 6a3370005e | |||
| 675cfb964a | |||
| 09312791a7 | |||
| 087d8b1016 | |||
| 6155dd2726 | |||
| 688cd8f46a | |||
| 3ab1f102dd | |||
| 480a32e4d4 | |||
| fadd559837 | |||
| 79c63c17fc | |||
| cdc772ba10 | |||
| 5f1b80a47c | |||
| 0f8b56f521 | |||
| 67d8d72506 | |||
| fcfa7b8bef | |||
| 0cc1e4685c | |||
| 3d2915093c | |||
| e1c54f4dec | |||
| 0c9fa3b7e5 | |||
| a95b488e6a | |||
| 4f8833e937 | |||
| f32599e030 | |||
| b6f0b41d3d | |||
| d5675b85cf | |||
| 951b5abe19 | |||
| a325ad16f1 | |||
| f93a1cc506 | |||
| 6e8dbcbfab | |||
| 97458207e5 | |||
| ab8929e2fa | |||
| 166d7d0f63 | |||
| 20a6f0c6fb | |||
| 3885e43b5d | |||
| ef7ad6082c | |||
| 717d319708 | |||
| 0b04757bef | |||
| f2b7dd6519 | |||
| 9d06208a7a | |||
| 955da3a74f | |||
| 7e79a544cf | |||
| f5db4d17d6 | |||
| 54fd1d313f | |||
| bb7fd9efc1 | |||
| aa79072411 | |||
| 8b72c5eb11 | |||
| e5cfc503d8 | |||
| 07d5081008 | |||
| dba7f47972 | |||
| 0a2d4e1cb2 | |||
| f45857acd4 | |||
| 9209b847f6 | |||
| cf9c2c23b6 | |||
| 1bd161a306 | |||
| 0a0206e866 | |||
| e6f31d7d4f | |||
| f0c3fdfe3a | |||
| 3c3314b498 | |||
| 8e7846c644 | |||
| 3ebaba920f | |||
| 19e620c8c6 | |||
| 300b6d81e7 | |||
| ed81dfc6cd | |||
| 2ffafc17d2 | |||
| 8589563a2f | |||
| 27d806e961 | |||
| 7aa02a1cd9 | |||
| fc342f5401 | |||
| ef089c1800 | |||
| 279c505af9 | |||
| f9058ee062 | |||
| 6c856bd1a4 | |||
| 4dbda8692c | |||
| 075914d8e8 | |||
| a2a644194b | |||
| 093ef742c3 | |||
| 010daf079d | |||
| 58c4f5d5b5 | |||
| c78a72bbef | |||
| 7e8f1ffd63 | |||
| 3d0b886ab8 | |||
| 2e7bfea240 | |||
| b9456c1130 | |||
| 3ce6e909dd | |||
| 3b4b401a51 | |||
| 260b88975d | |||
| be8e3772c1 | |||
| 8b91db2918 | |||
| 34758c52cc | |||
| 8b8c015dcc | |||
| 9afe5f6bed | |||
| fe03b3b8b7 | |||
| a04ef6d686 | |||
| fd807ff1f6 | |||
| b0632dde41 | |||
| cc3a8a1d40 | |||
| 30a540d8bc | |||
| 6d0832a6ee | |||
| 119aa6476d | |||
| b9854662f3 | |||
| dab50b1ec3 | |||
| 0338539221 | |||
| e94e614498 | |||
| 098e8a0b92 | |||
| 1c3890476a | |||
| 8ef6f02ee9 | |||
| 11020df89d | |||
| 47e3738807 | |||
| 8afb19757e | |||
| de3dfb2bea | |||
| bbb2615f2c | |||
| af1b4de231 | |||
| 677c407755 | |||
| e3ae714248 | |||
| fb9a2aa4d6 | |||
| 5164822090 | |||
| 6b013bcf60 |
@@ -75,7 +75,7 @@ runs:
|
||||
|
||||
- name: Set up QEMU
|
||||
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Login to builtin registry
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
name: Check Changelog
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
check-changelog:
|
||||
name: Check for changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
sparse-checkout: .
|
||||
|
||||
- name: Check for changelog entry
|
||||
id: check_files
|
||||
run: |
|
||||
git fetch origin ${GITHUB_BASE_REF}
|
||||
|
||||
# Check for Added (A) or Modified (M) files in changelog.d
|
||||
CHANGELOG_CHANGES=$(git diff --name-status origin/${GITHUB_BASE_REF} HEAD -- changelog.d/)
|
||||
|
||||
SRC_CHANGES=$(git diff --name-status origin/${GITHUB_BASE_REF} HEAD -- src/)
|
||||
|
||||
echo "Changes in changelog.d/:"
|
||||
echo "$CHANGELOG_CHANGES"
|
||||
echo "Changes in src/:"
|
||||
echo "$SRC_CHANGES"
|
||||
|
||||
if echo "$CHANGELOG_CHANGES" | grep -q "^[AM]"; then
|
||||
echo "has_changelog=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_changelog=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ -n "$SRC_CHANGES" ]; then
|
||||
echo "src_changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "src_changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Manage PR Comment
|
||||
uses: https://github.com/actions/github-script@v8
|
||||
env:
|
||||
HAS_CHANGELOG: ${{ steps.check_files.outputs.has_changelog }}
|
||||
SRC_CHANGED: ${{ steps.check_files.outputs.src_changed }}
|
||||
with:
|
||||
script: |
|
||||
const hasChangelog = process.env.HAS_CHANGELOG === 'true';
|
||||
const srcChanged = process.env.SRC_CHANGED === 'true';
|
||||
const commentSignature = '<!-- changelog-check-action -->';
|
||||
const commentBody = `${commentSignature}\nPlease add a changelog fragment to \`changelog.d/\` describing your changes.`;
|
||||
|
||||
const { data: currentUser } = await github.rest.users.getAuthenticated();
|
||||
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const botComment = comments.find(comment =>
|
||||
comment.user.id === currentUser.id &&
|
||||
comment.body.includes(commentSignature)
|
||||
);
|
||||
|
||||
const shouldWarn = srcChanged && !hasChangelog;
|
||||
|
||||
if (!shouldWarn) {
|
||||
if (botComment) {
|
||||
console.log('Changelog found or not required. Deleting existing warning comment.');
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!botComment) {
|
||||
console.log('Changelog missing and required. Creating warning comment.');
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: commentBody,
|
||||
});
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
github: [JadedBlueEyes, nexy7574, gingershaped]
|
||||
custom:
|
||||
- https://ko-fi.com/nexy7574
|
||||
- https://ko-fi.com/JadedBlueEyes
|
||||
- https://timedout.uk/donate.html
|
||||
- https://jade.ellis.link/sponsors
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-131
@@ -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).
|
||||
|
||||
+34
-2
@@ -36,7 +36,7 @@ # Run all checks
|
||||
prek --all-files
|
||||
```
|
||||
|
||||
Alternatively, you can use [pre-commit](https://pre-commit.com/):
|
||||
Alternatively, you can use [pre-commit][pre-commit]:
|
||||
```bash
|
||||
# Requires python
|
||||
|
||||
@@ -52,6 +52,8 @@ # Run all checks manually
|
||||
|
||||
These same checks are run in CI via the prek-checks workflow to ensure consistency. These must pass before the PR is merged.
|
||||
|
||||
[pre-commit]: https://pre-commit.com/
|
||||
|
||||
### Running tests locally
|
||||
|
||||
Tests, compilation, and linting can be run with standard Cargo commands:
|
||||
@@ -109,7 +111,7 @@ ### Writing documentation
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Continuwuity follows the [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages. This provides a standardized format that makes the commit history more readable and enables automated tools to generate changelogs.
|
||||
Continuwuity follows the [Conventional Commits][conventional-commits] specification for commit messages. This provides a standardized format that makes the commit history more readable and enables automated tools to generate changelogs.
|
||||
|
||||
The basic structure is:
|
||||
|
||||
@@ -168,6 +170,7 @@ ### Creating pull requests
|
||||
their contributions accepted. This includes users who have been banned from
|
||||
continuwuity Matrix rooms for Code of Conduct violations.
|
||||
|
||||
[conventional-commits]: https://www.conventionalcommits.org/
|
||||
[issues]: https://forgejo.ellis.link/continuwuation/continuwuity/issues
|
||||
[continuwuity-matrix]: https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org
|
||||
[complement]: https://github.com/matrix-org/complement/
|
||||
@@ -175,3 +178,32 @@ ### Creating pull requests
|
||||
[nodejs-download]: https://nodejs.org/en/download
|
||||
[rspress]: https://rspress.rs/
|
||||
[documentation.yml]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/.forgejo/workflows/documentation.yml
|
||||
|
||||
#### Writing news fragments
|
||||
|
||||
In order to make writing our changelogs easier, we make use of [Towncrier]. Towncrier builds changelogs based on
|
||||
"news fragments", which are little markdown files in the `changelog.d/` directory that describe individual changes.
|
||||
|
||||
When you make a pull request that changes functionality, fixes a bug, or adds documentation, please add a news fragment
|
||||
describing your change. The file name *MUST* be in the format of `{pull_request_number}.{type}`, where `{type}` is one
|
||||
of the following:
|
||||
|
||||
- `feature` - for new features
|
||||
- `bugfix` - for bug fixes
|
||||
- `doc` - for documentation changes
|
||||
- `misc` - for other changes that don't fit the above categories
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
$ echo "Fixed the quantum flux stabiliser. Contributed by @alice." > changelog.d/42.bugfix
|
||||
```
|
||||
|
||||
(Note: If you want to credit yourself, you should reference your forgejo handle, however links to other platforms are also acceptable.)
|
||||
|
||||
When the next release is made, Towncrier will automatically include your news fragment in the changelog.
|
||||
|
||||
You can read more about writing news fragments in the [Towncrier tutorial][tt].
|
||||
|
||||
[Towncrier]: https://towncrier.readthedocs.io/
|
||||
[tt]: https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
|
||||
|
||||
Generated
+315
-146
@@ -72,6 +72,12 @@ dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@@ -92,6 +98,15 @@ dependencies = [
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi-width"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "219e3ce6f2611d83b51ec2098a12702112c29e57203a6b0a0929b2cddb486608"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
@@ -104,6 +119,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "ar_archive_writer"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
|
||||
dependencies = [
|
||||
"object",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -271,8 +295,8 @@ checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002"
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.3.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/async-channel?rev=92e5e74063bf2a3b10414bcc8a0d68b235644280#92e5e74063bf2a3b10414bcc8a0d68b235644280"
|
||||
version = "2.5.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/async-channel?rev=e990f0006b68dc9bace7a3c95fc90b5c4e44948d#e990f0006b68dc9bace7a3c95fc90b5c4e44948d"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
@@ -811,6 +835,16 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chumsky"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
"stacker",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
@@ -940,6 +974,7 @@ dependencies = [
|
||||
name = "conduwuit_admin"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"clap",
|
||||
"conduwuit_api",
|
||||
"conduwuit_core",
|
||||
@@ -948,6 +983,7 @@ dependencies = [
|
||||
"conduwuit_service",
|
||||
"const-str",
|
||||
"futures",
|
||||
"lettre",
|
||||
"log",
|
||||
"ruma",
|
||||
"serde-saphyr",
|
||||
@@ -961,6 +997,7 @@ dependencies = [
|
||||
name = "conduwuit_api"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-client-ip",
|
||||
@@ -977,10 +1014,12 @@ dependencies = [
|
||||
"hyper",
|
||||
"ipaddress",
|
||||
"itertools 0.14.0",
|
||||
"lettre",
|
||||
"log",
|
||||
"rand 0.10.0",
|
||||
"reqwest",
|
||||
"ruma",
|
||||
"ruminuwuity",
|
||||
"serde",
|
||||
"serde_html_form",
|
||||
"serde_json",
|
||||
@@ -1002,6 +1041,7 @@ version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"arrayvec",
|
||||
"assign",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"bytes",
|
||||
@@ -1022,8 +1062,10 @@ dependencies = [
|
||||
"hardened_malloc-rs",
|
||||
"http",
|
||||
"http-body-util",
|
||||
"hyper-util",
|
||||
"ipaddress",
|
||||
"itertools 0.14.0",
|
||||
"lettre",
|
||||
"libc",
|
||||
"libloading 0.9.0",
|
||||
"lock_api",
|
||||
@@ -1051,7 +1093,7 @@ dependencies = [
|
||||
"tikv-jemallocator",
|
||||
"tokio",
|
||||
"tokio-metrics",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
@@ -1090,6 +1132,7 @@ dependencies = [
|
||||
name = "conduwuit_router"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"axum",
|
||||
"axum-client-ip",
|
||||
"axum-server",
|
||||
@@ -1125,6 +1168,7 @@ name = "conduwuit_service"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"assign",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"blurhash",
|
||||
@@ -1134,20 +1178,24 @@ dependencies = [
|
||||
"const-str",
|
||||
"either",
|
||||
"futures",
|
||||
"governor",
|
||||
"hickory-resolver",
|
||||
"http",
|
||||
"image",
|
||||
"ipaddress",
|
||||
"itertools 0.14.0",
|
||||
"ldap3",
|
||||
"lettre",
|
||||
"log",
|
||||
"loole",
|
||||
"lru-cache",
|
||||
"nonzero_ext",
|
||||
"rand 0.10.0",
|
||||
"recaptcha-verify",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"ruma",
|
||||
"ruminuwuity",
|
||||
"rustyline-async",
|
||||
"sd-notify",
|
||||
"serde",
|
||||
@@ -1167,6 +1215,7 @@ name = "conduwuit_web"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"assign",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
@@ -1247,16 +1296,6 @@ dependencies = [
|
||||
"typewit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "continuwuity-admin-api"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
dependencies = [
|
||||
"ruma-common",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.10.0"
|
||||
@@ -1313,8 +1352,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "core_affinity"
|
||||
version = "0.8.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/core_affinity_rs?rev=9c8e51510c35077df888ee72a36b4b05637147da#9c8e51510c35077df888ee72a36b4b05637147da"
|
||||
version = "0.8.3"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/core_affinity_rs?rev=7c7a9dea35382743a63837cdd1d977efdb8f1b8a#7c7a9dea35382743a63837cdd1d977efdb8f1b8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"num_cpus",
|
||||
@@ -1682,16 +1721,6 @@ dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "draupnir-antispam"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
dependencies = [
|
||||
"ruma-common",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtor"
|
||||
version = "0.1.1"
|
||||
@@ -1747,6 +1776,22 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email-encoding"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email_address"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
@@ -1815,10 +1860,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.3.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/event-listener?rev=fe4aebeeaae435af60087ddd56b573a2e0be671d#fe4aebeeaae435af60087ddd56b573a2e0be671d"
|
||||
version = "5.4.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/event-listener?rev=b2c19bcaf5a0a69c38c034e417bda04a9b991529#b2c19bcaf5a0a69c38c034e417bda04a9b991529"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
@@ -1949,6 +1993,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -2065,6 +2115,12 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
@@ -2155,6 +2211,25 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9efcab3c1958580ff1f25a2a41be1668f7603d849bb63af523b208a3cc1223b8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-sink",
|
||||
"futures-timer",
|
||||
"futures-util",
|
||||
"hashbrown 0.16.1",
|
||||
"nonzero_ext",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"smallvec",
|
||||
"spinning_top",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
@@ -2219,13 +2294,23 @@ version = "0.1.2+12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "647deb1583b14d160f85f3ff626f20b6edd366e3852c9843b06077388f794cb6"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2233,6 +2318,11 @@ name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hdrhistogram"
|
||||
@@ -2487,13 +2577,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.17"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/hyper-util?rev=5886d5292bf704c246206ad72d010d674a7b77d0#5886d5292bf704c246206ad72d010d674a7b77d0"
|
||||
version = "0.1.20"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/hyper-util?rev=09fcd3bf4656c81a8ad573bee410ab2b57f60b86#09fcd3bf4656c81a8ad573bee410ab2b57f60b86"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
@@ -2788,30 +2877,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "js_option"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c"
|
||||
checksum = "c7dd3e281add16813cf673bf74a32249b0aa0d1c8117519a17b3ada5e8552b3c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "konst"
|
||||
version = "0.3.16"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9"
|
||||
checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d"
|
||||
dependencies = [
|
||||
"const_panic",
|
||||
"konst_kernel",
|
||||
"typewit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "konst_kernel"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c"
|
||||
dependencies = [
|
||||
"typewit",
|
||||
]
|
||||
|
||||
@@ -2891,6 +2970,37 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
|
||||
|
||||
[[package]]
|
||||
name = "lettre"
|
||||
version = "0.11.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"chumsky",
|
||||
"email-encoding",
|
||||
"email_address",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hostname",
|
||||
"httpdate",
|
||||
"idna",
|
||||
"mime",
|
||||
"nom 8.0.0",
|
||||
"percent-encoding",
|
||||
"quoted_printable",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"serde",
|
||||
"socket2 0.6.3",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
@@ -3106,16 +3216,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "meowlnir-antispam"
|
||||
version = "0.1.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
dependencies = [
|
||||
"ruma-common",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@@ -3273,6 +3373,12 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
|
||||
|
||||
[[package]]
|
||||
name = "nonzero_ext"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21"
|
||||
|
||||
[[package]]
|
||||
name = "noop_proc_macro"
|
||||
version = "0.3.0"
|
||||
@@ -4015,6 +4121,16 @@ dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8"
|
||||
dependencies = [
|
||||
"ar_archive_writer",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.13.1"
|
||||
@@ -4119,6 +4235,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quoted_printable"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
@@ -4137,6 +4259,8 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
@@ -4146,7 +4270,7 @@ version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
@@ -4161,6 +4285,16 @@ dependencies = [
|
||||
"rand_core 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
@@ -4223,7 +4357,7 @@ dependencies = [
|
||||
"paste",
|
||||
"profiling",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha",
|
||||
"rand_chacha 0.9.0",
|
||||
"simd_helpers",
|
||||
"thiserror 2.0.18",
|
||||
"v_frame",
|
||||
@@ -4390,31 +4524,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.10.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
version = "0.14.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"continuwuity-admin-api",
|
||||
"draupnir-antispam",
|
||||
"js_int",
|
||||
"js_option",
|
||||
"meowlnir-antispam",
|
||||
"ruma-appservice-api",
|
||||
"ruma-client-api",
|
||||
"ruma-common",
|
||||
"ruma-events",
|
||||
"ruma-federation-api",
|
||||
"ruma-identifiers-validation",
|
||||
"ruma-identity-service-api",
|
||||
"ruma-push-gateway-api",
|
||||
"ruma-signatures",
|
||||
"ruma-state-res",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-appservice-api"
|
||||
version = "0.10.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
version = "0.14.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4425,13 +4555,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-client-api"
|
||||
version = "0.18.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
version = "0.22.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assign",
|
||||
"bytes",
|
||||
"date_header",
|
||||
"http",
|
||||
"js_int",
|
||||
"js_option",
|
||||
@@ -4448,12 +4577,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-common"
|
||||
version = "0.13.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
version = "0.17.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"date_header",
|
||||
"form_urlencoded",
|
||||
"getrandom 0.2.17",
|
||||
"http",
|
||||
@@ -4461,14 +4591,13 @@ dependencies = [
|
||||
"js_int",
|
||||
"konst",
|
||||
"percent-encoding",
|
||||
"rand 0.10.0",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"ruma-identifiers-validation",
|
||||
"ruma-macros",
|
||||
"serde",
|
||||
"serde_html_form",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tracing",
|
||||
@@ -4476,37 +4605,34 @@ dependencies = [
|
||||
"uuid",
|
||||
"web-time",
|
||||
"wildmatch",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-events"
|
||||
version = "0.28.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
version = "0.32.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"indexmap",
|
||||
"js_int",
|
||||
"js_option",
|
||||
"percent-encoding",
|
||||
"pulldown-cmark",
|
||||
"regex",
|
||||
"ruma-common",
|
||||
"ruma-identifiers-validation",
|
||||
"ruma-macros",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"url",
|
||||
"web-time",
|
||||
"wildmatch",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-federation-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
version = "0.13.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"headers",
|
||||
@@ -4516,9 +4642,10 @@ dependencies = [
|
||||
"js_int",
|
||||
"memchr",
|
||||
"mime",
|
||||
"rand 0.10.0",
|
||||
"rand 0.8.5",
|
||||
"ruma-common",
|
||||
"ruma-events",
|
||||
"ruma-signatures",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
@@ -4527,28 +4654,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-identifiers-validation"
|
||||
version = "0.9.5"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-identity-service-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-macros"
|
||||
version = "0.13.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
version = "0.17.1"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"cfg-if",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
@@ -4556,13 +4674,13 @@ dependencies = [
|
||||
"ruma-identifiers-validation",
|
||||
"serde",
|
||||
"syn",
|
||||
"toml 0.8.23",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-push-gateway-api"
|
||||
version = "0.9.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
version = "0.13.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4573,25 +4691,50 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruma-signatures"
|
||||
version = "0.15.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=bb12ed288a31a23aa11b10ba0fad22b7f985eb88#bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
version = "0.19.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"ed25519-dalek",
|
||||
"memchr",
|
||||
"pkcs8",
|
||||
"rand 0.10.0",
|
||||
"rand_core 0.6.4",
|
||||
"rand 0.8.5",
|
||||
"ruma-common",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"subslice",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma-state-res"
|
||||
version = "0.15.0"
|
||||
source = "git+https://github.com/ruma/ruma.git?rev=e2e5fece57800a2559ab028fd775a7978f3725e9#e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
"ruma-events",
|
||||
"ruma-signatures",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruminuwuity"
|
||||
version = "0.5.7-alpha.1"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"ruma",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wildmatch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-librocksdb-sys"
|
||||
version = "0.39.0+10.5.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=61d9d23872197e9ace4a477f2617d5c9f50ecb23#61d9d23872197e9ace4a477f2617d5c9f50ecb23"
|
||||
version = "0.42.0+10.10.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9#31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"bzip2-sys",
|
||||
@@ -4607,8 +4750,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-rocksdb"
|
||||
version = "0.43.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=61d9d23872197e9ace4a477f2617d5c9f50ecb23#61d9d23872197e9ace4a477f2617d5c9f50ecb23"
|
||||
version = "0.46.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9#31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"parking_lot",
|
||||
@@ -4725,16 +4868,16 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "rustyline-async"
|
||||
version = "0.4.6"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/rustyline-async?rev=e9f01cf8c6605483cb80b3b0309b400940493d7f#e9f01cf8c6605483cb80b3b0309b400940493d7f"
|
||||
version = "0.4.9"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/rustyline-async?rev=b13aca2cc08d5f78303746cd192d9a03d73e768e#b13aca2cc08d5f78303746cd192d9a03d73e768e"
|
||||
dependencies = [
|
||||
"ansi-width",
|
||||
"crossterm",
|
||||
"futures-util",
|
||||
"pin-project",
|
||||
"thingbuf",
|
||||
"thiserror 2.0.18",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5072,9 +5215,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.4"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -5247,6 +5390,15 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spinning_top"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
@@ -5263,6 +5415,19 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "stacker"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"psm",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strict"
|
||||
version = "0.2.0"
|
||||
@@ -5300,15 +5465,6 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subslice"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a8e4809a3bb02de01f1f7faf1ba01a83af9e8eabcd4d31dd6e413d14d56aae"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -5454,8 +5610,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemalloc-ctl"
|
||||
version = "0.6.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/jemallocator?rev=82af58d6a13ddd5dcdc7d4e91eae3b63292995b8#82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
version = "0.6.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/jemallocator?rev=df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554#df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"paste",
|
||||
@@ -5464,8 +5620,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemalloc-sys"
|
||||
version = "0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/jemallocator?rev=82af58d6a13ddd5dcdc7d4e91eae3b63292995b8#82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/jemallocator?rev=df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554#df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -5473,8 +5629,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tikv-jemallocator"
|
||||
version = "0.6.0"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/jemallocator?rev=82af58d6a13ddd5dcdc7d4e91eae3b63292995b8#82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
version = "0.6.1"
|
||||
source = "git+https://forgejo.ellis.link/continuwuation/jemallocator?rev=df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554#df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"tikv-jemalloc-sys",
|
||||
@@ -5630,13 +5786,26 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned 1.0.4",
|
||||
"serde_spanned 1.1.1",
|
||||
"toml_datetime 0.7.5+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_spanned 1.1.1",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
@@ -5657,9 +5826,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.0.1+spec-1.1.0"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -5685,16 +5854,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 1.0.1+spec-1.1.0",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.10+spec-1.1.0"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow 1.0.0",
|
||||
]
|
||||
@@ -5935,15 +6104,6 @@ name = "typewit"
|
||||
version = "1.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71"
|
||||
dependencies = [
|
||||
"typewit_proc_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typewit_proc_macros"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6"
|
||||
|
||||
[[package]]
|
||||
name = "uname"
|
||||
@@ -6398,6 +6558,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
|
||||
+45
-37
@@ -47,9 +47,9 @@ default-features = false
|
||||
features = ["features"]
|
||||
|
||||
[workspace.dependencies.toml]
|
||||
version = "0.9.5"
|
||||
version = "1.0.0"
|
||||
default-features = false
|
||||
features = ["parse"]
|
||||
features = ["parse", "serde"]
|
||||
|
||||
[workspace.dependencies.sanitize-filename]
|
||||
version = "0.6.0"
|
||||
@@ -278,7 +278,7 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.hyper-util]
|
||||
version = "=0.1.17"
|
||||
version = "=0.1.20"
|
||||
default-features = false
|
||||
features = [
|
||||
"server-auto",
|
||||
@@ -332,7 +332,7 @@ version = "0.4.0"
|
||||
|
||||
# used for MPMC channels
|
||||
[workspace.dependencies.async-channel]
|
||||
version = "2.3.1"
|
||||
version = "2.5.0"
|
||||
|
||||
[workspace.dependencies.async-trait]
|
||||
version = "0.1.88"
|
||||
@@ -340,55 +340,50 @@ version = "0.1.88"
|
||||
[workspace.dependencies.lru-cache]
|
||||
version = "0.1.2"
|
||||
|
||||
[workspace.dependencies.assign]
|
||||
version = "1.1.1"
|
||||
|
||||
# Used for matrix spec type definitions and helpers
|
||||
[workspace.dependencies.ruma]
|
||||
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
||||
#branch = "conduwuit-changes"
|
||||
rev = "bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
||||
# version = "0.14.1"
|
||||
git = "https://github.com/ruma/ruma.git"
|
||||
rev = "e2e5fece57800a2559ab028fd775a7978f3725e9"
|
||||
features = [
|
||||
"compat",
|
||||
"rand",
|
||||
"appservice-api-c",
|
||||
"client-api",
|
||||
"federation-api",
|
||||
"markdown",
|
||||
"push-gateway-api-c",
|
||||
"unstable-exhaustive-types",
|
||||
"state-res",
|
||||
"rand",
|
||||
"markdown",
|
||||
"ring-compat",
|
||||
"compat-upload-signatures",
|
||||
"identifiers-validation",
|
||||
"unstable-unspecified",
|
||||
"unstable-msc2448",
|
||||
"unstable-msc2666",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc2870",
|
||||
"unstable-msc3026",
|
||||
"unstable-msc3061",
|
||||
"unstable-msc3814",
|
||||
"unstable-msc3245",
|
||||
"unstable-msc3266",
|
||||
"unstable-msc3381", # polls
|
||||
"unstable-msc3489", # beacon / live location
|
||||
"unstable-msc3575",
|
||||
"unstable-msc3930", # polls push rules
|
||||
"unstable-msc4075",
|
||||
"unstable-msc4095",
|
||||
"unstable-msc4121",
|
||||
"unstable-msc4125",
|
||||
"unstable-msc4155",
|
||||
"unstable-msc4186",
|
||||
"unstable-msc4203", # sending to-device events to appservices
|
||||
"unstable-msc4210", # remove legacy mentions
|
||||
"unstable-extensible-events",
|
||||
"unstable-pdu",
|
||||
"unstable-msc4155",
|
||||
"unstable-msc4310",
|
||||
"unstable-msc4380",
|
||||
"unstable-msc4143", # livekit well_known response
|
||||
"unstable-msc4284"
|
||||
"unstable-msc4439", # pgp_key in .well_known/matrix/support
|
||||
"unstable-extensible-events",
|
||||
]
|
||||
|
||||
[workspace.dependencies.rust-rocksdb]
|
||||
git = "https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1"
|
||||
rev = "61d9d23872197e9ace4a477f2617d5c9f50ecb23"
|
||||
rev = "31fb8f772c7afcdc0061ab6a40cfa3a1be2fccd9"
|
||||
default-features = false
|
||||
features = [
|
||||
"multi-threaded-cf",
|
||||
@@ -451,7 +446,7 @@ version = "0.46.0"
|
||||
# jemalloc usage
|
||||
[workspace.dependencies.tikv-jemalloc-sys]
|
||||
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
rev = "df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554"
|
||||
default-features = false
|
||||
features = [
|
||||
"background_threads_runtime_support",
|
||||
@@ -459,7 +454,7 @@ features = [
|
||||
]
|
||||
[workspace.dependencies.tikv-jemallocator]
|
||||
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
rev = "df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554"
|
||||
default-features = false
|
||||
features = [
|
||||
"background_threads_runtime_support",
|
||||
@@ -467,7 +462,7 @@ features = [
|
||||
]
|
||||
[workspace.dependencies.tikv-jemalloc-ctl]
|
||||
git = "https://forgejo.ellis.link/continuwuation/jemallocator"
|
||||
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
|
||||
rev = "df86ff89d4b1e223b9f7d2dd2fbb7f202da7f554"
|
||||
default-features = false
|
||||
features = ["use_std"]
|
||||
|
||||
@@ -493,7 +488,7 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.rustyline-async]
|
||||
version = "0.4.3"
|
||||
version = "0.4.9"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.termimad]
|
||||
@@ -526,7 +521,7 @@ version = "0.4.13"
|
||||
version = "2.0"
|
||||
|
||||
[workspace.dependencies.core_affinity]
|
||||
version = "0.8.1"
|
||||
version = "0.8.3"
|
||||
|
||||
[workspace.dependencies.libc]
|
||||
version = "0.2"
|
||||
@@ -550,15 +545,25 @@ version = "0.12.0"
|
||||
default-features = false
|
||||
features = ["sync", "tls-rustls", "rustls-provider"]
|
||||
|
||||
[workspace.dependencies.resolv-conf]
|
||||
version = "0.7.5"
|
||||
|
||||
[workspace.dependencies.yansi]
|
||||
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
|
||||
#
|
||||
@@ -571,25 +576,25 @@ version = "0.15.0"
|
||||
# adds event for CTRL+\: https://forgejo.ellis.link/continuwuation/rustyline-async/src/branch/main/.patchy/0001-add-event-for-ctrl.patch
|
||||
[patch.crates-io.rustyline-async]
|
||||
git = "https://forgejo.ellis.link/continuwuation/rustyline-async"
|
||||
rev = "e9f01cf8c6605483cb80b3b0309b400940493d7f"
|
||||
rev = "b13aca2cc08d5f78303746cd192d9a03d73e768e"
|
||||
|
||||
# adds LIFO queue scheduling; this should be updated with PR progress.
|
||||
[patch.crates-io.event-listener]
|
||||
git = "https://forgejo.ellis.link/continuwuation/event-listener"
|
||||
rev = "fe4aebeeaae435af60087ddd56b573a2e0be671d"
|
||||
rev = "b2c19bcaf5a0a69c38c034e417bda04a9b991529"
|
||||
[patch.crates-io.async-channel]
|
||||
git = "https://forgejo.ellis.link/continuwuation/async-channel"
|
||||
rev = "92e5e74063bf2a3b10414bcc8a0d68b235644280"
|
||||
rev = "e990f0006b68dc9bace7a3c95fc90b5c4e44948d"
|
||||
|
||||
# adds affinity masks for selecting more than one core at a time
|
||||
[patch.crates-io.core_affinity]
|
||||
git = "https://forgejo.ellis.link/continuwuation/core_affinity_rs"
|
||||
rev = "9c8e51510c35077df888ee72a36b4b05637147da"
|
||||
rev = "7c7a9dea35382743a63837cdd1d977efdb8f1b8a"
|
||||
|
||||
# reverts hyperium#148 conflicting with our delicate federation resolver hooks
|
||||
[patch.crates-io.hyper-util]
|
||||
git = "https://forgejo.ellis.link/continuwuation/hyper-util"
|
||||
rev = "5886d5292bf704c246206ad72d010d674a7b77d0"
|
||||
rev = "09fcd3bf4656c81a8ad573bee410ab2b57f60b86"
|
||||
|
||||
#
|
||||
# Our crates
|
||||
@@ -646,6 +651,10 @@ default-features = false
|
||||
package = "conduwuit"
|
||||
path = "src/main"
|
||||
|
||||
[workspace.dependencies.ruminuwuity]
|
||||
package = "ruminuwuity"
|
||||
path = "src/ruminuwuity"
|
||||
|
||||
###############################################################################
|
||||
#
|
||||
# Release profiles
|
||||
@@ -919,7 +928,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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
Added support for associating email addresses with accounts, requiring email addresses for registration, and resetting passwords via email. Contributed by @ginger
|
||||
@@ -0,0 +1 @@
|
||||
Fixed room alias deletion so removing one local alias no longer removes other aliases from room alias listings.
|
||||
@@ -0,0 +1 @@
|
||||
Added Testing and Troubleshooting instructions for Livekit documentation. Contributed by @stratself.
|
||||
@@ -0,0 +1 @@
|
||||
Stripped `join_authorised_via_users_server` from json if user is already in room (@partha:cxy.run)
|
||||
@@ -0,0 +1 @@
|
||||
Fixed internal server errors for fetching thumbnails. Contributed by @PerformativeJade
|
||||
@@ -0,0 +1 @@
|
||||
Fixed error 500 when joining non-existent rooms. Contributed by @ezera.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
Added `!admin users reset-push-rules` command to reset the notification settings of users. Contributed by @nex.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed resolving IP of servers that only use SRV delegation. Contributed by @tulir.
|
||||
@@ -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
|
||||
#
|
||||
@@ -1865,6 +1869,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 +2046,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
|
||||
|
||||
+4
-4
@@ -10,18 +10,18 @@ RUN rm -f /etc/apt/apt.conf.d/docker-clean
|
||||
|
||||
# Match Rustc version as close as possible
|
||||
# rustc -vV
|
||||
ARG LLVM_VERSION=20
|
||||
ARG LLVM_VERSION=21
|
||||
# ENV RUSTUP_TOOLCHAIN=${RUST_VERSION}
|
||||
|
||||
# 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.7
|
||||
ENV BINSTALL_VERSION=1.17.8
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
|
||||
|
||||
# Developer tool versions
|
||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||
ENV BINSTALL_VERSION=1.17.7
|
||||
ENV BINSTALL_VERSION=1.17.8
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ # Calls
|
||||
For either one to work correctly, you have to do some additional setup.
|
||||
|
||||
- For legacy calls to work, you need to set up a TURN/STUN server. [Read the TURN guide for tips on how to set up coturn](./calls/turn.mdx)
|
||||
- For MatrixRTC / Element Call to work, you have to set up the LiveKit backend (foci). LiveKit also uses TURN/STUN to increase reliability, so you might want to configure your TURN server first. [Read the LiveKit guide](./calls/livekit.mdx)
|
||||
- For MatrixRTC / Element Call to work, you have to set up the LiveKit backend (foci). LiveKit also uses TURN/STUN to increase reliability - you can set up its built-in TURN server, or integrate with an existing one. [Read the LiveKit guide](./calls/livekit.mdx)
|
||||
|
||||
+190
-57
@@ -4,6 +4,10 @@ # Matrix RTC/Element Call Setup
|
||||
This guide assumes that you are using docker compose for deployment. LiveKit only provides Docker images.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
You can find help setting up MatrixRTC in our dedicated room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
|
||||
:::
|
||||
|
||||
## Instructions
|
||||
|
||||
### 1. Domain
|
||||
@@ -14,17 +18,21 @@ ### 1. Domain
|
||||
|
||||
### 2. Services
|
||||
|
||||
Using LiveKit with Matrix requires two services - Livekit itself, and a service (`lk-jwt-service`) that grants Matrix users permission to connect to it.
|
||||
Using LiveKit with Matrix requires two services - LiveKit itself, and a service (`lk-jwt-service`) that grants Matrix users permission to connect to it.
|
||||
|
||||
You must generate a key and secret to allow the Matrix service to authenticate with LiveKit. `LK_MATRIX_KEY` should be around 20 random characters, and `LK_MATRIX_SECRET` should be around 64. Remember to replace these with the actual values!
|
||||
|
||||
:::tip Generating the secrets
|
||||
LiveKit provides a utility to generate secure random keys
|
||||
```bash
|
||||
docker run --rm livekit/livekit-server:latest generate-keys
|
||||
~$ docker run --rm livekit/livekit-server:latest generate-keys
|
||||
API Key: APIUxUnMnSkuFWV
|
||||
API Secret: t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
|
||||
```
|
||||
:::
|
||||
|
||||
Create a `docker-compose.yml` file as following:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
lk-jwt-service:
|
||||
@@ -32,10 +40,11 @@ ### 2. Services
|
||||
container_name: lk-jwt-service
|
||||
environment:
|
||||
- LIVEKIT_JWT_BIND=:8081
|
||||
- LIVEKIT_URL=wss://livekit.example.com
|
||||
- LIVEKIT_KEY=LK_MATRIX_KEY
|
||||
- LIVEKIT_SECRET=LK_MATRIX_SECRET
|
||||
- LIVEKIT_FULL_ACCESS_HOMESERVERS=example.com
|
||||
- LIVEKIT_URL=wss://livekit.example.com # your LiveKit domain
|
||||
- LIVEKIT_FULL_ACCESS_HOMESERVERS=example.com # your server_name
|
||||
# Replace these with the generated values as above
|
||||
- LIVEKIT_KEY=LK_MATRIX_KEY # APIUxUnMnSkuFWV
|
||||
- LIVEKIT_SECRET=LK_MATRIX_SECRET # t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8081:8081"
|
||||
@@ -70,6 +79,8 @@ # - "50100-50200:50100-50200/udp"
|
||||
enable_loopback_candidate: false
|
||||
keys:
|
||||
LK_MATRIX_KEY: LK_MATRIX_SECRET
|
||||
# replace these with your key-secret pair. Example:
|
||||
# APIUxUnMnSkuFWV: t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
|
||||
```
|
||||
|
||||
#### Firewall hints
|
||||
@@ -95,7 +106,7 @@ ### 4. Configure your Reverse Proxy
|
||||
|
||||
Reverse proxies can be configured in many different ways - so we can't provide a step by step for this.
|
||||
|
||||
By default, all routes should be forwarded to Livekit with the exception of the following path prefixes, which should be forwarded to the JWT/Authentication service:
|
||||
All paths should be forwarded to LiveKit by default, with the exception of the following path prefixes, which should be forwarded to the JWT/Authentication service:
|
||||
|
||||
- `/sfu/get`
|
||||
- `/healthz`
|
||||
@@ -104,7 +115,7 @@ ### 4. Configure your Reverse Proxy
|
||||
<details>
|
||||
<summary>Example caddy config</summary>
|
||||
```
|
||||
matrix-rtc.example.com {
|
||||
livekit.example.com {
|
||||
|
||||
# for lk-jwt-service
|
||||
@lk-jwt-service path /sfu/get* /healthz* /get_token*
|
||||
@@ -122,7 +133,7 @@ ### 4. Configure your Reverse Proxy
|
||||
<summary>Example nginx config</summary>
|
||||
```
|
||||
server {
|
||||
server_name matrix-rtc.example.com;
|
||||
server_name livekit.example.com;
|
||||
|
||||
# for lk-jwt-service
|
||||
location ~ ^/(sfu/get|healthz|get_token) {
|
||||
@@ -133,7 +144,7 @@ ### 4. Configure your Reverse Proxy
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
# for livekit
|
||||
# for LiveKit
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:7880$request_uri;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
@@ -173,44 +184,11 @@ ### 6. Start Everything
|
||||
|
||||
Start up the services using your usual method - for example `docker compose up -d`.
|
||||
|
||||
## Additional Configuration
|
||||
## Additional TURN configuration
|
||||
|
||||
### TURN Integration
|
||||
### Using LiveKit's built-in TURN server
|
||||
|
||||
If you've already set up coturn, there may be a port clash between the two services. To fix this, make sure the `min-port` and `max-port` for coturn so it doesn't overlap with LiveKit's range:
|
||||
|
||||
```ini
|
||||
min-port=50201
|
||||
max-port=65535
|
||||
```
|
||||
|
||||
To improve LiveKit's reliability, you can configure it to use your coturn server.
|
||||
|
||||
Generate a long random secret for LiveKit, and add it to your coturn config under the `static-auth-secret` option. You can add as many secrets as you want - so set a different one for each thing using your TURN server.
|
||||
|
||||
Then configure livekit, making sure to replace `COTURN_SECRET`:
|
||||
|
||||
```yaml
|
||||
# livekit.yaml
|
||||
rtc:
|
||||
turn_servers:
|
||||
- host: coturn.ellis.link
|
||||
port: 3478
|
||||
protocol: tcp
|
||||
secret: "COTURN_SECRET"
|
||||
- host: coturn.ellis.link
|
||||
port: 5349
|
||||
protocol: tls # Only if you've set up TLS in your coturn
|
||||
secret: "COTURN_SECRET"
|
||||
- host: coturn.ellis.link
|
||||
port: 3478
|
||||
protocol: udp
|
||||
secret: "COTURN_SECRET"
|
||||
```
|
||||
|
||||
## LiveKit's built in TURN server
|
||||
|
||||
Livekit includes a built in TURN server which can be used in place of an external option. This TURN server will only work with Livekit, so you can't use it for legacy Matrix calling - or anything else.
|
||||
LiveKit includes a built-in TURN server which can be used in place of an external option. This TURN server will only work with LiveKit, so you can't use it for legacy Matrix calling or anything else.
|
||||
|
||||
If you don't want to set up a separate TURN server, you can enable this with the following changes:
|
||||
|
||||
@@ -221,20 +199,175 @@ ### add this to livekit.yaml ###
|
||||
udp_port: 3478
|
||||
relay_range_start: 50300
|
||||
relay_range_end: 50400
|
||||
domain: matrix-rtc.example.com
|
||||
domain: livekit.example.com
|
||||
```
|
||||
|
||||
```yaml
|
||||
### Add these to docker-compose ###
|
||||
- "3478:3478/udp"
|
||||
- "50300-50400:50300-50400/udp"
|
||||
### add these to livekit's docker-compose ###
|
||||
ports:
|
||||
- "3478:3478/udp"
|
||||
- "50300-50400:50300-50400/udp"
|
||||
### if you're using `network_mode: host`, you can skip this part
|
||||
```
|
||||
|
||||
### Related Documentation
|
||||
Recreate the LiveKit container (with `docker-compose up -d livekit`) to apply these changes. Remember to allow the new `3478/udp` and `50100:50200/udp` ports through your firewall.
|
||||
|
||||
- [LiveKit GitHub](https://github.com/livekit/livekit)
|
||||
- [LiveKit Connection Tester](https://livekit.io/connection-test) - use with the token returned by `/sfu/get` or `/get_token`
|
||||
- [MatrixRTC proposal](https://half-shot.github.io/msc-crafter/#msc/4143)
|
||||
- [Synapse documentation](https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md)
|
||||
- [Community guide](https://tomfos.tr/matrix/livekit/)
|
||||
- [Community guide](https://blog.kimiblock.top/2024/12/24/hosting-element-call/)
|
||||
### Integration with an external TURN server
|
||||
|
||||
If you've already [set up coturn](./turn), you can configure Livekit to use it.
|
||||
|
||||
:::tip Avoid port clashes between the two services
|
||||
|
||||
Before continuing, make sure coturn's `min-port` and `max-port` do not overlap with LiveKit's port range:
|
||||
|
||||
```ini
|
||||
# in your coturn.conf
|
||||
min-port=50201
|
||||
max-port=65535
|
||||
```
|
||||
:::
|
||||
|
||||
Generate a long random secret for LiveKit, and add it to your coturn config under the `static-auth-secret` option. You can add as many secrets as you want, so set a different one for LiveKit to use.
|
||||
|
||||
Then configure LiveKit, making sure to replace `COTURN_SECRET` with the one you generated:
|
||||
|
||||
```yaml
|
||||
# livekit.yaml
|
||||
rtc:
|
||||
turn_servers:
|
||||
- host: coturn.example.com
|
||||
port: 3478
|
||||
protocol: udp
|
||||
secret: "COTURN_SECRET"
|
||||
- host: coturn.example.com
|
||||
port: 3478
|
||||
protocol: tcp
|
||||
secret: "COTURN_SECRET"
|
||||
- host: coturn.example.com
|
||||
port: 5349
|
||||
protocol: tls # Only if you have already set up TLS in your coturn
|
||||
secret: "COTURN_SECRET"
|
||||
```
|
||||
|
||||
Restart LiveKit and coturn to apply these changes.
|
||||
|
||||
## Testing
|
||||
|
||||
To test that LiveKit is successfully integrated with Continuwuity, you will need to replicate its [Token Exchange Flow](https://github.com/element-hq/lk-jwt-service#%EF%B8%8F-how-it-works--token-exchange-flow).
|
||||
|
||||
First, you will need an access token for your current login session. These can be found in your client's settings or obtained via [this website](https://timedout.uk/mxtoken.html).
|
||||
|
||||
Then, using that token, request another OpenID token for use with the lk-jwt-service:
|
||||
|
||||
```bash
|
||||
~$ curl -X POST -H "Authorization: Bearer <session-access-token>" \
|
||||
https://matrix.example.com/_matrix/client/v3/user/@user:example.com/openid/request_token
|
||||
{"access_token":"<openid_access_token>","token_type":"Bearer","matrix_server_name":"example.com","expires_in":3600}
|
||||
```
|
||||
|
||||
Next, create a `payload.json` file with the following content:
|
||||
|
||||
<details>
|
||||
|
||||
<summary>`payload.json`</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"room_id": "abc",
|
||||
"slot_id": "xyz",
|
||||
"openid_token": {
|
||||
"matrix_server_name": "example.com",
|
||||
"access_token": "<openid_access_token>",
|
||||
"token_type": "Bearer"
|
||||
},
|
||||
"member": {
|
||||
"id": "xyz",
|
||||
"claimed_device_id": "DEVICEID",
|
||||
"claimed_user_id": "@user:example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `matrix_server_name` and `claimed_user_id` with your information, and `<openid_access_token>` with the one you got from the previous step. Other values can be left as-is.
|
||||
|
||||
</details>
|
||||
|
||||
You can then send this payload to the lk-jwt-service:
|
||||
|
||||
```bash
|
||||
~$ curl -X POST -d @payload.json https://livekit.example.com/get_token
|
||||
{"url":"wss://livekit.example.com","jwt":"a_really_really_long_string"}
|
||||
```
|
||||
|
||||
The lk-jwt-service will, after checking against Continuwuity, answer with a `jwt` token to create a LiveKit media room. Use this token to test at the [LiveKit Connection Tester](https://livekit.io/connection-test). If everything works there, then you have set up LiveKit successfully!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
To debug any issues, you can place a call or redo the Testing instructions, and check the container logs for any specific errors. Use `docker-compose logs --follow` to follow them in real-time.
|
||||
|
||||
### Common errors in Element Call UI
|
||||
|
||||
- `MISSING_MATRIX_RTC_FOCUS`: LiveKit is missing from Continuwuity's config file
|
||||
- "Waiting for media" popup always showing: a LiveKit URL has been configured in Continuwuity, but your client cannot connect to it for some reason
|
||||
|
||||
### Docker loopback networking issues
|
||||
|
||||
Some distros do not allow Docker containers to connect to its host's public IP by default. This would cause `lk-jwt-service` to fail connecting to `livekit` or `continuwuity` on the same host. As a result, you would see connection refused/connection timeouts log entries in the JWT service, even when `LIVEKIT_URL` has been configured correctly.
|
||||
|
||||
To alleviate this, you can try one of the following workarounds:
|
||||
|
||||
- Use `network_mode: host` for the `lk-jwt-service` container (instead of the default bridge networking).
|
||||
|
||||
- Add an `extra_hosts` file mapping livekit's (and continuwuity's) domain name to a localhost address:
|
||||
|
||||
```diff
|
||||
# in docker-compose.yaml
|
||||
services:
|
||||
lk-jwt-service:
|
||||
...
|
||||
+ extra_hosts:
|
||||
+ - "livekit.example.com:127.0.0.1"
|
||||
+ - "matrix.example.com:127.0.0.1"
|
||||
```
|
||||
|
||||
- (**untested, use at your own risk**) Implement an iptables workaround as shown [here](https://forums.docker.com/t/unable-to-connect-to-host-service-from-inside-docker-container/145749/6).
|
||||
|
||||
After implementing the changes and restarting your compose, you can test whether the connection works by cURLing from a sidecar container:
|
||||
|
||||
```bash
|
||||
~$ docker run --rm --net container:lk-jwt-service docker.io/curlimages/curl https://livekit.example.com
|
||||
OK
|
||||
```
|
||||
|
||||
### Workaround for non-federating servers
|
||||
|
||||
When deploying on servers with federation disabled (`allow_federation = false`), LiveKit will fail as it can't fetch the required [OpenID endpoint](https://spec.matrix.org/v1.17/server-server-api/#get_matrixfederationv1openiduserinfo) via federation paths.
|
||||
|
||||
As a workaround, you can enable federation, but forbid all remote servers via the following config parameters:
|
||||
|
||||
```toml
|
||||
### in your continuwuity.toml file ###
|
||||
allow_federation = true
|
||||
forbidden_remote_server_names = [".*"]
|
||||
```
|
||||
|
||||
Subscribe to issue [!1440](https://forgejo.ellis.link/continuwuation/continuwuity/issues/1440) for future updates on this matter.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
Guides:
|
||||
|
||||
- [Element Call self-hosting documentation](https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md)
|
||||
- [Community guide with overview of LiveKit's mechanisms](https://tomfos.tr/matrix/livekit/)
|
||||
- [Community guide using systemd](https://blog.kimiblock.top/2024/12/24/hosting-element-call/)
|
||||
|
||||
Specifications:
|
||||
|
||||
- [MatrixRTC proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
|
||||
- [LiveKit proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4195)
|
||||
|
||||
Source code:
|
||||
|
||||
- [Element Call](https://github.com/element-hq/element-call)
|
||||
- [lk-jwt-service](https://github.com/element-hq/lk-jwt-service)
|
||||
- [LiveKit server](https://github.com/livekit/livekit)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../CONTRIBUTING.md
|
||||
@@ -1,203 +0,0 @@
|
||||
# Contributing guide
|
||||
|
||||
This page is about contributing to Continuwuity. The
|
||||
[development](./index.mdx) and [code style guide](./code_style.mdx) pages may be of interest for you as well.
|
||||
|
||||
If you would like to work on an [issue][issues] that is not assigned, preferably
|
||||
ask in the Matrix room first at [#continuwuity:continuwuity.org][continuwuity-matrix],
|
||||
and comment on it.
|
||||
|
||||
### Code Style
|
||||
|
||||
Please review and follow the [code style guide](./code_style) for formatting, linting, naming conventions, and other code standards.
|
||||
|
||||
### Pre-commit Checks
|
||||
|
||||
Continuwuity uses pre-commit hooks to enforce various coding standards and catch common issues before they're committed. These checks include:
|
||||
|
||||
- Code formatting and linting
|
||||
- Typo detection (both in code and commit messages)
|
||||
- Checking for large files
|
||||
- Ensuring proper line endings and no trailing whitespace
|
||||
- Validating YAML, JSON, and TOML files
|
||||
- Checking for merge conflicts
|
||||
|
||||
You can run these checks locally by installing [prefligit](https://github.com/j178/prefligit):
|
||||
|
||||
|
||||
```bash
|
||||
# Requires UV: https://docs.astral.sh/uv/getting-started/installation/
|
||||
# Mac/linux: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
# Windows: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
||||
|
||||
# Install prefligit using cargo-binstall
|
||||
cargo binstall prefligit
|
||||
|
||||
# Install git hooks to run checks automatically
|
||||
prefligit install
|
||||
|
||||
# Run all checks
|
||||
prefligit --all-files
|
||||
```
|
||||
|
||||
Alternatively, you can use [pre-commit](https://pre-commit.com/):
|
||||
```bash
|
||||
# Requires python
|
||||
|
||||
# Install pre-commit
|
||||
pip install pre-commit
|
||||
|
||||
# Install the hooks
|
||||
pre-commit install
|
||||
|
||||
# Run all checks manually
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
These same checks are run in CI via the prefligit-checks workflow to ensure consistency. These must pass before the PR is merged.
|
||||
|
||||
### Running tests locally
|
||||
|
||||
Tests, compilation, and linting can be run with standard Cargo commands:
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
cargo test
|
||||
|
||||
# Check compilation
|
||||
cargo check --workspace --features full
|
||||
|
||||
# Run lints
|
||||
cargo clippy --workspace --features full
|
||||
# Auto-fix: cargo clippy --workspace --features full --fix --allow-staged;
|
||||
|
||||
# Format code (must use nightly)
|
||||
cargo +nightly fmt
|
||||
```
|
||||
|
||||
### Matrix tests
|
||||
|
||||
Continuwuity uses [Complement][complement] for Matrix protocol compliance testing. Complement tests are run manually by developers, and documentation on how to run these tests locally is currently being developed.
|
||||
|
||||
If your changes are done to fix Matrix tests, please note that in your pull request. If more Complement tests start failing from your changes, please review the logs and determine if they're intended or not.
|
||||
|
||||
[Sytest][sytest] is currently unsupported.
|
||||
|
||||
### Writing documentation
|
||||
|
||||
Continuwuity's website uses [`mdbook`][mdbook] and is deployed via CI using Cloudflare Pages
|
||||
in the [`documentation.yml`][documentation.yml] workflow file. All documentation is in the `docs/`
|
||||
directory at the top level.
|
||||
|
||||
To build the documentation locally:
|
||||
|
||||
1. Install mdbook if you don't have it already:
|
||||
```bash
|
||||
cargo install mdbook # or cargo binstall, or another method
|
||||
```
|
||||
|
||||
2. Build the documentation:
|
||||
```bash
|
||||
mdbook build
|
||||
```
|
||||
|
||||
The output of the mdbook generation is in `public/`. You can open the HTML files directly in your browser without needing a web server.
|
||||
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Continuwuity follows the [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages. This provides a standardized format that makes the commit history more readable and enables automated tools to generate changelogs.
|
||||
|
||||
The basic structure is:
|
||||
|
||||
```
|
||||
<type>[(optional scope)]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
The allowed types for commits are:
|
||||
- `fix`: Bug fixes
|
||||
- `feat`: New features
|
||||
- `docs`: Documentation changes
|
||||
- `style`: Changes that don't affect the meaning of the code (formatting, etc.)
|
||||
- `refactor`: Code changes that neither fix bugs nor add features
|
||||
- `perf`: Performance improvements
|
||||
- `test`: Adding or fixing tests
|
||||
- `build`: Changes to the build system or dependencies
|
||||
- `ci`: Changes to CI configuration
|
||||
- `chore`: Other changes that don't modify source or test files
|
||||
|
||||
Examples:
|
||||
```
|
||||
feat: add user authentication
|
||||
fix(database): resolve connection pooling issue
|
||||
docs: update installation instructions
|
||||
```
|
||||
|
||||
The project uses the `committed` hook to validate commit messages in pre-commit. This ensures all commits follow the conventional format.
|
||||
|
||||
### Creating pull requests
|
||||
|
||||
Please try to keep contributions to the Forgejo Instance. While the mirrors of continuwuity
|
||||
allow for pull/merge requests, there is no guarantee the maintainers will see them in a timely
|
||||
manner. Additionally, please mark WIP or unfinished or incomplete PRs as drafts.
|
||||
This prevents us from having to ping once in a while to double check the status
|
||||
of it, especially when the CI completed successfully and everything so it
|
||||
*looks* done.
|
||||
|
||||
Before submitting a pull request, please ensure:
|
||||
1. Your code passes all CI checks (formatting, linting, typo detection, etc.). Run pre-commit for this.
|
||||
2. Your code follows the [code style guide](./code_style)
|
||||
3. Your commit messages follow the conventional commits format
|
||||
4. Tests are added for new functionality
|
||||
5. Documentation is updated if needed
|
||||
6. You have written a [news fragment](#writing-news-fragments) for your changes
|
||||
|
||||
Direct all PRs/MRs to the `main` branch.
|
||||
|
||||
By sending a pull request or patch, you are agreeing that your changes are
|
||||
allowed to be licenced under the Apache-2.0 licence and all of your conduct is
|
||||
in line with the Contributor's Covenant, and continuwuity's Code of Conduct.
|
||||
|
||||
Contribution by users who violate either of these code of conducts may not have
|
||||
their contributions accepted. This includes users who have been banned from
|
||||
continuwuity Matrix rooms for Code of Conduct violations.
|
||||
|
||||
[issues]: https://forgejo.ellis.link/continuwuation/continuwuity/issues
|
||||
[continuwuity-matrix]: https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org
|
||||
[complement]: https://github.com/matrix-org/complement/
|
||||
[sytest]: https://github.com/matrix-org/sytest/
|
||||
[mdbook]: https://rust-lang.github.io/mdBook/
|
||||
[documentation.yml]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/.forgejo/workflows/documentation.yml
|
||||
|
||||
#### Writing news fragments
|
||||
|
||||
In order to make writing our changelogs easier, we make use of [Towncrier]. Towncrier builds changelogs based on
|
||||
"news fragments", which are little markdown files in the `changelog.d/` directory that describe individual changes.
|
||||
|
||||
When you make a pull request that changes functionality, fixes a bug, or adds documentation, please add a news fragment
|
||||
describing your change. The file name *MUST* be in the format of `{pull_request_number}.{type}`, where `{type}` is one
|
||||
of the following:
|
||||
|
||||
- `feature` - for new features
|
||||
- `bugfix` - for bug fixes
|
||||
- `doc` - for documentation changes
|
||||
- `misc` - for other changes that don't fit the above categories
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
$ echo "Fixed the quantum flux stabiliser. Contributed by @alice." > changelog.d/42.bugfix
|
||||
```
|
||||
|
||||
(Note: If you want to credit yourself, you should reference your forgejo handle, however links to other platforms are also acceptable.)
|
||||
|
||||
When the next release is made, Towncrier will automatically include your news fragment in the changelog.
|
||||
|
||||
You can read more about writing news fragments in the [Towncrier tutorial][tt].
|
||||
|
||||
[Towncrier]: https://towncrier.readthedocs.io/
|
||||
[tt]: https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../CONTRIBUTING.md
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"$id": "https://continwuity.org/schema/announcements.schema.json",
|
||||
"$id": "https://continuwuity.org/schema/announcements.schema.json",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"announcements": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
file = inputs.self + "/rust-toolchain.toml";
|
||||
|
||||
# See also `rust-toolchain.toml`
|
||||
sha256 = "sha256-SJwZ8g0zF2WrKDVmHrVG3pD2RGoQeo24MEXnNx5FyuI=";
|
||||
sha256 = "sha256-sqSWJDUxc+zaz1nBWMAJKTAGBuGWP25GCftIOlCEAtA=";
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
# 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
|
||||
|
||||
Generated
+117
-101
@@ -12,13 +12,13 @@
|
||||
"@rspress/core": "^2.0.0",
|
||||
"@rspress/plugin-client-redirects": "^2.0.0",
|
||||
"@rspress/plugin-sitemap": "^2.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
|
||||
"integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -28,9 +28,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -106,27 +106,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
|
||||
"integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.5.0",
|
||||
"@emnapi/runtime": "^1.5.0",
|
||||
"@emnapi/core": "^1.7.1",
|
||||
"@emnapi/runtime": "^1.7.1",
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@rsbuild/core": {
|
||||
"version": "2.0.0-beta.6",
|
||||
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.0-beta.6.tgz",
|
||||
"integrity": "sha512-DUBhUzvzj6xlGUAHTTipFskSuZmVEuTX7lGU+ToPuo8n3bsQrWn/UBOEQAd45g66k7QfXadoZ/v7eodQErpvGQ==",
|
||||
"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.3",
|
||||
"@swc/helpers": "^0.5.19"
|
||||
"@rspack/core": "2.0.0-beta.9",
|
||||
"@swc/helpers": "^0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"rsbuild": "bin/rsbuild.js"
|
||||
@@ -163,28 +167,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-GSj+d8AlLs1oElhYq32vIN/eAsxWG9jy0EiNgSxWTt5Gdamv87kcvsV4jwfWIjlltdnBIJgey2RnU+hDZlTAvw==",
|
||||
"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.3",
|
||||
"@rspack/binding-darwin-x64": "2.0.0-beta.3",
|
||||
"@rspack/binding-linux-arm64-gnu": "2.0.0-beta.3",
|
||||
"@rspack/binding-linux-arm64-musl": "2.0.0-beta.3",
|
||||
"@rspack/binding-linux-x64-gnu": "2.0.0-beta.3",
|
||||
"@rspack/binding-linux-x64-musl": "2.0.0-beta.3",
|
||||
"@rspack/binding-wasm32-wasi": "2.0.0-beta.3",
|
||||
"@rspack/binding-win32-arm64-msvc": "2.0.0-beta.3",
|
||||
"@rspack/binding-win32-ia32-msvc": "2.0.0-beta.3",
|
||||
"@rspack/binding-win32-x64-msvc": "2.0.0-beta.3"
|
||||
"@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.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-QebSomLWlCbFsC0sfDuGqLJtkgyrnr38vrCepWukaAXIY4ANy5QB49LDKdLpVv6bKlC95MpnW37NvSNWY5GMYA==",
|
||||
"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"
|
||||
],
|
||||
@@ -196,9 +200,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-darwin-x64": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-EysmBq+sz+Ph0bu0gXpU1uuZG9gXgjqY+w3MJel+ieTFyQO3L/R56V32McgssMbheJbYcviDDn7Tz4D+lTvdJA==",
|
||||
"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"
|
||||
],
|
||||
@@ -210,13 +214,16 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-arm64-gnu": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-iFPj4TQZKewnqWPfTbyk3F8QCBI/Edv7TVSRIPBHRnCM0lvYZl/8IZlUzXSamLvrtDpouF0nUzht/fktoWOhAg==",
|
||||
"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"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -224,13 +231,16 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-arm64-musl": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-355mygfCNb0eF/y4HgtJcd0i9csNTG4Z15PCCplIkSAKJpFpkORM2xJb50BqsbhVafYl6AHoBlGWAo9iIzUb/w==",
|
||||
"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"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -238,13 +248,16 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-x64-gnu": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-U8a+bcP/tkMyiwiO9XfeRYYO20YPGiZNxWWt7FEsdmRuRAl6M+EmWaJllJFQtKH+GG8IN93pNoVPMvARjLoJOQ==",
|
||||
"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"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -252,13 +265,16 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-linux-x64-musl": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-g81rqkaqDFRTID2VrHBYeM+xZe8yWov7IcryTrl9RGXXr61s+6Tu/mWyM378PuHOCyMNu7G3blVaSjLvKauG6Q==",
|
||||
"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"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -266,9 +282,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-wasm32-wasi": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-tzGd8H2oj5F3oR/Hxp+J68zVU/nG+9ndH2KK3/RieVjNAiVNHCR0/ZU9D47s6fnmvWOqAQ1qO8gnVoVLopC4YA==",
|
||||
"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"
|
||||
],
|
||||
@@ -276,13 +292,13 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@napi-rs/wasm-runtime": "1.0.7"
|
||||
"@napi-rs/wasm-runtime": "1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-arm64-msvc": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-TZZRSWa34sm5WyoQHwnyBjLJ4w3fcWRYA9ybYjSVWjUU6tVGdMiHiZp+WexUpIETvChLXU1JENNmBg/U7wvZEA==",
|
||||
"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"
|
||||
],
|
||||
@@ -294,9 +310,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-ia32-msvc": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-VFnfdbJhyl6gNW1VzTyd1ZrHCboHPR7vrOalEsulQRqVNbtDkjm1sqLHtDcLmhTEv0a9r4lli8uubWDwmel8KQ==",
|
||||
"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"
|
||||
],
|
||||
@@ -308,9 +324,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/binding-win32-x64-msvc": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-rwZ6Y3b3oqPj+ZDPPRxr3136HUPKDSlPQa4v7bBOPLDlrFDFOynMIEqDUUi5+8lPaUQ8WWR0aJK4cgcTTT0Siw==",
|
||||
"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"
|
||||
],
|
||||
@@ -322,13 +338,13 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rspack/core": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-VuLteRIesuyFFTXZaciUY0lwDZiwMc7JcpE8guvjArztDhtpVvlaOcLlVBp/Yza8c/Tk8Dxwe1ARzFL7xG1/0w==",
|
||||
"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.3"
|
||||
"@rspack/binding": "2.0.0-beta.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
@@ -367,20 +383,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/core": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.5.tgz",
|
||||
"integrity": "sha512-2ezGmANmIrWmhsUrvlRb9Df4xsun1BDgEertDc890aQqtKcNrbu+TBRsOoO+E/N6ioavun7JGGe1wWjvxubCHw==",
|
||||
"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.6",
|
||||
"@rsbuild/plugin-react": "~1.4.5",
|
||||
"@rspress/shared": "2.0.5",
|
||||
"@shikijs/rehype": "^4.0.1",
|
||||
"@rsbuild/core": "2.0.0-beta.11",
|
||||
"@rsbuild/plugin-react": "~1.4.6",
|
||||
"@rspress/shared": "2.0.8",
|
||||
"@shikijs/rehype": "^4.0.2",
|
||||
"@types/unist": "^3.0.3",
|
||||
"@unhead/react": "^2.1.9",
|
||||
"@unhead/react": "^2.1.12",
|
||||
"body-scroll-lock": "4.0.0-beta.0",
|
||||
"cac": "^7.0.0",
|
||||
"chokidar": "^3.6.0",
|
||||
@@ -401,7 +417,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",
|
||||
@@ -411,7 +427,7 @@
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"scroll-into-view-if-needed": "^3.1.0",
|
||||
"shiki": "^4.0.1",
|
||||
"shiki": "^4.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinypool": "^1.1.1",
|
||||
"unified": "^11.0.5",
|
||||
@@ -427,40 +443,40 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/plugin-client-redirects": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.5.tgz",
|
||||
"integrity": "sha512-sxwWzwHPefSPWUyV6/AA/hBlQUeNFntL8dBQi/vZCQiZHM6ShvKbqa3s5Xu2yI7DeFKHH3jb0VGbjufu8M3Ypw==",
|
||||
"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.5"
|
||||
"@rspress/core": "^2.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/plugin-sitemap": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.5.tgz",
|
||||
"integrity": "sha512-wBxKL8sNd3bkKFxlFtB1xJ7jCtSRDL6pfVvWsmTIbTNDPCtefd1nmiMBIDMLOR8EflwuStIz3bMQXdWpbC7ahA==",
|
||||
"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.5"
|
||||
"@rspress/core": "^2.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/shared": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.5.tgz",
|
||||
"integrity": "sha512-Wdhh+VjU8zJWoVLhv9KJTRAZQ4X2V/Z81Lo2D0hQsa0Kj5F3EaxlMt5/dhX7DoflqNuZPZk/e7CSUB+gO/Umlg==",
|
||||
"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.6",
|
||||
"@shikijs/rehype": "^4.0.1",
|
||||
"@rsbuild/core": "2.0.0-beta.11",
|
||||
"@shikijs/rehype": "^4.0.2",
|
||||
"gray-matter": "4.0.3",
|
||||
"lodash-es": "^4.17.23",
|
||||
"unified": "^11.0.5"
|
||||
@@ -593,9 +609,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.20",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.20.tgz",
|
||||
"integrity": "sha512-2egEBHUMasdypIzrprsu8g+OEVd7Vp2MM3a2eVlM/cyFYto0nGz5BX5BTgh/ShZZI9ed+ozEq+Ngt+rgmUs8tw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -3135,9 +3151,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.13.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz",
|
||||
"integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3158,13 +3174,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.13.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz",
|
||||
"integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.13.1"
|
||||
"react-router": "7.13.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -3711,9 +3727,9 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
|
||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
+1
-1
@@ -25,6 +25,6 @@
|
||||
"@rspress/core": "^2.0.0",
|
||||
"@rspress/plugin-client-redirects": "^2.0.0",
|
||||
"@rspress/plugin-sitemap": "^2.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended", "replacements:all", ":semanticCommitTypeAll(chore)"],
|
||||
"extends": ["config:recommended", "replacements:all", ":semanticCommitTypeAll(chore)", "helpers:pinGitHubActionDigests"],
|
||||
"dependencyDashboard": true,
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"lockFileMaintenance": {
|
||||
@@ -95,16 +95,16 @@
|
||||
}
|
||||
],
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Update _VERSION variables in Dockerfiles",
|
||||
"managerFilePatterns": [
|
||||
"/(^|/)([Dd]ocker|[Cc]ontainer)file[^/]*$/",
|
||||
"/(^|/|\\.)([Dd]ocker|[Cc]ontainer)file$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"# renovate: datasource=(?<datasource>[a-zA-Z0-9-._]+?) depName=(?<depName>[^\\s]+?)(?: (lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?(?: registryUrl=(?<registryUrl>[^\\s]+?))?\\s+(?:ENV\\s+|ARG\\s+)?[A-Za-z0-9_]+?_VERSION[ =][\"']?(?<currentValue>.+?)[\"']?\\s+(?:(?:ENV\\s+|ARG\\s+)?[A-Za-z0-9_]+?_CHECKSUM[ =][\"']?(?<currentDigest>.+?)[\"']?\\s)?"
|
||||
]
|
||||
}
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Update _VERSION variables in Dockerfiles",
|
||||
"managerFilePatterns": [
|
||||
"/(^|/)([Dd]ocker|[Cc]ontainer)file[^/]*$/",
|
||||
"/(^|/|\\.)([Dd]ocker|[Cc]ontainer)file$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"# renovate: datasource=(?<datasource>[a-zA-Z0-9-._]+?) depName=(?<depName>[^\\s]+?)(?: (lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?(?: registryUrl=(?<registryUrl>[^\\s]+?))?\\s+(?:ENV\\s+|ARG\\s+)?[A-Za-z0-9_]+?_VERSION[ =][\"']?(?<currentValue>.+?)[\"']?\\s+(?:(?:ENV\\s+|ARG\\s+)?[A-Za-z0-9_]+?_CHECKSUM[ =][\"']?(?<currentDigest>.+?)[\"']?\\s)?"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
[toolchain]
|
||||
profile = "minimal"
|
||||
channel = "1.90.0"
|
||||
channel = "1.92.0"
|
||||
components = [
|
||||
# For rust-analyzer
|
||||
"rust-src",
|
||||
|
||||
@@ -80,7 +80,9 @@ conduwuit-macros.workspace = true
|
||||
conduwuit-service.workspace = true
|
||||
const-str.workspace = true
|
||||
futures.workspace = true
|
||||
lettre.workspace = true
|
||||
log.workspace = true
|
||||
assign.workspace = true
|
||||
ruma.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde-saphyr.workspace = true
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+134
-23
@@ -3,14 +3,18 @@
|
||||
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},
|
||||
matrix::{Event, pdu::PartialPdu},
|
||||
utils::{self, ReadyExt},
|
||||
warn,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use lettre::Address;
|
||||
use ruma::{
|
||||
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, UserId,
|
||||
events::{
|
||||
@@ -728,40 +732,33 @@ pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAli
|
||||
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&room_id).await;
|
||||
|
||||
let room_power_levels: Option<RoomPowerLevelsEventContent> = self
|
||||
let mut room_power_levels = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content(&room_id, &StateEventType::RoomPowerLevels, "")
|
||||
.await
|
||||
.ok();
|
||||
.get_room_power_levels(&room_id)
|
||||
.await;
|
||||
|
||||
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)
|
||||
}) || self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.is_ok_and(|event| event.sender() == user_id);
|
||||
let user_can_demote_self =
|
||||
room_power_levels.user_can_change_user_power_level(&user_id, &user_id);
|
||||
|
||||
if !user_can_demote_self {
|
||||
return Err!("User is not allowed to modify their own power levels in the room.",);
|
||||
}
|
||||
|
||||
let mut power_levels_content = room_power_levels.unwrap_or_default();
|
||||
power_levels_content.users.remove(&user_id);
|
||||
room_power_levels.users.remove(&user_id);
|
||||
|
||||
let event_id = self
|
||||
.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(String::new(), &power_levels_content),
|
||||
PartialPdu::state(
|
||||
String::new(),
|
||||
room_power_levels
|
||||
.try_into()
|
||||
.expect("PLs should be valid for room version"),
|
||||
),
|
||||
&user_id,
|
||||
Some(&room_id),
|
||||
&state_lock,
|
||||
@@ -924,9 +921,9 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
redacts: Some(event.event_id().to_owned()),
|
||||
..PduBuilder::timeline(&RoomRedactionEventContent {
|
||||
..PartialPdu::timeline(&RoomRedactionEventContent {
|
||||
redacts: Some(event.event_id().to_owned()),
|
||||
reason: Some(reason),
|
||||
})
|
||||
@@ -1094,3 +1091,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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -85,10 +85,13 @@ 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
|
||||
assign.workspace = true
|
||||
ruma.workspace = true
|
||||
ruminuwuity.workspace = true
|
||||
serde_html_form.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, info, utils::ReadyExt, warn};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::{
|
||||
OwnedRoomAliasId, continuwuity_admin_api::rooms,
|
||||
events::room::message::RoomMessageEventContent,
|
||||
};
|
||||
use ruma::{OwnedRoomAliasId, events::room::message::RoomMessageEventContent};
|
||||
use ruminuwuity::admin::continuwuity::rooms;
|
||||
|
||||
use crate::{Ruma, client::leave_room};
|
||||
|
||||
@@ -36,7 +34,6 @@ pub(crate) async fn ban_room(
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&body.room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.ready_filter(|user| services.globals.user_is_local(user))
|
||||
.boxed();
|
||||
let mut evicted = Vec::new();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::StreamExt;
|
||||
use ruma::{OwnedRoomId, continuwuity_admin_api::rooms};
|
||||
use ruma::OwnedRoomId;
|
||||
use ruminuwuity::admin::continuwuity::rooms;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -22,7 +23,7 @@ pub(crate) async fn list_rooms(
|
||||
.metadata
|
||||
.iter_ids()
|
||||
.filter_map(|room_id| async move {
|
||||
if !services.rooms.metadata.is_banned(room_id).await {
|
||||
if !services.rooms.metadata.is_banned(&room_id).await {
|
||||
Some(room_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -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(¬ice).await;
|
||||
}
|
||||
} else {
|
||||
let notice = format!("New user \"{user_id}\" registered on this server.");
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log in conduit admin channel if a guest registered
|
||||
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
||||
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
||||
|
||||
if !device_display_name.is_empty() {
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with device display name \
|
||||
\"{device_display_name}\" registered on this server from IP {client}"
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with no device display name registered on \
|
||||
this server from IP {client}",
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !is_guest {
|
||||
// Make the first user to register an administrator and disable first-run mode.
|
||||
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
|
||||
|
||||
// If the registering user was not the first and we're suspending users on
|
||||
// register, suspend them.
|
||||
if !was_first_user && services.config.suspend_on_register {
|
||||
// Note that we can still do auto joins for suspended users
|
||||
services
|
||||
.users
|
||||
.suspend_account(&user_id, &services.globals.server_user)
|
||||
.await;
|
||||
// And send an @room notice to the admin room, to prompt admins to review the
|
||||
// new user and ideally unsuspend them if deemed appropriate.
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} has been suspended as they are not the first user on \
|
||||
this server. Please review and unsuspend them if appropriate."
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if body.appservice_info.is_none()
|
||||
&& !services.server.config.auto_join_rooms.is_empty()
|
||||
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
||||
{
|
||||
for room in &services.server.config.auto_join_rooms {
|
||||
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
||||
error!(
|
||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||
{room}, skipping"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Skipping room {room} to automatically join as we have never joined before."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_server_name) = room.server_name() {
|
||||
match join_room_by_id_helper(
|
||||
&services,
|
||||
&user_id,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||
&body.appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Err(e) => {
|
||||
// don't return this error so we don't fail registrations
|
||||
error!(
|
||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
||||
);
|
||||
},
|
||||
| _ => {
|
||||
info!("Automatically joined room {room} for user {user_id}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(register::v3::Response {
|
||||
access_token: token,
|
||||
user_id,
|
||||
device_id: device,
|
||||
refresh_token: None,
|
||||
expires_in: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/password`
|
||||
///
|
||||
/// Changes the password of this account.
|
||||
///
|
||||
/// - Requires UIAA to verify user password
|
||||
/// - Changes the password of the sender user
|
||||
/// - The password hash is calculated using argon2 with 32 character salt, the
|
||||
/// plain password is
|
||||
/// not saved
|
||||
///
|
||||
/// If logout_devices is true it does the following for each device except the
|
||||
/// sender device:
|
||||
/// - Invalidates access token
|
||||
/// - Deletes device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")]
|
||||
pub(crate) async fn change_password_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<change_password::v3::Request>,
|
||||
) -> Result<change_password::v3::Response> {
|
||||
// Authentication for this endpoint was made optional, but we need
|
||||
// authentication currently
|
||||
let sender_user = body
|
||||
.sender_user
|
||||
.as_ref()
|
||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, body.sender_device(), &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("JSON body is not valid")));
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_password(sender_user, Some(&body.new_password))
|
||||
.await?;
|
||||
|
||||
if body.logout_devices {
|
||||
// Logout all devices except the current one
|
||||
services
|
||||
.users
|
||||
.all_device_ids(sender_user)
|
||||
.ready_filter(|id| *id != body.sender_device())
|
||||
.for_each(|id| services.users.remove_device(sender_user, id))
|
||||
.await;
|
||||
|
||||
// Remove all pushers except the ones associated with this session
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(sender_user)
|
||||
.map(ToOwned::to_owned)
|
||||
.broad_filter_map(async |pushkey| {
|
||||
services
|
||||
.pusher
|
||||
.get_pusher_device(&pushkey)
|
||||
.await
|
||||
.ok()
|
||||
.filter(|pusher_device| pusher_device != body.sender_device())
|
||||
.is_some()
|
||||
.then_some(pushkey)
|
||||
})
|
||||
.for_each(async |pushkey| {
|
||||
services.pusher.delete_pusher(sender_user, &pushkey).await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
info!("User {sender_user} changed their password.");
|
||||
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("User {sender_user} changed their password."))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(change_password::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v3/account/whoami`
|
||||
///
|
||||
/// Get `user_id` of the sender user.
|
||||
///
|
||||
/// Note: Also works for Application Services
|
||||
pub(crate) async fn whoami_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<whoami::v3::Request>,
|
||||
) -> Result<whoami::v3::Response> {
|
||||
let is_guest = services
|
||||
.users
|
||||
.is_deactivated(body.sender_user())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
err!(Request(Forbidden("Application service has not registered this user.")))
|
||||
})? && body.appservice_info.is_none();
|
||||
Ok(whoami::v3::Response {
|
||||
user_id: body.sender_user().to_owned(),
|
||||
device_id: body.sender_device.clone(),
|
||||
is_guest,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/deactivate`
|
||||
///
|
||||
/// Deactivate sender user account.
|
||||
///
|
||||
/// - Leaves all rooms and rejects all invitations
|
||||
/// - Invalidates all access tokens
|
||||
/// - Deletes all device metadata (device id, device display name, last seen ip,
|
||||
/// last seen ts)
|
||||
/// - Forgets all to-device events
|
||||
/// - Triggers device list updates
|
||||
/// - Removes ability to log in again
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "deactivate", level = "info")]
|
||||
pub(crate) async fn deactivate_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<deactivate::v3::Request>,
|
||||
) -> Result<deactivate::v3::Response> {
|
||||
// Authentication for this endpoint was made optional, but we need
|
||||
// authentication currently
|
||||
let sender_user = body
|
||||
.sender_user
|
||||
.as_ref()
|
||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
||||
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
||||
completed: Vec::new(),
|
||||
params: Box::default(),
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
match &body.auth {
|
||||
| Some(auth) => {
|
||||
let (worked, uiaainfo) = services
|
||||
.uiaa
|
||||
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
|
||||
.await?;
|
||||
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
},
|
||||
| _ => match body.json_body {
|
||||
| Some(ref json) => {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
services
|
||||
.uiaa
|
||||
.create(sender_user, body.sender_device(), &uiaainfo, json);
|
||||
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
},
|
||||
| _ => {
|
||||
return Err!(Request(NotJson("JSON body is not valid")));
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Remove profile pictures and display name
|
||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
full_user_deactivate(&services, sender_user, &all_joined_rooms)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
info!("User {sender_user} deactivated their account.");
|
||||
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("User {sender_user} deactivated their account."))
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(deactivate::v3::Response {
|
||||
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET _matrix/client/v3/account/3pid`
|
||||
///
|
||||
/// Get a list of third party identifiers associated with this account.
|
||||
///
|
||||
/// - Currently always returns empty list
|
||||
pub(crate) async fn third_party_route(
|
||||
body: Ruma<get_3pids::v3::Request>,
|
||||
) -> Result<get_3pids::v3::Response> {
|
||||
let _sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
Ok(get_3pids::v3::Response::new(Vec::new()))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an email
|
||||
/// address to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||
/// as a contact option.
|
||||
pub(crate) async fn request_3pid_management_token_via_email_route(
|
||||
_body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
||||
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an phone
|
||||
/// number to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||
/// as a contact option.
|
||||
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
||||
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
|
||||
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
||||
///
|
||||
/// Checks if the provided registration token is valid at the time of checking.
|
||||
pub(crate) async fn check_registration_token_validity(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<check_registration_token_validity::v1::Request>,
|
||||
) -> Result<check_registration_token_validity::v1::Response> {
|
||||
// TODO: ratelimit this pretty heavily
|
||||
|
||||
let valid = services
|
||||
.registration_tokens
|
||||
.validate_token(body.token.clone())
|
||||
.await
|
||||
.is_some();
|
||||
|
||||
Ok(check_registration_token_validity::v1::Response { valid })
|
||||
}
|
||||
|
||||
/// Runs through all the deactivation steps:
|
||||
///
|
||||
/// - Mark as deactivated
|
||||
/// - Removing display name
|
||||
/// - Removing avatar URL and blurhash
|
||||
/// - Removing all profile data
|
||||
/// - Leaving all rooms (and forgets all of them)
|
||||
pub async fn full_user_deactivate(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
all_joined_rooms: &[OwnedRoomId],
|
||||
) -> Result<()> {
|
||||
services.users.deactivate_account(user_id).await.ok();
|
||||
|
||||
services
|
||||
.users
|
||||
.all_profile_keys(user_id)
|
||||
.ready_for_each(|(profile_key, _)| {
|
||||
services.users.set_profile_key(user_id, &profile_key, None);
|
||||
})
|
||||
.await;
|
||||
|
||||
// TODO: Rescind all user invites
|
||||
|
||||
let mut pdu_queue: Vec<(PduBuilder, &OwnedRoomId)> = Vec::new();
|
||||
|
||||
for room_id in all_joined_rooms {
|
||||
let room_power_levels = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<RoomPowerLevelsEventContent>(
|
||||
room_id,
|
||||
&StateEventType::RoomPowerLevels,
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let user_can_demote_self =
|
||||
room_power_levels
|
||||
.as_ref()
|
||||
.is_some_and(|power_levels_content| {
|
||||
RoomPowerLevels::from(power_levels_content.clone())
|
||||
.user_can_change_user_power_level(user_id, user_id)
|
||||
}) || services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.is_ok_and(|event| event.sender() == user_id);
|
||||
|
||||
if user_can_demote_self {
|
||||
let mut power_levels_content = room_power_levels.unwrap_or_default();
|
||||
power_levels_content.users.remove(user_id);
|
||||
let pl_evt = PduBuilder::state(String::new(), &power_levels_content);
|
||||
pdu_queue.push((pl_evt, room_id));
|
||||
}
|
||||
|
||||
// Leave the room
|
||||
pdu_queue.push((
|
||||
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
avatar_url: None,
|
||||
blurhash: None,
|
||||
membership: MembershipState::Leave,
|
||||
displayname: None,
|
||||
join_authorized_via_users_server: None,
|
||||
reason: None,
|
||||
is_direct: None,
|
||||
third_party_invite: None,
|
||||
redact_events: None,
|
||||
}),
|
||||
room_id,
|
||||
));
|
||||
|
||||
// TODO: Redact all messages sent by the user in the room
|
||||
}
|
||||
|
||||
super::update_all_rooms(services, pdu_queue, user_id).await;
|
||||
for room_id in all_joined_rooms {
|
||||
services.rooms.state_cache.forget(room_id, user_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, err, info,
|
||||
pdu::PartialPdu,
|
||||
utils::{ReadyExt, stream::BroadbandExt},
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use lettre::{Address, message::Mailbox};
|
||||
use ruma::{
|
||||
OwnedRoomId, 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},
|
||||
},
|
||||
assign,
|
||||
events::room::{
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
power_levels::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::new(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 = UserId::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(async |id| services.users.remove_device(&sender_user, &id).await)
|
||||
.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::new())
|
||||
}
|
||||
|
||||
/// # `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 =
|
||||
UserId::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(assign!(whoami::v3::Response::new(body.sender_user().to_owned(), is_guest), {
|
||||
device_id: body.sender_device.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// # `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::new(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::new(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<(PartialPdu, &OwnedRoomId)> = Vec::new();
|
||||
|
||||
for room_id in all_joined_rooms {
|
||||
let room_power_levels = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_power_levels(&room_id)
|
||||
.await;
|
||||
|
||||
let user_can_demote_self =
|
||||
room_power_levels.user_can_change_user_power_level(user_id, user_id);
|
||||
|
||||
if user_can_demote_self
|
||||
&& let Ok(mut power_levels_content) =
|
||||
RoomPowerLevelsEventContent::try_from(room_power_levels)
|
||||
{
|
||||
power_levels_content.users.remove(user_id);
|
||||
let pl_evt = PartialPdu::state(String::new(), &power_levels_content);
|
||||
pdu_queue.push((pl_evt, room_id));
|
||||
}
|
||||
|
||||
// Leave the room
|
||||
pdu_queue.push((
|
||||
PartialPdu::state(
|
||||
user_id.to_string(),
|
||||
&RoomMemberEventContent::new(MembershipState::Leave),
|
||||
),
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
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},
|
||||
},
|
||||
assign,
|
||||
events::{
|
||||
GlobalAccountDataEventType, push_rules::PushRulesEvent,
|
||||
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(PushRulesEvent::new(
|
||||
push::Ruleset::server_default(&user_id).into(),
|
||||
))
|
||||
.expect("should be able to serialize push rules"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Generate new device id if the user didn't specify one
|
||||
let (token, device) = if !body.inhibit_login {
|
||||
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?;
|
||||
(Some(new_token), Some(device_id))
|
||||
} else {
|
||||
// Don't create a device for inhibited logins
|
||||
(None, None)
|
||||
};
|
||||
|
||||
debug_info!(%user_id, ?device, "User account was created");
|
||||
|
||||
// If the user registered with an email, associate it with their account.
|
||||
if let Some(identity) = identity
|
||||
&& let Some(email) = identity.email
|
||||
{
|
||||
// This may fail if the email is already in use, but we already check for that
|
||||
// in `/requestToken`, so ignoring the error is acceptable here in the rare case
|
||||
// that an email is sniped by another user between the `/requestToken` request
|
||||
// and the `/register` request.
|
||||
let _ = services
|
||||
.threepid
|
||||
.associate_localpart_email(user_id.localpart(), &email)
|
||||
.await;
|
||||
}
|
||||
|
||||
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
|
||||
|
||||
// log in conduit admin channel if a non-guest user registered
|
||||
if body.appservice_info.is_none() && !is_guest {
|
||||
if !device_display_name.is_empty() {
|
||||
let notice = format!(
|
||||
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
||||
display name \"{device_display_name}\""
|
||||
);
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
} else {
|
||||
let notice = format!("New user \"{user_id}\" registered on this server.");
|
||||
|
||||
info!("{notice}");
|
||||
if services.server.config.admin_room_notices {
|
||||
services.admin.notice(¬ice).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log in conduit admin channel if a guest registered
|
||||
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
||||
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
||||
|
||||
if !device_display_name.is_empty() {
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with device display name \
|
||||
\"{device_display_name}\" registered on this server from IP {client}"
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with no device display name registered on \
|
||||
this server from IP {client}",
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !is_guest {
|
||||
// Make the first user to register an administrator and disable first-run mode.
|
||||
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
|
||||
|
||||
// If the registering user was not the first and we're suspending users on
|
||||
// register, suspend them.
|
||||
if !was_first_user && services.config.suspend_on_register {
|
||||
// Note that we can still do auto joins for suspended users
|
||||
services
|
||||
.users
|
||||
.suspend_account(&user_id, &services.globals.server_user)
|
||||
.await;
|
||||
// And send an @room notice to the admin room, to prompt admins to review the
|
||||
// new user and ideally unsuspend them if deemed appropriate.
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
||||
"User {user_id} has been suspended as they are not the first user on \
|
||||
this server. Please review and unsuspend them if appropriate."
|
||||
)))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if body.appservice_info.is_none()
|
||||
&& !services.server.config.auto_join_rooms.is_empty()
|
||||
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
||||
{
|
||||
for room in &services.server.config.auto_join_rooms {
|
||||
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
||||
error!(
|
||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||
{room}, skipping"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &room_id)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
"Skipping room {room} to automatically join as we have never joined before."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room_server_name) = room.server_name() {
|
||||
match join_room_by_id_helper(
|
||||
&services,
|
||||
&user_id,
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||
&body.appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Err(e) => {
|
||||
// don't return this error so we don't fail registrations
|
||||
error!(
|
||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
||||
);
|
||||
},
|
||||
| _ => {
|
||||
info!("Automatically joined room {room} for user {user_id}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(assign!(register::v3::Response::new(user_id), {
|
||||
access_token: token,
|
||||
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);
|
||||
}
|
||||
|
||||
if flows.is_empty() {
|
||||
// No flows are configured. Bail out by default
|
||||
// unless open registration was explicitly enabled.
|
||||
if !services
|
||||
.config
|
||||
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
// We have open registration enabled (😧), provide a dummy flow
|
||||
flows.push(AuthFlow::new(vec![AuthType::Dummy]));
|
||||
}
|
||||
|
||||
flows
|
||||
};
|
||||
|
||||
let params = serde_json::value::to_raw_value(¶ms).expect("params should be valid JSON");
|
||||
|
||||
Ok((flows, params))
|
||||
}
|
||||
|
||||
async fn determine_registration_user_id(
|
||||
services: &Services,
|
||||
supplied_username: Option<String>,
|
||||
is_guest: bool,
|
||||
emergency_mode_enabled: bool,
|
||||
) -> Result<OwnedUserId> {
|
||||
if let Some(supplied_username) = supplied_username
|
||||
&& !is_guest
|
||||
{
|
||||
// The user gets to pick their username. Do some validation to make sure it's
|
||||
// acceptable.
|
||||
|
||||
// Don't allow registration with forbidden usernames.
|
||||
if services
|
||||
.globals
|
||||
.forbidden_usernames()
|
||||
.is_match(&supplied_username)
|
||||
&& !emergency_mode_enabled
|
||||
{
|
||||
return Err!(Request(Forbidden("Username is forbidden")));
|
||||
}
|
||||
|
||||
// Create and validate the user ID
|
||||
let user_id = match UserId::parse_with_server_name(
|
||||
&supplied_username,
|
||||
services.globals.server_name(),
|
||||
) {
|
||||
| Ok(user_id) => {
|
||||
if let Err(e) = user_id.validate_strict() {
|
||||
// Unless we are in emergency mode, we should follow synapse's behaviour on
|
||||
// not allowing things like spaces and UTF-8 characters in usernames
|
||||
if !emergency_mode_enabled {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {supplied_username} contains disallowed characters or \
|
||||
spaces: {e}"
|
||||
))));
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow registration with user IDs that aren't local
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
return Err!(Request(InvalidUsername(
|
||||
"Username {supplied_username} is not local to this server"
|
||||
)));
|
||||
}
|
||||
|
||||
user_id
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidUsername(debug_warn!(
|
||||
"Username {supplied_username} is not valid: {e}"
|
||||
))));
|
||||
},
|
||||
};
|
||||
|
||||
if services.users.exists(&user_id).await {
|
||||
return Err!(Request(UserInUse("User ID is not available.")));
|
||||
}
|
||||
|
||||
Ok(user_id)
|
||||
} else {
|
||||
// The user is a guest or didn't specify a username. Generate a username for
|
||||
// them.
|
||||
|
||||
loop {
|
||||
let user_id = UserId::parse_with_server_name(
|
||||
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
||||
services.globals.server_name(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if !services.users.exists(&user_id).await {
|
||||
break Ok(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/register/email/requestToken`
|
||||
///
|
||||
/// Requests a validation email for the purpose of registering a new account.
|
||||
pub(crate) async fn request_registration_token_via_email_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<request_registration_token_via_email::v3::Request>,
|
||||
) -> Result<request_registration_token_via_email::v3::Response> {
|
||||
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||
};
|
||||
|
||||
if services
|
||||
.threepid
|
||||
.get_localpart_for_email(&email)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
return Err!(Request(ThreepidInUse("This email address is already in use.")));
|
||||
}
|
||||
|
||||
let session = services
|
||||
.threepid
|
||||
.send_validation_email(
|
||||
Mailbox::new(None, email),
|
||||
|verification_link| messages::NewAccount {
|
||||
server_name: services.config.server_name.as_ref(),
|
||||
verification_link,
|
||||
},
|
||||
&body.client_secret,
|
||||
body.send_attempt.try_into().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(request_registration_token_via_email::v3::Response::new(session))
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -40,7 +40,7 @@ pub(crate) async fn set_global_account_data_route(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(set_global_account_data::v3::Response {})
|
||||
Ok(set_global_account_data::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
|
||||
@@ -65,7 +65,7 @@ pub(crate) async fn set_room_account_data_route(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(set_room_account_data::v3::Response {})
|
||||
Ok(set_room_account_data::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/account_data/{type}`
|
||||
@@ -119,7 +119,7 @@ async fn set_account_data(
|
||||
event_type_s: &str,
|
||||
data: &RawJsonValue,
|
||||
) -> Result {
|
||||
if event_type_s == RoomAccountDataEventType::FullyRead.to_cow_str() {
|
||||
if event_type_s == "m.fully_read" {
|
||||
return Err!(Request(BadJson(
|
||||
"This endpoint cannot be used for marking a room as fully read (setting \
|
||||
m.fully_read)"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::future::{join, join3};
|
||||
use ruma::api::client::admin::{get_suspended, set_suspended};
|
||||
use ruminuwuity::admin::{get_suspended, set_suspended};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
|
||||
@@ -47,5 +47,5 @@ pub(crate) async fn appservice_ping(
|
||||
.await?
|
||||
.expect("We already validated if an appservice URL exists above");
|
||||
|
||||
Ok(request_ping::v1::Response { duration: timer.elapsed() })
|
||||
Ok(request_ping::v1::Response::new(timer.elapsed()))
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ pub(crate) async fn create_backup_version_route(
|
||||
.key_backups
|
||||
.create_backup(body.sender_user(), &body.algorithm)?;
|
||||
|
||||
Ok(create_backup_version::v3::Response { version })
|
||||
Ok(create_backup_version::v3::Response::new(version))
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/version/{version}`
|
||||
@@ -44,7 +44,7 @@ pub(crate) async fn update_backup_version_route(
|
||||
.update_backup(body.sender_user(), &body.version, &body.algorithm)
|
||||
.await?;
|
||||
|
||||
Ok(update_backup_version::v3::Response {})
|
||||
Ok(update_backup_version::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/version`
|
||||
@@ -105,7 +105,7 @@ pub(crate) async fn delete_backup_version_route(
|
||||
.delete_backup(body.sender_user(), &body.version)
|
||||
.await;
|
||||
|
||||
Ok(delete_backup_version::v3::Response {})
|
||||
Ok(delete_backup_version::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/keys`
|
||||
@@ -292,7 +292,7 @@ pub(crate) async fn get_backup_keys_route(
|
||||
.get_all(body.sender_user(), &body.version)
|
||||
.await;
|
||||
|
||||
Ok(get_backup_keys::v3::Response { rooms })
|
||||
Ok(get_backup_keys::v3::Response::new(rooms))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
@@ -307,7 +307,7 @@ pub(crate) async fn get_backup_keys_for_room_route(
|
||||
.get_room(body.sender_user(), &body.version, &body.room_id)
|
||||
.await;
|
||||
|
||||
Ok(get_backup_keys_for_room::v3::Response { sessions })
|
||||
Ok(get_backup_keys_for_room::v3::Response::new(sessions))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
@@ -325,7 +325,7 @@ pub(crate) async fn get_backup_keys_for_session_route(
|
||||
err!(Request(NotFound(debug_error!("Backup key not found for this user's session."))))
|
||||
})?;
|
||||
|
||||
Ok(get_backup_keys_for_session::v3::Response { key_data })
|
||||
Ok(get_backup_keys_for_session::v3::Response::new(key_data))
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys`
|
||||
@@ -342,7 +342,7 @@ pub(crate) async fn delete_backup_keys_route(
|
||||
|
||||
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
|
||||
|
||||
Ok(delete_backup_keys::v3::Response { count, etag })
|
||||
Ok(delete_backup_keys::v3::Response::new(etag, count))
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
@@ -359,7 +359,7 @@ pub(crate) async fn delete_backup_keys_for_room_route(
|
||||
|
||||
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
|
||||
|
||||
Ok(delete_backup_keys_for_room::v3::Response { count, etag })
|
||||
Ok(delete_backup_keys_for_room::v3::Response::new(etag, count))
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
@@ -376,7 +376,7 @@ pub(crate) async fn delete_backup_keys_for_session_route(
|
||||
|
||||
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
|
||||
|
||||
Ok(delete_backup_keys_for_session::v3::Response { count, etag })
|
||||
Ok(delete_backup_keys_for_session::v3::Response::new(etag, count))
|
||||
}
|
||||
|
||||
async fn get_count_etag(
|
||||
|
||||
@@ -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,8 +53,8 @@ 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 })
|
||||
Ok(get_capabilities::v3::Response::new(capabilities))
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(crate) async fn put_dehydrated_device_route(
|
||||
.set_dehydrated_device(sender_user, body.body)
|
||||
.await?;
|
||||
|
||||
Ok(put_dehydrated_device::Response { device_id })
|
||||
Ok(put_dehydrated_device::Response::new(device_id))
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/../dehydrated_device`
|
||||
@@ -51,7 +51,7 @@ pub(crate) async fn delete_dehydrated_device_route(
|
||||
|
||||
services.users.remove_device(sender_user, &device_id).await;
|
||||
|
||||
Ok(delete_dehydrated_device::Response { device_id })
|
||||
Ok(delete_dehydrated_device::Response::new(device_id))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/../dehydrated_device`
|
||||
@@ -67,10 +67,7 @@ pub(crate) async fn get_dehydrated_device_route(
|
||||
|
||||
let device = services.users.get_dehydrated_device(sender_user).await?;
|
||||
|
||||
Ok(get_dehydrated_device::Response {
|
||||
device_id: device.device_id,
|
||||
device_data: device.device_data,
|
||||
})
|
||||
Ok(get_dehydrated_device::Response::new(device.device_id, device.device_data))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
|
||||
|
||||
+25
-93
@@ -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`
|
||||
@@ -27,7 +25,7 @@ pub(crate) async fn get_devices_route(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(get_devices::v3::Response { devices })
|
||||
Ok(get_devices::v3::Response::new(devices))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/devices/{deviceId}`
|
||||
@@ -43,7 +41,7 @@ pub(crate) async fn get_device_route(
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("Device not found."))))?;
|
||||
|
||||
Ok(get_device::v3::Response { device })
|
||||
Ok(get_device::v3::Response::new(device))
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
|
||||
@@ -75,19 +73,16 @@ pub(crate) async fn update_device_route(
|
||||
.update_device_metadata(sender_user, &body.device_id, &device)
|
||||
.await?;
|
||||
|
||||
Ok(update_device::v3::Response {})
|
||||
Ok(update_device::v3::Response::new())
|
||||
},
|
||||
| Err(_) => {
|
||||
let Some(appservice) = appservice else {
|
||||
return Err!(Request(NotFound("Device not found.")));
|
||||
};
|
||||
if !appservice.registration.device_management {
|
||||
return Err!(Request(NotFound("Device not found.")));
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Creating new device for {sender_user} from appservice {} as MSC4190 is enabled \
|
||||
and device ID does not exist",
|
||||
"Creating new device for {sender_user} from appservice {} as device ID does not \
|
||||
exist",
|
||||
appservice.registration.id
|
||||
);
|
||||
|
||||
@@ -104,7 +99,7 @@ pub(crate) async fn update_device_route(
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(update_device::v3::Response {});
|
||||
return Ok(update_device::v3::Response::new());
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -123,57 +118,24 @@ 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) {
|
||||
debug!(
|
||||
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
|
||||
enabled"
|
||||
);
|
||||
if appservice.is_some() {
|
||||
debug!("Skipping UIAA for {sender_user} as this is from an appservice");
|
||||
services
|
||||
.users
|
||||
.remove_device(sender_user, &body.device_id)
|
||||
.await;
|
||||
|
||||
return Ok(delete_device::v3::Response {});
|
||||
return Ok(delete_device::v3::Response::new());
|
||||
}
|
||||
|
||||
// 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 +162,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 +177,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;
|
||||
|
||||
+10
-36
@@ -26,7 +26,7 @@
|
||||
},
|
||||
federation,
|
||||
},
|
||||
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
|
||||
directory::{Filter, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
@@ -34,6 +34,7 @@
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
},
|
||||
room::JoinRuleKind,
|
||||
uint,
|
||||
};
|
||||
|
||||
@@ -142,7 +143,13 @@ pub(crate) async fn set_room_visibility_route(
|
||||
return Err!(Request(Forbidden("Guests cannot publish to room directories")));
|
||||
}
|
||||
|
||||
if !user_can_publish_room(&services, sender_user, &body.room_id).await? {
|
||||
let room_power_levels = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_power_levels(&body.room_id)
|
||||
.await;
|
||||
|
||||
if !room_power_levels.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility) {
|
||||
return Err!(Request(Forbidden("User is not allowed to publish this room")));
|
||||
}
|
||||
|
||||
@@ -339,39 +346,6 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
||||
})
|
||||
}
|
||||
|
||||
/// Check whether the user can publish to the room directory via power levels of
|
||||
/// room history visibility event or room creator
|
||||
async fn user_can_publish_room(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
room_id: &RoomId,
|
||||
) -> Result<bool> {
|
||||
match services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
|
||||
.await
|
||||
{
|
||||
| Ok(event) => serde_json::from_str(event.content().get())
|
||||
.map_err(|_| err!(Database("Invalid event content for m.room.power_levels")))
|
||||
.map(|content: RoomPowerLevelsEventContent| {
|
||||
RoomPowerLevels::from(content)
|
||||
.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
|
||||
}),
|
||||
| _ => {
|
||||
match services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
{
|
||||
| Ok(event) => Ok(event.sender() == user_id),
|
||||
| _ => Err!(Request(Forbidden("User is not allowed to publish this room"))),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> PublicRoomsChunk {
|
||||
let name = services.rooms.state_accessor.get_name(&room_id).ok();
|
||||
|
||||
@@ -394,7 +368,7 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
|
||||
.state_accessor
|
||||
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
|
||||
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
|
||||
| JoinRule::Public => PublicRoomJoinRule::Public,
|
||||
| JoinRule::Public => JoinRuleKind::Public,
|
||||
| JoinRule::Knock => "knock".into(),
|
||||
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
|
||||
| _ => "invite".into(),
|
||||
|
||||
+6
-39
@@ -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?;
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+14
-2
@@ -13,7 +13,7 @@
|
||||
};
|
||||
use reqwest::Url;
|
||||
use ruma::{
|
||||
Mxc, UserId,
|
||||
UserId,
|
||||
api::client::{
|
||||
authenticated_media::{
|
||||
get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
|
||||
@@ -114,7 +114,19 @@ pub(crate) async fn get_content_thumbnail_route(
|
||||
content,
|
||||
content_type,
|
||||
content_disposition,
|
||||
} = fetch_thumbnail(&services, &mxc, user, body.timeout_ms, &dim).await?;
|
||||
} = match fetch_thumbnail(&services, &mxc, user, body.timeout_ms, &dim).await {
|
||||
| Ok(meta) => meta,
|
||||
| Err(conduwuit::Error::Io(e)) => match e.kind() {
|
||||
| std::io::ErrorKind::NotFound =>
|
||||
return Err!(Request(NotFound("Thumbnail not found."))),
|
||||
| std::io::ErrorKind::PermissionDenied => {
|
||||
error!("Permission denied when trying to read file: {e:?}");
|
||||
return Err!(Request(Unknown("Unknown error when fetching thumbnail.")));
|
||||
},
|
||||
| _ => return Err!(Request(Unknown("Unknown error when fetching thumbnail."))),
|
||||
},
|
||||
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching thumbnail."))),
|
||||
};
|
||||
|
||||
Ok(get_content_thumbnail::v1::Response {
|
||||
file: content.expect("entire file contents"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
|
||||
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
|
||||
use ruma::{
|
||||
api::client::membership::ban_user,
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
@@ -24,30 +24,29 @@ pub(crate) async fn ban_user_route(
|
||||
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
||||
}
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
|
||||
|
||||
let current_member_content = services
|
||||
let mut content = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_member(&body.room_id, &body.user_id)
|
||||
.await
|
||||
.unwrap_or_else(|_| RoomMemberEventContent::new(MembershipState::Ban));
|
||||
|
||||
content.membership = MembershipState::Ban;
|
||||
content.reason = body.reason.clone();
|
||||
content.displayname = None;
|
||||
content.avatar_url = None;
|
||||
content.is_direct = None;
|
||||
content.join_authorized_via_users_server = None;
|
||||
content.third_party_invite = None;
|
||||
// TODO(upstream): MSC4293
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
|
||||
membership: MembershipState::Ban,
|
||||
reason: body.reason.clone(),
|
||||
displayname: None, // display name may be offensive
|
||||
avatar_url: None, // avatar may be offensive
|
||||
is_direct: None,
|
||||
join_authorized_via_users_server: None,
|
||||
third_party_invite: None,
|
||||
redact_events: body.redact_events,
|
||||
..current_member_content
|
||||
}),
|
||||
PartialPdu::state(body.user_id.to_string(), &content),
|
||||
sender_user,
|
||||
Some(&body.room_id),
|
||||
&state_lock,
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, debug_error, err, info,
|
||||
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
|
||||
matrix::{event::gen_event_id_canonical_json, pdu::PartialPdu},
|
||||
warn,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use ruma::{
|
||||
RoomId, UserId,
|
||||
api::{client::membership::invite_user, federation::membership::create_invite},
|
||||
events::{
|
||||
invite_permission_config::FilterLevel,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
api::{
|
||||
client::membership::invite_user::{self, v3::InviteUserId},
|
||||
federation::membership::{RawStrippedState, create_invite},
|
||||
},
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
};
|
||||
use ruminuwuity::invite_permission_config::FilterLevel;
|
||||
use service::Services;
|
||||
|
||||
use super::banned_room_check;
|
||||
@@ -51,7 +52,11 @@ pub(crate) async fn invite_user_route(
|
||||
.await?;
|
||||
|
||||
match &body.recipient {
|
||||
| invite_user::v3::InvitationRecipient::UserId { user_id: recipient_user } => {
|
||||
| invite_user::v3::InvitationRecipient::UserId(InviteUserId {
|
||||
user_id: recipient_user,
|
||||
reason,
|
||||
..
|
||||
}) => {
|
||||
let sender_filter_level = services
|
||||
.users
|
||||
.invite_filter_level(recipient_user, sender_user)
|
||||
@@ -59,7 +64,7 @@ pub(crate) async fn invite_user_route(
|
||||
|
||||
if !matches!(sender_filter_level, FilterLevel::Allow) {
|
||||
// drop invites if the sender has the recipient filtered
|
||||
return Ok(invite_user::v3::Response {});
|
||||
return Ok(invite_user::v3::Response::new());
|
||||
}
|
||||
|
||||
if let Ok(target_user_membership) = services
|
||||
@@ -95,13 +100,13 @@ pub(crate) async fn invite_user_route(
|
||||
sender_user,
|
||||
recipient_user,
|
||||
&body.room_id,
|
||||
body.reason.clone(),
|
||||
reason.clone(),
|
||||
false,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
Ok(invite_user::v3::Response {})
|
||||
Ok(invite_user::v3::Response::new())
|
||||
},
|
||||
| _ => {
|
||||
Err!(Request(NotFound("User not found.")))
|
||||
@@ -141,25 +146,32 @@ pub(crate) async fn invite_helper(
|
||||
let (pdu, pdu_json, invite_room_state) = {
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).await;
|
||||
|
||||
let content = RoomMemberEventContent {
|
||||
avatar_url: services.users.avatar_url(recipient_user).await.ok(),
|
||||
is_direct: Some(is_direct),
|
||||
reason,
|
||||
..RoomMemberEventContent::new(MembershipState::Invite)
|
||||
};
|
||||
let mut content = RoomMemberEventContent::new(MembershipState::Invite);
|
||||
content.displayname = services.users.displayname(recipient_user).await.ok();
|
||||
content.avatar_url = services.users.avatar_url(recipient_user).await.ok();
|
||||
content.is_direct = Some(is_direct);
|
||||
content.reason = reason;
|
||||
|
||||
let (pdu, pdu_json) = services
|
||||
.rooms
|
||||
.timeline
|
||||
.create_hash_and_sign_event(
|
||||
PduBuilder::state(recipient_user.to_string(), &content),
|
||||
PartialPdu::state(recipient_user.to_string(), &content),
|
||||
sender_user,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let invite_room_state = services.rooms.state.summary_stripped(&pdu, room_id).await;
|
||||
#[allow(deprecated)]
|
||||
let invite_room_state = services
|
||||
.rooms
|
||||
.state
|
||||
.summary_stripped(&pdu, room_id)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|event| RawStrippedState::Stripped(event))
|
||||
.collect();
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
@@ -168,32 +180,39 @@ pub(crate) async fn invite_helper(
|
||||
|
||||
let room_version_id = services.rooms.state.get_room_version(room_id).await?;
|
||||
|
||||
let mut request = create_invite::v2::Request::new(
|
||||
room_id.to_owned(),
|
||||
(*pdu.event_id).to_owned(),
|
||||
room_version_id.clone(),
|
||||
services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(pdu_json.clone())
|
||||
.await,
|
||||
invite_room_state,
|
||||
);
|
||||
request.via = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_route_via(room_id)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let response = services
|
||||
.sending
|
||||
.send_federation_request(recipient_user.server_name(), create_invite::v2::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: (*pdu.event_id).to_owned(),
|
||||
room_version: room_version_id.clone(),
|
||||
event: services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(pdu_json.clone())
|
||||
.await,
|
||||
invite_room_state,
|
||||
via: services
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_route_via(room_id)
|
||||
.await
|
||||
.ok(),
|
||||
})
|
||||
.send_federation_request(recipient_user.server_name(), request)
|
||||
.await?;
|
||||
|
||||
// We do not add the event_id field to the pdu here because of signature and
|
||||
// hashes checks
|
||||
let (event_id, value) = gen_event_id_canonical_json(&response.event, &room_version_id)
|
||||
.map_err(|e| {
|
||||
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
|
||||
})?;
|
||||
let (event_id, value) = gen_event_id_canonical_json(
|
||||
&response.event,
|
||||
&room_version_id
|
||||
.rules()
|
||||
.expect("room version should have defined rules"),
|
||||
)
|
||||
.map_err(|e| {
|
||||
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
|
||||
})?;
|
||||
|
||||
if pdu.event_id != event_id {
|
||||
return Err!(Request(BadJson(warn!(
|
||||
@@ -229,19 +248,17 @@ pub(crate) async fn invite_helper(
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).await;
|
||||
|
||||
let content = RoomMemberEventContent {
|
||||
displayname: services.users.displayname(recipient_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(recipient_user).await.ok(),
|
||||
blurhash: services.users.blurhash(recipient_user).await.ok(),
|
||||
is_direct: Some(is_direct),
|
||||
reason,
|
||||
..RoomMemberEventContent::new(MembershipState::Invite)
|
||||
};
|
||||
let mut content = RoomMemberEventContent::new(MembershipState::Invite);
|
||||
content.displayname = services.users.displayname(recipient_user).await.ok();
|
||||
content.avatar_url = services.users.avatar_url(recipient_user).await.ok();
|
||||
content.blurhash = services.users.blurhash(recipient_user).await.ok();
|
||||
content.is_direct = Some(is_direct);
|
||||
content.reason = reason;
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder::state(recipient_user.to_string(), &content),
|
||||
sender_user,
|
||||
Some(room_id),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
matrix::{
|
||||
StateKey,
|
||||
event::{gen_event_id, gen_event_id_canonical_json},
|
||||
pdu::{PduBuilder, PduEvent},
|
||||
pdu::{PartialPdu, PduEvent},
|
||||
state_res,
|
||||
},
|
||||
result::FlatOk,
|
||||
@@ -24,10 +24,8 @@
|
||||
CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
|
||||
RoomVersionId, UserId,
|
||||
api::{
|
||||
client::{
|
||||
error::ErrorKind,
|
||||
membership::{join_room_by_id, join_room_by_id_or_alias},
|
||||
},
|
||||
client::membership::{join_room_by_id, join_room_by_id_or_alias},
|
||||
error::{ErrorKind, IncompatibleRoomVersionErrorData},
|
||||
federation::{self},
|
||||
},
|
||||
canonical_json::to_canonical_value,
|
||||
@@ -89,7 +87,6 @@ pub(crate) async fn join_room_by_id_route(
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&body.room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
@@ -168,7 +165,6 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await,
|
||||
);
|
||||
@@ -209,11 +205,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let addl_via_servers = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&room_id)
|
||||
.map(ToOwned::to_owned);
|
||||
let addl_via_servers = services.rooms.state_cache.servers_invite_via(&room_id);
|
||||
|
||||
let addl_state_servers = services
|
||||
.rooms
|
||||
@@ -226,7 +218,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||
.iter()
|
||||
.map(|event| event.get_field("sender"))
|
||||
.filter_map(FlatOk::flat_ok)
|
||||
.map(|user: &UserId| user.server_name().to_owned())
|
||||
.map(|user: OwnedUserId| user.server_name().to_owned())
|
||||
.stream()
|
||||
.chain(addl_via_servers)
|
||||
.collect()
|
||||
@@ -252,7 +244,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
Ok(join_room_by_id_or_alias::v3::Response { room_id: join_room_response.room_id })
|
||||
Ok(join_room_by_id_or_alias::v3::Response::new(join_room_response.room_id))
|
||||
}
|
||||
|
||||
pub async fn join_room_by_id_helper(
|
||||
@@ -283,7 +275,7 @@ pub async fn join_room_by_id_helper(
|
||||
.await
|
||||
{
|
||||
debug_warn!("{sender_user} is already joined in {room_id}");
|
||||
return Ok(join_room_by_id::v3::Response { room_id: room_id.into() });
|
||||
return Ok(join_room_by_id::v3::Response::new(room_id.to_owned()));
|
||||
}
|
||||
|
||||
if let Err(e) = services
|
||||
@@ -383,12 +375,15 @@ async fn join_room_by_id_helper_remote(
|
||||
|
||||
info!("make_join finished");
|
||||
|
||||
let room_version_id = make_join_response.room_version.unwrap_or(RoomVersionId::V1);
|
||||
let room_version = make_join_response.room_version.unwrap_or(RoomVersionId::V1);
|
||||
let room_version_rules = room_version
|
||||
.rules()
|
||||
.expect("room version should have defined rules");
|
||||
|
||||
if !services.server.supported_room_version(&room_version_id) {
|
||||
if !services.server.supported_room_version(&room_version) {
|
||||
// How did we get here?
|
||||
return Err!(BadServerResponse(
|
||||
"Remote room version {room_version_id} is not supported by conduwuit"
|
||||
"Remote room version {room_version} is not supported by conduwuit"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -401,7 +396,7 @@ async fn join_room_by_id_helper_remote(
|
||||
|
||||
let join_authorized_via_users_server = {
|
||||
use RoomVersionId::*;
|
||||
if !matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
|
||||
if !matches!(room_version, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
|
||||
join_event_stub
|
||||
.get("content")
|
||||
.map(|s| {
|
||||
@@ -423,36 +418,30 @@ async fn join_room_by_id_helper_remote(
|
||||
.expect("Timestamp is valid js_int value"),
|
||||
),
|
||||
);
|
||||
|
||||
let mut join_content = RoomMemberEventContent::new(MembershipState::Join);
|
||||
join_content.displayname = services.users.displayname(sender_user).await.ok();
|
||||
join_content.avatar_url = services.users.avatar_url(sender_user).await.ok();
|
||||
join_content.blurhash = services.users.blurhash(sender_user).await.ok();
|
||||
join_content.reason = reason;
|
||||
join_content.join_authorized_via_users_server = join_authorized_via_users_server.clone();
|
||||
|
||||
join_event_stub.insert(
|
||||
"content".to_owned(),
|
||||
to_canonical_value(RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
reason,
|
||||
join_authorized_via_users_server: join_authorized_via_users_server.clone(),
|
||||
..RoomMemberEventContent::new(MembershipState::Join)
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
to_canonical_value(join_content).expect("event is valid, we just created it"),
|
||||
);
|
||||
|
||||
// We keep the "event_id" in the pdu only in v1 or
|
||||
// v2 rooms
|
||||
match room_version_id {
|
||||
| RoomVersionId::V1 | RoomVersionId::V2 => {},
|
||||
| _ => {
|
||||
join_event_stub.remove("event_id");
|
||||
},
|
||||
}
|
||||
// Remove event id if it exists
|
||||
join_event_stub.remove("event_id");
|
||||
|
||||
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
||||
// to be present
|
||||
services
|
||||
.server_keys
|
||||
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
|
||||
.hash_and_sign_event(&mut join_event_stub, &room_version_rules)?;
|
||||
|
||||
// Generate event id
|
||||
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
|
||||
let event_id = gen_event_id(&join_event_stub, &room_version_rules)?;
|
||||
|
||||
// Add event_id back
|
||||
join_event_stub
|
||||
@@ -462,15 +451,14 @@ async fn join_room_by_id_helper_remote(
|
||||
let mut join_event = join_event_stub;
|
||||
|
||||
info!("Asking {remote_server} for send_join in room {room_id}");
|
||||
let send_join_request = federation::membership::create_join_event::v2::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: event_id.clone(),
|
||||
omit_members: false,
|
||||
pdu: services
|
||||
let send_join_request = federation::membership::create_join_event::v2::Request::new(
|
||||
room_id.to_owned(),
|
||||
event_id.clone(),
|
||||
services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(join_event.clone())
|
||||
.await,
|
||||
};
|
||||
);
|
||||
|
||||
let send_join_response = match services
|
||||
.sending
|
||||
@@ -494,7 +482,7 @@ async fn join_room_by_id_helper_remote(
|
||||
);
|
||||
|
||||
let (signed_event_id, signed_value) =
|
||||
gen_event_id_canonical_json(signed_raw, &room_version_id).map_err(|e| {
|
||||
gen_event_id_canonical_json(signed_raw, &room_version_rules).map_err(|e| {
|
||||
err!(Request(BadJson(warn!(
|
||||
"Could not convert event to canonical JSON: {e}"
|
||||
))))
|
||||
@@ -569,7 +557,7 @@ async fn join_room_by_id_helper_remote(
|
||||
.then(|pdu| {
|
||||
services
|
||||
.server_keys
|
||||
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
|
||||
.validate_and_add_event_id_no_fetch(pdu, &room_version_rules)
|
||||
.inspect_err(|e| {
|
||||
debug_warn!("Could not validate send_join response room_state event: {e:?}");
|
||||
})
|
||||
@@ -617,7 +605,7 @@ async fn join_room_by_id_helper_remote(
|
||||
.then(|pdu| {
|
||||
services
|
||||
.server_keys
|
||||
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
|
||||
.validate_and_add_event_id_no_fetch(pdu, &room_version_rules)
|
||||
})
|
||||
.ready_filter_map(Result::ok)
|
||||
.ready_for_each(|(event_id, value)| {
|
||||
@@ -638,7 +626,7 @@ async fn join_room_by_id_helper_remote(
|
||||
};
|
||||
|
||||
let auth_check = state_res::event_auth::auth_check(
|
||||
&state_res::RoomVersion::new(&room_version_id)?,
|
||||
&room_version.rules().unwrap(),
|
||||
&parsed_join_pdu,
|
||||
None, // TODO: third party invite
|
||||
|k, s| state_fetch(k.clone(), s.into()),
|
||||
@@ -759,21 +747,19 @@ async fn join_room_by_id_helper_local(
|
||||
}
|
||||
}
|
||||
|
||||
let content = RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
reason: reason.clone(),
|
||||
join_authorized_via_users_server: auth_user,
|
||||
..RoomMemberEventContent::new(MembershipState::Join)
|
||||
};
|
||||
let mut content = RoomMemberEventContent::new(MembershipState::Join);
|
||||
content.displayname = services.users.displayname(sender_user).await.ok();
|
||||
content.avatar_url = services.users.avatar_url(sender_user).await.ok();
|
||||
content.blurhash = services.users.blurhash(sender_user).await.ok();
|
||||
content.reason = reason.clone();
|
||||
content.join_authorized_via_users_server = auth_user;
|
||||
|
||||
// Try normal join first
|
||||
let Err(error) = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(sender_user.to_string(), &content),
|
||||
PartialPdu::state(sender_user.to_string(), &content),
|
||||
sender_user,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
@@ -785,6 +771,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);
|
||||
}
|
||||
|
||||
@@ -813,16 +808,16 @@ async fn make_join_request(
|
||||
"Asking {remote_server} for make_join (attempt {make_join_counter}/{})",
|
||||
servers.len()
|
||||
);
|
||||
|
||||
let mut request = federation::membership::prepare_join_event::v1::Request::new(
|
||||
room_id.to_owned(),
|
||||
sender_user.to_owned(),
|
||||
);
|
||||
request.ver = services.server.supported_room_versions().collect();
|
||||
|
||||
let make_join_response = services
|
||||
.sending
|
||||
.send_federation_request(
|
||||
remote_server,
|
||||
federation::membership::prepare_join_event::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
user_id: sender_user.to_owned(),
|
||||
ver: services.server.supported_room_versions().collect(),
|
||||
},
|
||||
)
|
||||
.send_federation_request(remote_server, request)
|
||||
.await;
|
||||
|
||||
trace!("make_join response: {:?}", make_join_response);
|
||||
@@ -855,7 +850,10 @@ async fn make_join_request(
|
||||
rules, but is unable to authorise a join for us. Will continue trying."
|
||||
);
|
||||
},
|
||||
| ErrorKind::IncompatibleRoomVersion { room_version } => {
|
||||
| ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData {
|
||||
room_version,
|
||||
..
|
||||
}) => {
|
||||
warn!(
|
||||
"{remote_server} reports the room we are trying to join is \
|
||||
v{room_version}, which we do not support."
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
|
||||
use ruma::{
|
||||
api::client::membership::kick_user,
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
};
|
||||
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
|
||||
use ruma::{api::client::membership::kick_user, events::room::member::MembershipState};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -18,9 +15,9 @@ pub(crate) async fn kick_user_route(
|
||||
if services.users.is_suspended(sender_user).await? {
|
||||
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
||||
}
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
|
||||
|
||||
let Ok(event) = services
|
||||
let Ok(mut event) = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_member(&body.room_id, &body.user_id)
|
||||
@@ -41,18 +38,17 @@ pub(crate) async fn kick_user_route(
|
||||
)));
|
||||
}
|
||||
|
||||
event.membership = MembershipState::Leave;
|
||||
event.reason = body.reason.clone();
|
||||
event.is_direct = None;
|
||||
event.join_authorized_via_users_server = None;
|
||||
event.third_party_invite = None;
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
|
||||
membership: MembershipState::Leave,
|
||||
reason: body.reason.clone(),
|
||||
is_direct: None,
|
||||
join_authorized_via_users_server: None,
|
||||
third_party_invite: None,
|
||||
..event
|
||||
}),
|
||||
PartialPdu::state(body.user_id.to_string(), &event),
|
||||
sender_user,
|
||||
Some(&body.room_id),
|
||||
&state_lock,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
Err, Result, debug, debug_info, debug_warn, err, info,
|
||||
matrix::{
|
||||
event::gen_event_id,
|
||||
pdu::{PduBuilder, PduEvent},
|
||||
pdu::{PartialPdu, PduEvent},
|
||||
},
|
||||
result::FlatOk,
|
||||
trace,
|
||||
@@ -15,8 +15,8 @@
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName, RoomId,
|
||||
RoomVersionId, UserId,
|
||||
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName,
|
||||
OwnedUserId, RoomId, UserId,
|
||||
api::{
|
||||
client::knock::knock_room,
|
||||
federation::{self},
|
||||
@@ -73,7 +73,6 @@ pub(crate) async fn knock_room_route(
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await,
|
||||
);
|
||||
@@ -113,11 +112,7 @@ pub(crate) async fn knock_room_route(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let addl_via_servers = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(&room_id)
|
||||
.map(ToOwned::to_owned);
|
||||
let addl_via_servers = services.rooms.state_cache.servers_invite_via(&room_id);
|
||||
|
||||
let addl_state_servers = services
|
||||
.rooms
|
||||
@@ -130,7 +125,7 @@ pub(crate) async fn knock_room_route(
|
||||
.iter()
|
||||
.map(|event| event.get_field("sender"))
|
||||
.filter_map(FlatOk::flat_ok)
|
||||
.map(|user: &UserId| user.server_name().to_owned())
|
||||
.map(|user: OwnedUserId| user.server_name().to_owned())
|
||||
.stream()
|
||||
.chain(addl_via_servers)
|
||||
.collect()
|
||||
@@ -188,7 +183,7 @@ async fn knock_room_by_id_helper(
|
||||
.await
|
||||
{
|
||||
debug_warn!("{sender_user} is already knocked in {room_id}");
|
||||
return Ok(knock_room::v3::Response { room_id: room_id.into() });
|
||||
return Ok(knock_room::v3::Response::new(room_id.into()));
|
||||
}
|
||||
|
||||
if let Ok(membership) = services
|
||||
@@ -339,34 +334,27 @@ async fn knock_room_helper_local(
|
||||
) -> Result {
|
||||
debug_info!("We can knock locally");
|
||||
|
||||
let room_version_id = services.rooms.state.get_room_version(room_id).await?;
|
||||
let room_version = services.rooms.state.get_room_version(room_id).await?;
|
||||
let room_version_rules = room_version
|
||||
.rules()
|
||||
.expect("room version should have defined rules");
|
||||
|
||||
if matches!(
|
||||
room_version_id,
|
||||
RoomVersionId::V1
|
||||
| RoomVersionId::V2
|
||||
| RoomVersionId::V3
|
||||
| RoomVersionId::V4
|
||||
| RoomVersionId::V5
|
||||
| RoomVersionId::V6
|
||||
) {
|
||||
if !room_version_rules.authorization.knocking {
|
||||
return Err!(Request(Forbidden("This room does not support knocking.")));
|
||||
}
|
||||
|
||||
let content = RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
reason: reason.clone(),
|
||||
..RoomMemberEventContent::new(MembershipState::Knock)
|
||||
};
|
||||
let mut content = RoomMemberEventContent::new(MembershipState::Knock);
|
||||
content.displayname = services.users.displayname(sender_user).await.ok();
|
||||
content.avatar_url = services.users.avatar_url(sender_user).await.ok();
|
||||
content.blurhash = services.users.blurhash(sender_user).await.ok();
|
||||
content.reason = reason.clone();
|
||||
|
||||
// Try normal knock first
|
||||
let Err(error) = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(sender_user.to_string(), &content),
|
||||
PartialPdu::state(sender_user.to_string(), &content),
|
||||
sender_user,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
@@ -381,19 +369,18 @@ async fn knock_room_helper_local(
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
warn!("We couldn't do the knock locally, maybe federation can help to satisfy the knock");
|
||||
|
||||
let (make_knock_response, remote_server) =
|
||||
make_knock_request(services, sender_user, room_id, servers).await?;
|
||||
|
||||
info!("make_knock finished");
|
||||
|
||||
let room_version_id = make_knock_response.room_version;
|
||||
let room_version = make_knock_response.room_version;
|
||||
let room_version_rules = room_version
|
||||
.rules()
|
||||
.expect("room version should have defined rules");
|
||||
|
||||
if !services.server.supported_room_version(&room_version_id) {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote room version {room_version_id} is not supported by conduwuit"
|
||||
));
|
||||
if !services.server.supported_room_version(&room_version) {
|
||||
return Err!(BadServerResponse("Remote room version {room_version} is not supported"));
|
||||
}
|
||||
|
||||
let mut knock_event_stub = serde_json::from_str::<CanonicalJsonObject>(
|
||||
@@ -424,24 +411,17 @@ async fn knock_room_helper_local(
|
||||
);
|
||||
knock_event_stub.insert(
|
||||
"content".to_owned(),
|
||||
to_canonical_value(RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
reason,
|
||||
..RoomMemberEventContent::new(MembershipState::Knock)
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
to_canonical_value(content).expect("event is valid, we just created it"),
|
||||
);
|
||||
|
||||
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
||||
// to be present
|
||||
services
|
||||
.server_keys
|
||||
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
|
||||
.hash_and_sign_event(&mut knock_event_stub, &room_version_rules)?;
|
||||
|
||||
// Generate event id
|
||||
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
|
||||
let event_id = gen_event_id(&knock_event_stub, &room_version_rules)?;
|
||||
|
||||
// Add event_id
|
||||
knock_event_stub
|
||||
@@ -451,14 +431,14 @@ async fn knock_room_helper_local(
|
||||
let knock_event = knock_event_stub;
|
||||
|
||||
info!("Asking {remote_server} for send_knock in room {room_id}");
|
||||
let send_knock_request = federation::knock::send_knock::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: event_id.clone(),
|
||||
pdu: services
|
||||
let send_knock_request = federation::membership::create_knock_event::v1::Request::new(
|
||||
room_id.to_owned(),
|
||||
event_id.clone(),
|
||||
services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(knock_event.clone())
|
||||
.await,
|
||||
};
|
||||
);
|
||||
|
||||
services
|
||||
.sending
|
||||
@@ -520,11 +500,14 @@ async fn knock_room_helper_remote(
|
||||
|
||||
info!("make_knock finished");
|
||||
|
||||
let room_version_id = make_knock_response.room_version;
|
||||
let room_version = make_knock_response.room_version;
|
||||
let room_version_rules = room_version
|
||||
.rules()
|
||||
.expect("room version should have defined rules");
|
||||
|
||||
if !services.server.supported_room_version(&room_version_id) {
|
||||
if !services.server.supported_room_version(&room_version) {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote room version {room_version_id} is not supported by conduwuit"
|
||||
"Remote room version {room_version} is not supported by conduwuit"
|
||||
));
|
||||
}
|
||||
|
||||
@@ -545,26 +528,26 @@ async fn knock_room_helper_remote(
|
||||
.expect("Timestamp is valid js_int value"),
|
||||
),
|
||||
);
|
||||
|
||||
let mut knock_content = RoomMemberEventContent::new(MembershipState::Knock);
|
||||
knock_content.displayname = services.users.displayname(sender_user).await.ok();
|
||||
knock_content.avatar_url = services.users.avatar_url(sender_user).await.ok();
|
||||
knock_content.blurhash = services.users.blurhash(sender_user).await.ok();
|
||||
knock_content.reason = reason;
|
||||
|
||||
knock_event_stub.insert(
|
||||
"content".to_owned(),
|
||||
to_canonical_value(RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
reason,
|
||||
..RoomMemberEventContent::new(MembershipState::Knock)
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
to_canonical_value(knock_content).expect("event is valid, we just created it"),
|
||||
);
|
||||
|
||||
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
||||
// to be present
|
||||
services
|
||||
.server_keys
|
||||
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
|
||||
.hash_and_sign_event(&mut knock_event_stub, &room_version_rules)?;
|
||||
|
||||
// Generate event id
|
||||
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
|
||||
let event_id = gen_event_id(&knock_event_stub, &room_version_rules)?;
|
||||
|
||||
// Add event_id
|
||||
knock_event_stub
|
||||
@@ -574,18 +557,18 @@ async fn knock_room_helper_remote(
|
||||
let knock_event = knock_event_stub;
|
||||
|
||||
info!("Asking {remote_server} for send_knock in room {room_id}");
|
||||
let send_knock_request = federation::knock::send_knock::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: event_id.clone(),
|
||||
pdu: services
|
||||
let request = federation::membership::create_knock_event::v1::Request::new(
|
||||
room_id.to_owned(),
|
||||
event_id.clone(),
|
||||
services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(knock_event.clone())
|
||||
.await,
|
||||
};
|
||||
);
|
||||
|
||||
let send_knock_response = services
|
||||
.sending
|
||||
.send_federation_request(&remote_server, send_knock_request)
|
||||
.send_federation_request(&remote_server, request)
|
||||
.await?;
|
||||
|
||||
info!("send_knock finished");
|
||||
@@ -604,7 +587,17 @@ async fn knock_room_helper_remote(
|
||||
let state = send_knock_response
|
||||
.knock_room_state
|
||||
.iter()
|
||||
.map(|event| serde_json::from_str::<CanonicalJsonObject>(event.clone().into_json().get()))
|
||||
.map(|event| {
|
||||
#[allow(deprecated)]
|
||||
let raw_value = match event {
|
||||
| federation::membership::RawStrippedState::Stripped(raw_state) =>
|
||||
&raw_state.clone().into_json(),
|
||||
| federation::membership::RawStrippedState::Pdu(raw_value) => raw_value,
|
||||
| _ => panic!("unknown raw stripped state type"),
|
||||
};
|
||||
|
||||
serde_json::from_str::<CanonicalJsonObject>(raw_value.get())
|
||||
})
|
||||
.filter_map(Result::ok);
|
||||
|
||||
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
|
||||
@@ -629,7 +622,7 @@ async fn knock_room_helper_remote(
|
||||
continue;
|
||||
};
|
||||
|
||||
let event_id = gen_event_id(&event, &room_version_id)?;
|
||||
let event_id = gen_event_id(&event, &room_version_rules)?;
|
||||
let shortstatekey = services
|
||||
.rooms
|
||||
.short
|
||||
@@ -709,7 +702,7 @@ async fn make_knock_request(
|
||||
sender_user: &UserId,
|
||||
room_id: &RoomId,
|
||||
servers: &[OwnedServerName],
|
||||
) -> Result<(federation::knock::create_knock_event_template::v1::Response, OwnedServerName)> {
|
||||
) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> {
|
||||
let mut make_knock_response_and_server =
|
||||
Err!(BadServerResponse("No server available to assist in knocking."));
|
||||
|
||||
@@ -722,16 +715,15 @@ async fn make_knock_request(
|
||||
|
||||
info!("Asking {remote_server} for make_knock ({make_knock_counter})");
|
||||
|
||||
let mut request = federation::membership::prepare_knock_event::v1::Request::new(
|
||||
room_id.to_owned(),
|
||||
sender_user.to_owned(),
|
||||
);
|
||||
request.ver = services.server.supported_room_versions().collect();
|
||||
|
||||
let make_knock_response = services
|
||||
.sending
|
||||
.send_federation_request(
|
||||
remote_server,
|
||||
federation::knock::create_knock_event_template::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
user_id: sender_user.to_owned(),
|
||||
ver: services.server.supported_room_versions().collect(),
|
||||
},
|
||||
)
|
||||
.send_federation_request(remote_server, request)
|
||||
.await;
|
||||
|
||||
trace!("make_knock response: {make_knock_response:?}");
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Pdu, Result, debug_info, debug_warn, err,
|
||||
matrix::{event::gen_event_id, pdu::PduBuilder},
|
||||
matrix::{event::gen_event_id, pdu::PartialPdu},
|
||||
utils::{self, FutureBoolExt, future::ReadyEqExt},
|
||||
warn,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt, pin_mut};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, RoomVersionId, UserId,
|
||||
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, UserId,
|
||||
api::{
|
||||
client::membership::leave_room,
|
||||
federation::{self},
|
||||
@@ -42,11 +42,7 @@ pub(crate) async fn leave_room_route(
|
||||
// Make a user leave all their joined rooms, rescinds knocks, forgets all rooms,
|
||||
// and ignores errors
|
||||
pub async fn leave_all_rooms(services: &Services, user_id: &UserId) {
|
||||
let rooms_joined = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(user_id)
|
||||
.map(ToOwned::to_owned);
|
||||
let rooms_joined = services.rooms.state_cache.rooms_joined(user_id);
|
||||
|
||||
let rooms_invited = services
|
||||
.rooms
|
||||
@@ -142,18 +138,17 @@ pub async fn leave_room(
|
||||
.await;
|
||||
|
||||
match user_member_event_content {
|
||||
| Ok(content) => {
|
||||
| Ok(mut content) => {
|
||||
content.membership = MembershipState::Leave;
|
||||
content.reason = reason;
|
||||
content.join_authorized_via_users_server = None;
|
||||
content.is_direct = None;
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
membership: MembershipState::Leave,
|
||||
reason,
|
||||
join_authorized_via_users_server: None,
|
||||
is_direct: None,
|
||||
..content
|
||||
}),
|
||||
PartialPdu::state(user_id.to_string(), &content),
|
||||
user_id,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
@@ -226,7 +221,6 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
.rooms
|
||||
.state_cache
|
||||
.servers_invite_via(room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<HashSet<OwnedServerName>>()
|
||||
.await,
|
||||
);
|
||||
@@ -260,7 +254,7 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
.filter_map(|event| event.get_field("sender").ok().flatten())
|
||||
.filter_map(|sender: &str| UserId::parse(sender).ok())
|
||||
.filter_map(|sender| {
|
||||
if !services.globals.user_is_local(sender) {
|
||||
if !services.globals.user_is_local(&sender) {
|
||||
Some(sender.server_name().to_owned())
|
||||
} else {
|
||||
None
|
||||
@@ -289,10 +283,10 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
.sending
|
||||
.send_federation_request(
|
||||
remote_server.as_ref(),
|
||||
federation::membership::prepare_leave_event::v1::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
user_id: user_id.to_owned(),
|
||||
},
|
||||
federation::membership::prepare_leave_event::v1::Request::new(
|
||||
room_id.to_owned(),
|
||||
user_id.to_owned(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -329,6 +323,10 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
)));
|
||||
}
|
||||
|
||||
let room_version_rules = room_version_id
|
||||
.rules()
|
||||
.expect("room version should have defined rules");
|
||||
|
||||
let mut leave_event_stub = serde_json::from_str::<CanonicalJsonObject>(
|
||||
make_leave_response.event.get(),
|
||||
)
|
||||
@@ -366,21 +364,16 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
}
|
||||
|
||||
// room v3 and above removed the "event_id" field from remote PDU format
|
||||
match room_version_id {
|
||||
| RoomVersionId::V1 | RoomVersionId::V2 => {},
|
||||
| _ => {
|
||||
leave_event_stub.remove("event_id");
|
||||
},
|
||||
}
|
||||
leave_event_stub.remove("event_id");
|
||||
|
||||
// In order to create a compatible ref hash (EventID) the `hashes` field needs
|
||||
// to be present
|
||||
services
|
||||
.server_keys
|
||||
.hash_and_sign_event(&mut leave_event_stub, &room_version_id)?;
|
||||
.hash_and_sign_event(&mut leave_event_stub, &room_version_rules)?;
|
||||
|
||||
// Generate event id
|
||||
let event_id = gen_event_id(&leave_event_stub, &room_version_id)?;
|
||||
let event_id = gen_event_id(&leave_event_stub, &room_version_rules)?;
|
||||
|
||||
// Add event_id back
|
||||
leave_event_stub
|
||||
@@ -393,14 +386,14 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&remote_server,
|
||||
federation::membership::create_leave_event::v2::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: event_id.clone(),
|
||||
pdu: services
|
||||
federation::membership::create_leave_event::v2::Request::new(
|
||||
room_id.to_owned(),
|
||||
event_id.clone(),
|
||||
services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(leave_event.clone())
|
||||
.await,
|
||||
},
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
use futures::{FutureExt, StreamExt, future::join};
|
||||
use ruma::{
|
||||
api::client::membership::{
|
||||
get_member_events::{self, v3::MembershipEventFilter},
|
||||
get_member_events::{self},
|
||||
joined_members::{self, v3::RoomMember},
|
||||
},
|
||||
events::{
|
||||
@@ -43,20 +43,20 @@ pub(crate) async fn get_member_events_route(
|
||||
return Err!(Request(Forbidden("You don't have permission to view this room.")));
|
||||
}
|
||||
|
||||
Ok(get_member_events::v3::Response {
|
||||
chunk: services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full(&body.room_id)
|
||||
.ready_filter_map(Result::ok)
|
||||
.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
|
||||
.map(at!(1))
|
||||
.ready_filter_map(|pdu| membership_filter(pdu, membership, not_membership))
|
||||
.map(Event::into_format)
|
||||
.collect()
|
||||
.boxed()
|
||||
.await,
|
||||
})
|
||||
let chunk = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full(&body.room_id)
|
||||
.ready_filter_map(Result::ok)
|
||||
.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
|
||||
.map(at!(1))
|
||||
.ready_filter_map(|pdu| membership_filter(pdu, membership, not_membership))
|
||||
.map(Event::into_format)
|
||||
.collect()
|
||||
.boxed()
|
||||
.await;
|
||||
|
||||
Ok(get_member_events::v3::Response::new(chunk))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/joined_members`
|
||||
@@ -78,70 +78,46 @@ pub(crate) async fn joined_members_route(
|
||||
return Err!(Request(Forbidden("You don't have permission to view this room.")));
|
||||
}
|
||||
|
||||
Ok(joined_members::v3::Response {
|
||||
joined: services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&body.room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.broad_then(|user_id| async move {
|
||||
let (display_name, avatar_url) = join(
|
||||
services.users.displayname(&user_id).ok(),
|
||||
services.users.avatar_url(&user_id).ok(),
|
||||
)
|
||||
.await;
|
||||
let joined = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members(&body.room_id)
|
||||
.broad_then(|user_id| async move {
|
||||
let mut member = RoomMember::new();
|
||||
let (display_name, avatar_url) = join(
|
||||
services.users.displayname(&user_id).ok(),
|
||||
services.users.avatar_url(&user_id).ok(),
|
||||
)
|
||||
.await;
|
||||
member.display_name = display_name;
|
||||
member.avatar_url = avatar_url;
|
||||
|
||||
(user_id, RoomMember { display_name, avatar_url })
|
||||
})
|
||||
.collect()
|
||||
.await,
|
||||
})
|
||||
(user_id, member)
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(joined_members::v3::Response::new(joined))
|
||||
}
|
||||
|
||||
fn membership_filter<Pdu: Event>(
|
||||
pdu: Pdu,
|
||||
for_membership: Option<&MembershipEventFilter>,
|
||||
not_membership: Option<&MembershipEventFilter>,
|
||||
membership_state_filter: Option<&MembershipState>,
|
||||
not_membership_state_filter: Option<&MembershipState>,
|
||||
) -> Option<impl Event> {
|
||||
let membership_state_filter = match for_membership {
|
||||
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
|
||||
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
|
||||
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
|
||||
| Some(MembershipEventFilter::Leave) => MembershipState::Leave,
|
||||
| Some(_) | None => MembershipState::Join,
|
||||
};
|
||||
|
||||
let not_membership_state_filter = match not_membership {
|
||||
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
|
||||
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
|
||||
| Some(MembershipEventFilter::Join) => MembershipState::Join,
|
||||
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
|
||||
| Some(_) | None => MembershipState::Leave,
|
||||
};
|
||||
|
||||
let evt_membership = pdu.get_content::<RoomMemberEventContent>().ok()?.membership;
|
||||
|
||||
if for_membership.is_some() && not_membership.is_some() {
|
||||
if membership_state_filter != evt_membership
|
||||
|| not_membership_state_filter == evt_membership
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(pdu)
|
||||
}
|
||||
} else if for_membership.is_some() && not_membership.is_none() {
|
||||
if membership_state_filter != evt_membership {
|
||||
None
|
||||
} else {
|
||||
Some(pdu)
|
||||
}
|
||||
} else if not_membership.is_some() && for_membership.is_none() {
|
||||
if not_membership_state_filter == evt_membership {
|
||||
None
|
||||
} else {
|
||||
Some(pdu)
|
||||
}
|
||||
} else {
|
||||
Some(pdu)
|
||||
if let Some(membership_state_filter) = membership_state_filter
|
||||
&& *membership_state_filter != evt_membership
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(not_membership_state_filter) = not_membership_state_filter
|
||||
&& *not_membership_state_filter == evt_membership
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
return Some(pdu);
|
||||
}
|
||||
|
||||
@@ -47,15 +47,14 @@ pub(crate) async fn joined_rooms_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<joined_rooms::v3::Request>,
|
||||
) -> Result<joined_rooms::v3::Response> {
|
||||
Ok(joined_rooms::v3::Response {
|
||||
joined_rooms: services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(body.sender_user())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await,
|
||||
})
|
||||
let joined_rooms = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(body.sender_user())
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(joined_rooms::v3::Response::new(joined_rooms))
|
||||
}
|
||||
|
||||
/// Checks if the room is banned in any way possible and the sender user is not
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
|
||||
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
|
||||
use ruma::{
|
||||
api::client::membership::unban_user,
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
@@ -18,9 +18,9 @@ pub(crate) async fn unban_user_route(
|
||||
if services.users.is_suspended(sender_user).await? {
|
||||
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
||||
}
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
|
||||
|
||||
let current_member_content = services
|
||||
let mut current_member_content = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_member(&body.room_id, &body.user_id)
|
||||
@@ -34,18 +34,17 @@ pub(crate) async fn unban_user_route(
|
||||
)));
|
||||
}
|
||||
|
||||
current_member_content.membership = MembershipState::Leave;
|
||||
current_member_content.reason = body.reason.clone();
|
||||
current_member_content.join_authorized_via_users_server = None;
|
||||
current_member_content.third_party_invite = None;
|
||||
current_member_content.is_direct = None;
|
||||
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
|
||||
membership: MembershipState::Leave,
|
||||
reason: body.reason.clone(),
|
||||
join_authorized_via_users_server: None,
|
||||
third_party_invite: None,
|
||||
is_direct: None,
|
||||
..current_member_content
|
||||
}),
|
||||
PartialPdu::state(body.user_id.to_string(), ¤t_member_content),
|
||||
sender_user,
|
||||
Some(&body.room_id),
|
||||
&state_lock,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result,
|
||||
matrix::pdu::PduBuilder,
|
||||
matrix::pdu::PartialPdu,
|
||||
utils::{IterStream, future::TryExtExt, stream::TryIgnore},
|
||||
warn,
|
||||
};
|
||||
@@ -326,7 +326,7 @@ pub async fn update_displayname(
|
||||
.iter()
|
||||
.try_stream()
|
||||
.and_then(|room_id: &OwnedRoomId| async move {
|
||||
let pdu = PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
let pdu = PartialPdu::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
displayname: displayname.clone(),
|
||||
membership: MembershipState::Join,
|
||||
avatar_url: avatar_url.clone(),
|
||||
@@ -376,7 +376,7 @@ pub async fn update_avatar_url(
|
||||
.iter()
|
||||
.try_stream()
|
||||
.and_then(|room_id: &OwnedRoomId| async move {
|
||||
let pdu = PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
let pdu = PartialPdu::state(user_id.to_string(), &RoomMemberEventContent {
|
||||
avatar_url: avatar_url.clone(),
|
||||
blurhash: blurhash.clone(),
|
||||
membership: MembershipState::Join,
|
||||
@@ -399,15 +399,15 @@ pub async fn update_avatar_url(
|
||||
|
||||
pub async fn update_all_rooms(
|
||||
services: &Services,
|
||||
all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>,
|
||||
all_joined_rooms: Vec<(PartialPdu, &OwnedRoomId)>,
|
||||
user_id: &UserId,
|
||||
) {
|
||||
for (pdu_builder, room_id) in all_joined_rooms {
|
||||
for (partial_pdu, room_id) in all_joined_rooms {
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).await;
|
||||
if let Err(e) = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(pdu_builder, user_id, Some(room_id), &state_lock)
|
||||
.build_and_append_pdu(partial_pdu, user_id, Some(room_id), &state_lock)
|
||||
.await
|
||||
{
|
||||
warn!(%user_id, %room_id, "Failed to update/send new profile join membership update in room: {e}");
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
|
||||
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
|
||||
use ruma::{
|
||||
api::client::redact::redact_event, events::room::redaction::RoomRedactionEventContent,
|
||||
};
|
||||
@@ -34,9 +34,9 @@ pub(crate) async fn redact_event_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
redacts: Some(body.event_id.clone()),
|
||||
..PduBuilder::timeline(&RoomRedactionEventContent {
|
||||
..PartialPdu::timeline(&RoomRedactionEventContent {
|
||||
redacts: Some(body.event_id.clone()),
|
||||
reason: body.reason.clone(),
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use ruma::{
|
||||
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
api::client::{
|
||||
report_user,
|
||||
reporting::report_user,
|
||||
room::{report_content, report_room},
|
||||
},
|
||||
events::{Mentions, room::message::RoomMessageEventContent},
|
||||
|
||||
@@ -26,13 +26,12 @@ pub(crate) async fn get_room_aliases_route(
|
||||
return Err!(Request(Forbidden("You don't have permission to view this room.",)));
|
||||
}
|
||||
|
||||
Ok(aliases::v3::Response {
|
||||
aliases: services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await,
|
||||
})
|
||||
let aliases = services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(aliases::v3::Response::new(aliases))
|
||||
}
|
||||
|
||||
+34
-102
@@ -2,8 +2,8 @@
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result, RoomVersion, debug, debug_info, debug_warn, err, info,
|
||||
matrix::{StateKey, pdu::PduBuilder},
|
||||
Err, Result, debug, debug_info, debug_warn, err, info,
|
||||
matrix::{StateKey, pdu::PartialPdu},
|
||||
trace, warn,
|
||||
};
|
||||
use conduwuit_service::{Services, appservice::RegistrationInfo};
|
||||
@@ -13,7 +13,6 @@
|
||||
api::client::room::{self, create_room},
|
||||
events::{
|
||||
TimelineEventType,
|
||||
invite_permission_config::FilterLevel,
|
||||
room::{
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
create::RoomCreateEventContent,
|
||||
@@ -27,8 +26,10 @@
|
||||
},
|
||||
},
|
||||
int,
|
||||
room_version_rules::RoomIdFormatVersion,
|
||||
serde::{JsonObject, Raw},
|
||||
};
|
||||
use ruminuwuity::invite_permission_config::FilterLevel;
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
|
||||
use crate::{Ruma, client::invite_helper};
|
||||
@@ -81,15 +82,11 @@ pub(crate) async fn create_room_route(
|
||||
},
|
||||
| None => services.server.config.default_room_version.clone(),
|
||||
};
|
||||
let room_features = RoomVersion::new(&room_version)?;
|
||||
let room_version_rules = room_version.rules().unwrap();
|
||||
|
||||
let room_id: Option<OwnedRoomId> = if !room_features.room_ids_as_hashes {
|
||||
match &body.room_id {
|
||||
| Some(custom_room_id) => Some(custom_room_id_check(&services, custom_room_id)?),
|
||||
| None => Some(RoomId::new(services.globals.server_name())),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
let room_id: Option<OwnedRoomId> = match room_version_rules.room_id_format {
|
||||
| RoomIdFormatVersion::V1 => Some(RoomId::new_v1(services.globals.server_name())),
|
||||
| _ => None,
|
||||
};
|
||||
|
||||
// check if room ID doesn't already exist instead of erroring on auth check
|
||||
@@ -167,7 +164,7 @@ pub(crate) async fn create_room_route(
|
||||
use RoomVersionId::*;
|
||||
|
||||
let mut content = content
|
||||
.deserialize_as::<CanonicalJsonObject>()
|
||||
.deserialize_as_unchecked::<CanonicalJsonObject>()
|
||||
.map_err(|e| {
|
||||
err!(Request(BadJson(error!(
|
||||
"Failed to deserialise content as canonical JSON: {e}"
|
||||
@@ -201,8 +198,7 @@ pub(crate) async fn create_room_route(
|
||||
let content = match room_version {
|
||||
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
|
||||
RoomCreateEventContent::new_v1(sender_user.to_owned()),
|
||||
| V11 => RoomCreateEventContent::new_v11(),
|
||||
| _ => RoomCreateEventContent::new_v12(),
|
||||
| _ => RoomCreateEventContent::new_v11(),
|
||||
};
|
||||
let mut content =
|
||||
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
|
||||
@@ -234,7 +230,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&create_content)?,
|
||||
state_key: Some(StateKey::new()),
|
||||
@@ -257,30 +253,23 @@ pub(crate) async fn create_room_route(
|
||||
},
|
||||
};
|
||||
drop(state_lock);
|
||||
if let Some(expected_room_id) = body.room_id.as_ref() {
|
||||
if expected_room_id.as_str() != room_id.as_str() {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Custom room ID {expected_room_id} does not match the generated room ID \
|
||||
{room_id}.",
|
||||
)));
|
||||
}
|
||||
}
|
||||
debug!("Room created with ID {room_id}");
|
||||
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;
|
||||
|
||||
// 2. Let the room creator join
|
||||
|
||||
let mut join_event = RoomMemberEventContent::new(MembershipState::Join);
|
||||
join_event.displayname = services.users.displayname(sender_user).await.ok();
|
||||
join_event.avatar_url = services.users.avatar_url(sender_user).await.ok();
|
||||
join_event.blurhash = services.users.blurhash(sender_user).await.ok();
|
||||
join_event.is_direct = Some(body.is_direct);
|
||||
|
||||
debug_info!("Joining {sender_user} to room {room_id}");
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(sender_user.to_string(), &RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
blurhash: services.users.blurhash(sender_user).await.ok(),
|
||||
is_direct: Some(body.is_direct),
|
||||
..RoomMemberEventContent::new(MembershipState::Join)
|
||||
}),
|
||||
PartialPdu::state(sender_user.to_string(), &join_event),
|
||||
sender_user,
|
||||
Some(&room_id),
|
||||
&state_lock,
|
||||
@@ -306,7 +295,7 @@ pub(crate) async fn create_room_route(
|
||||
|
||||
let mut creators: Vec<OwnedUserId> = vec![sender_user.to_owned()];
|
||||
// Do we care about additional_creators?
|
||||
if room_features.explicitly_privilege_room_creators {
|
||||
if room_version_rules.explicitly_privilege_room_creators {
|
||||
// Have they been specified?
|
||||
if let Some(additional_creators) = create_content.get("additional_creators") {
|
||||
// Are they a real array?
|
||||
@@ -341,7 +330,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
event_type: TimelineEventType::RoomPowerLevels,
|
||||
content: to_raw_value(&power_levels_content)?,
|
||||
state_key: Some(StateKey::new()),
|
||||
@@ -360,7 +349,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(String::new(), &RoomCanonicalAliasEventContent {
|
||||
PartialPdu::state(String::new(), &RoomCanonicalAliasEventContent {
|
||||
alias: Some(room_alias_id.to_owned()),
|
||||
alt_aliases: vec![],
|
||||
}),
|
||||
@@ -379,7 +368,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
PartialPdu::state(
|
||||
String::new(),
|
||||
&RoomJoinRulesEventContent::new(match preset {
|
||||
| RoomPreset::PublicChat => JoinRule::Public,
|
||||
@@ -399,7 +388,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
PartialPdu::state(
|
||||
String::new(),
|
||||
&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared),
|
||||
),
|
||||
@@ -415,7 +404,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(
|
||||
PartialPdu::state(
|
||||
String::new(),
|
||||
&RoomGuestAccessEventContent::new(match preset {
|
||||
| RoomPreset::PublicChat => GuestAccess::Forbidden,
|
||||
@@ -431,7 +420,7 @@ pub(crate) async fn create_room_route(
|
||||
|
||||
// 6. Events listed in initial_state
|
||||
for event in &body.initial_state {
|
||||
let mut pdu_builder = event.deserialize_as::<PduBuilder>().map_err(|e| {
|
||||
let mut partial_pdu = event.deserialize_as::<PartialPdu>().map_err(|e| {
|
||||
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
|
||||
})?;
|
||||
|
||||
@@ -440,17 +429,17 @@ pub(crate) async fn create_room_route(
|
||||
// client/appservice workaround: if a user sends an initial_state event with a
|
||||
// state event in there with the content of literally `{}` (not null or empty
|
||||
// string), let's just skip it over and warn.
|
||||
if pdu_builder.content.get().eq("{}") {
|
||||
if partial_pdu.content.get().eq("{}") {
|
||||
debug_warn!("skipping empty initial state event with content of `{{}}`: {event:?}");
|
||||
debug_warn!("content: {}", pdu_builder.content.get());
|
||||
debug_warn!("content: {}", partial_pdu.content.get());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Implicit state key defaults to ""
|
||||
pdu_builder.state_key.get_or_insert_with(StateKey::new);
|
||||
partial_pdu.state_key.get_or_insert_with(StateKey::new);
|
||||
|
||||
// Silently skip encryption events if they are not allowed
|
||||
if pdu_builder.event_type == TimelineEventType::RoomEncryption
|
||||
if partial_pdu.event_type == TimelineEventType::RoomEncryption
|
||||
&& !services.config.allow_encryption
|
||||
{
|
||||
continue;
|
||||
@@ -459,7 +448,7 @@ pub(crate) async fn create_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(pdu_builder, sender_user, Some(&room_id), &state_lock)
|
||||
.build_and_append_pdu(partial_pdu, sender_user, Some(&room_id), &state_lock)
|
||||
.boxed()
|
||||
.await?;
|
||||
}
|
||||
@@ -470,7 +459,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(String::new(), &RoomNameEventContent::new(name.clone())),
|
||||
PartialPdu::state(String::new(), &RoomNameEventContent::new(name.clone())),
|
||||
sender_user,
|
||||
Some(&room_id),
|
||||
&state_lock,
|
||||
@@ -484,7 +473,7 @@ pub(crate) async fn create_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }),
|
||||
PartialPdu::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }),
|
||||
sender_user,
|
||||
Some(&room_id),
|
||||
&state_lock,
|
||||
@@ -667,60 +656,3 @@ async fn room_alias_check(
|
||||
|
||||
Ok(full_room_alias)
|
||||
}
|
||||
|
||||
/// if a room is being created with a custom room ID, run our checks against it
|
||||
fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
|
||||
// apply forbidden room alias checks to custom room IDs too
|
||||
if services
|
||||
.globals
|
||||
.forbidden_alias_names()
|
||||
.is_match(custom_room_id)
|
||||
{
|
||||
return Err!(Request(Unknown("Custom room ID is forbidden.")));
|
||||
}
|
||||
|
||||
if custom_room_id.contains(':') {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Custom room ID contained `:` which is not allowed. Please note that this expects a \
|
||||
localpart, not the full room ID.",
|
||||
)));
|
||||
} else if custom_room_id.contains(char::is_whitespace) {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Custom room ID contained spaces which is not valid."
|
||||
)));
|
||||
}
|
||||
|
||||
let server_name = services.globals.server_name();
|
||||
let mut room_id = custom_room_id.to_owned();
|
||||
if custom_room_id.contains(':') {
|
||||
if !custom_room_id.starts_with('!') {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Custom room ID contains an unexpected `:` which is not allowed.",
|
||||
)));
|
||||
}
|
||||
} else if custom_room_id.starts_with('!') {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Room ID is prefixed with !, but is not fully qualified. You likely did not want \
|
||||
this.",
|
||||
)));
|
||||
} else {
|
||||
room_id = format!("!{custom_room_id}:{server_name}");
|
||||
}
|
||||
OwnedRoomId::parse(room_id)
|
||||
.map_err(Into::into)
|
||||
.and_then(|full_room_id| {
|
||||
if full_room_id
|
||||
.server_name()
|
||||
.expect("failed to extract server name from room ID")
|
||||
!= server_name
|
||||
{
|
||||
Err!(Request(InvalidParam("Custom room ID must be on this server.",)))
|
||||
} else {
|
||||
Ok(full_room_id)
|
||||
}
|
||||
})
|
||||
.inspect(|full_room_id| {
|
||||
debug_info!(%full_room_id, "Full custom room ID");
|
||||
})
|
||||
.inspect_err(|e| warn!(?e, %custom_room_id, "Failed to create room with custom room ID",))
|
||||
}
|
||||
|
||||
@@ -6,10 +6,7 @@
|
||||
mod upgrade;
|
||||
|
||||
pub(crate) use self::{
|
||||
aliases::get_room_aliases_route,
|
||||
create::create_room_route,
|
||||
event::get_room_event_route,
|
||||
initial_sync::room_initial_sync_route,
|
||||
summary::{get_room_summary, get_room_summary_legacy},
|
||||
aliases::get_room_aliases_route, create::create_room_route, event::get_room_event_route,
|
||||
initial_sync::room_initial_sync_route, summary::get_room_summary,
|
||||
upgrade::upgrade_room_route,
|
||||
};
|
||||
|
||||
+15
-318
@@ -1,48 +1,11 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, debug, debug_warn, info, trace,
|
||||
utils::{IterStream, future::TryExtExt},
|
||||
};
|
||||
use futures::{
|
||||
FutureExt, StreamExt, TryFutureExt,
|
||||
future::{OptionFuture, join3},
|
||||
stream::FuturesUnordered,
|
||||
};
|
||||
use ruma::{
|
||||
OwnedServerName, RoomId, UserId,
|
||||
api::{
|
||||
client::room::get_summary,
|
||||
federation::space::{SpaceHierarchyParentSummary, get_hierarchy},
|
||||
},
|
||||
events::room::member::MembershipState,
|
||||
space::SpaceRoomJoinRule::{self, *},
|
||||
};
|
||||
use service::Services;
|
||||
use conduwuit::{Err, Result};
|
||||
use ruma::api::client::room::get_summary;
|
||||
use service::rooms::summary::Accessibility;
|
||||
|
||||
use crate::{Ruma, RumaResponse};
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_matrix/client/unstable/im.nheko.summary/rooms/{roomIdOrAlias}/summary`
|
||||
///
|
||||
/// Returns a short description of the state of a room.
|
||||
///
|
||||
/// This is the "wrong" endpoint that some implementations/clients may use
|
||||
/// according to the MSC. Request and response bodies are the same as
|
||||
/// `get_room_summary`.
|
||||
///
|
||||
/// An implementation of [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)
|
||||
pub(crate) async fn get_room_summary_legacy(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_summary::msc3266::Request>,
|
||||
) -> Result<RumaResponse<get_summary::msc3266::Response>> {
|
||||
get_room_summary(State(services), InsecureClientIp(client), body)
|
||||
.boxed()
|
||||
.await
|
||||
.map(RumaResponse)
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/unstable/im.nheko.summary/summary/{roomIdOrAlias}`
|
||||
/// # `GET /_matrix/client/v1/room_summary/{roomIdOrAlias}`
|
||||
///
|
||||
/// Returns a short description of the state of a room.
|
||||
@@ -50,8 +13,8 @@ pub(crate) async fn get_room_summary_legacy(
|
||||
pub(crate) async fn get_room_summary(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_summary::msc3266::Request>,
|
||||
) -> Result<get_summary::msc3266::Response> {
|
||||
body: Ruma<get_summary::v1::Request>,
|
||||
) -> Result<get_summary::v1::Response> {
|
||||
let (room_id, servers) = services
|
||||
.rooms
|
||||
.alias
|
||||
@@ -62,285 +25,19 @@ pub(crate) async fn get_room_summary(
|
||||
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
|
||||
}
|
||||
|
||||
room_summary_response(&services, &room_id, &servers, body.sender_user.as_deref())
|
||||
.boxed()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn room_summary_response(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
servers: &[OwnedServerName],
|
||||
sender_user: Option<&UserId>,
|
||||
) -> Result<get_summary::msc3266::Response> {
|
||||
if services
|
||||
let summary = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), room_id)
|
||||
.await
|
||||
{
|
||||
match local_room_summary_response(services, room_id, sender_user)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Ok(response) => return Ok(response),
|
||||
| Err(e) => {
|
||||
debug_warn!("Failed to get local room summary: {e:?}, falling back to remote");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let room =
|
||||
remote_room_summary_hierarchy_response(services, room_id, servers, sender_user).await?;
|
||||
|
||||
Ok(get_summary::msc3266::Response {
|
||||
room_id: room_id.to_owned(),
|
||||
canonical_alias: room.canonical_alias,
|
||||
avatar_url: room.avatar_url,
|
||||
guest_can_join: room.guest_can_join,
|
||||
name: room.name,
|
||||
num_joined_members: room.num_joined_members,
|
||||
topic: room.topic,
|
||||
world_readable: room.world_readable,
|
||||
join_rule: room.join_rule,
|
||||
room_type: room.room_type,
|
||||
room_version: room.room_version,
|
||||
encryption: room.encryption,
|
||||
allowed_room_ids: room.allowed_room_ids,
|
||||
membership: sender_user.is_some().then_some(MembershipState::Leave),
|
||||
})
|
||||
}
|
||||
|
||||
async fn local_room_summary_response(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
sender_user: Option<&UserId>,
|
||||
) -> Result<get_summary::msc3266::Response> {
|
||||
trace!(
|
||||
sender_user = sender_user.map(tracing::field::display),
|
||||
"Sending local room summary response for {room_id:?}"
|
||||
);
|
||||
let (join_rule, world_readable, guest_can_join) = join3(
|
||||
services.rooms.state_accessor.get_join_rules(room_id),
|
||||
services.rooms.state_accessor.is_world_readable(room_id),
|
||||
services.rooms.state_accessor.guest_can_join(room_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Synapse allows server admins to bypass visibility checks.
|
||||
// That seems neat so we'll copy that behaviour.
|
||||
if sender_user.is_none() || !services.users.is_admin(sender_user.unwrap()).await {
|
||||
user_can_see_summary(
|
||||
services,
|
||||
room_id,
|
||||
&join_rule.clone().into(),
|
||||
guest_can_join,
|
||||
world_readable,
|
||||
join_rule.allowed_rooms(),
|
||||
sender_user,
|
||||
)
|
||||
.summary
|
||||
.get_room_summary_for_user(body.sender_user.as_deref(), &room_id, &servers)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let canonical_alias = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_canonical_alias(room_id)
|
||||
.ok();
|
||||
|
||||
let name = services.rooms.state_accessor.get_name(room_id).ok();
|
||||
|
||||
let topic = services.rooms.state_accessor.get_room_topic(room_id).ok();
|
||||
|
||||
let room_type = services.rooms.state_accessor.get_room_type(room_id).ok();
|
||||
|
||||
let avatar_url = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_avatar(room_id)
|
||||
.map(|res| res.into_option().unwrap_or_default().url);
|
||||
|
||||
let room_version = services.rooms.state.get_room_version(room_id).ok();
|
||||
|
||||
let encryption = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_encryption(room_id)
|
||||
.ok();
|
||||
|
||||
let num_joined_members = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(room_id)
|
||||
.unwrap_or(0);
|
||||
|
||||
let membership: OptionFuture<_> = sender_user
|
||||
.map(|sender_user| {
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_member(room_id, sender_user)
|
||||
.map_ok_or(MembershipState::Leave, |content| content.membership)
|
||||
})
|
||||
.into();
|
||||
|
||||
let (
|
||||
canonical_alias,
|
||||
name,
|
||||
num_joined_members,
|
||||
topic,
|
||||
avatar_url,
|
||||
room_type,
|
||||
room_version,
|
||||
encryption,
|
||||
membership,
|
||||
) = futures::join!(
|
||||
canonical_alias,
|
||||
name,
|
||||
num_joined_members,
|
||||
topic,
|
||||
avatar_url,
|
||||
room_type,
|
||||
room_version,
|
||||
encryption,
|
||||
membership,
|
||||
);
|
||||
|
||||
Ok(get_summary::msc3266::Response {
|
||||
room_id: room_id.to_owned(),
|
||||
canonical_alias,
|
||||
avatar_url,
|
||||
guest_can_join,
|
||||
name,
|
||||
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
|
||||
topic,
|
||||
world_readable,
|
||||
room_type,
|
||||
room_version,
|
||||
encryption,
|
||||
membership,
|
||||
allowed_room_ids: join_rule.allowed_rooms().map(Into::into).collect(),
|
||||
join_rule: join_rule.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// used by MSC3266 to fetch a room's info if we do not know about it
|
||||
async fn remote_room_summary_hierarchy_response(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
servers: &[OwnedServerName],
|
||||
sender_user: Option<&UserId>,
|
||||
) -> Result<SpaceHierarchyParentSummary> {
|
||||
trace!(sender_user = ?sender_user.map(tracing::field::display), ?servers, "Sending remote room summary response for {room_id:?}");
|
||||
if !services.config.allow_federation {
|
||||
return Err!(Request(Forbidden("Federation is disabled.")));
|
||||
}
|
||||
|
||||
if services.rooms.metadata.is_disabled(room_id).await {
|
||||
return Err!(Request(Forbidden(
|
||||
"Federaton of room {room_id} is currently disabled on this server."
|
||||
)));
|
||||
}
|
||||
if servers.is_empty() {
|
||||
return Err!(Request(MissingParam(
|
||||
"No servers were provided to fetch the room over federation"
|
||||
)));
|
||||
}
|
||||
|
||||
let request = get_hierarchy::v1::Request::new(room_id.to_owned());
|
||||
|
||||
let mut requests: FuturesUnordered<_> = servers
|
||||
.iter()
|
||||
.map(|server| {
|
||||
info!("Fetching room summary for {room_id} from server {server}");
|
||||
services
|
||||
.sending
|
||||
.send_federation_request(server, request.clone())
|
||||
.inspect_ok(move |v| {
|
||||
debug!("Fetched room summary for {room_id} from server {server}: {v:?}");
|
||||
})
|
||||
.inspect_err(move |e| {
|
||||
info!("Failed to fetch room summary for {room_id} from server {server}: {e}");
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
while let Some(Ok(response)) = requests.next().await {
|
||||
trace!("{response:?}");
|
||||
let room = response.room.clone();
|
||||
if room.room_id != room_id {
|
||||
debug_warn!(
|
||||
"Room ID {} returned does not belong to the requested room ID {}",
|
||||
room.room_id,
|
||||
room_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if sender_user.is_none() || !services.users.is_admin(sender_user.unwrap()).await {
|
||||
return user_can_see_summary(
|
||||
services,
|
||||
room_id,
|
||||
&room.join_rule,
|
||||
room.guest_can_join,
|
||||
room.world_readable,
|
||||
room.allowed_room_ids.iter().map(AsRef::as_ref),
|
||||
sender_user,
|
||||
)
|
||||
.await
|
||||
.map(|()| room);
|
||||
}
|
||||
return Ok(room);
|
||||
}
|
||||
|
||||
Err!(Request(NotFound("Room not found or is not accessible")))
|
||||
}
|
||||
|
||||
async fn user_can_see_summary<'a, I>(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
join_rule: &SpaceRoomJoinRule,
|
||||
guest_can_join: bool,
|
||||
world_readable: bool,
|
||||
allowed_room_ids: I,
|
||||
sender_user: Option<&UserId>,
|
||||
) -> Result
|
||||
where
|
||||
I: Iterator<Item = &'a RoomId> + Send,
|
||||
{
|
||||
let is_public_room = matches!(join_rule, Public | Knock | KnockRestricted);
|
||||
match sender_user {
|
||||
| Some(sender_user) => {
|
||||
let user_can_see_state_events = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, room_id);
|
||||
let is_guest = services.users.is_deactivated(sender_user).unwrap_or(false);
|
||||
let user_in_allowed_restricted_room = allowed_room_ids
|
||||
.stream()
|
||||
.any(|room| services.rooms.state_cache.is_joined(sender_user, room));
|
||||
|
||||
let (user_can_see_state_events, is_guest, user_in_allowed_restricted_room) =
|
||||
join3(user_can_see_state_events, is_guest, user_in_allowed_restricted_room)
|
||||
.boxed()
|
||||
.await;
|
||||
|
||||
if user_can_see_state_events
|
||||
|| (is_guest && guest_can_join)
|
||||
|| is_public_room
|
||||
|| user_in_allowed_restricted_room
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err!(Request(Forbidden("Room is not accessible")))
|
||||
match summary {
|
||||
| Accessibility::Accessible(summary) => Ok(get_summary::v1::Response::new(summary)),
|
||||
| Accessibility::Inaccessible => {
|
||||
Err!(Request(Forbidden("You may not preview this room."), FORBIDDEN))
|
||||
},
|
||||
| None => {
|
||||
if is_public_room || world_readable {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err!(Request(Forbidden("Room is not accessible")))
|
||||
| Accessibility::NotFound => {
|
||||
Err!(Request(Forbidden("This room does not exist."), FORBIDDEN))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Error, Event, Result, RoomVersion, debug, err, info,
|
||||
matrix::{StateKey, pdu::PduBuilder},
|
||||
matrix::{StateKey, pdu::PartialPdu},
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::{
|
||||
@@ -85,7 +85,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.create_hash_and_sign_event(
|
||||
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
PartialPdu::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: RoomId::new(services.globals.server_name()),
|
||||
}),
|
||||
@@ -128,7 +128,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
let tombstone_event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: replacement_room.unwrap().to_owned(),
|
||||
@@ -209,7 +209,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
let create_event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&create_event_content)
|
||||
@@ -237,7 +237,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder {
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
@@ -304,7 +304,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder {
|
||||
event_type: event_type.to_string().into(),
|
||||
content: event_content,
|
||||
@@ -364,7 +364,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder::state(StateKey::new(), &RoomPowerLevelsEventContent {
|
||||
events_default: new_level,
|
||||
invite: new_level,
|
||||
@@ -387,7 +387,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: replacement_room.unwrap().to_owned(),
|
||||
@@ -434,7 +434,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder {
|
||||
event_type: StateEventType::SpaceChild.into(),
|
||||
content: to_raw_value(&RedactedSpaceChildEventContent {})
|
||||
@@ -457,7 +457,7 @@ pub(crate) async fn upgrade_room_route(
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
.PartialPduappend_pdu(
|
||||
PduBuilder {
|
||||
event_type: StateEventType::SpaceChild.into(),
|
||||
content: to_raw_value(&SpaceChildEventContent {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{Err, Result, err, matrix::pdu::PduBuilder, utils};
|
||||
use conduwuit::{Err, Result, err, matrix::pdu::PartialPdu, utils};
|
||||
use ruma::{api::client::message::send_message_event, events::MessageLikeEventType};
|
||||
use serde_json::from_str;
|
||||
|
||||
@@ -79,7 +79,7 @@ pub(crate) async fn send_message_event_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
event_type: body.event_type.clone().into(),
|
||||
content,
|
||||
unsigned: Some(unsigned),
|
||||
|
||||
+69
-62
@@ -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())
|
||||
|
||||
+28
-176
@@ -1,26 +1,12 @@
|
||||
use std::{
|
||||
collections::{BTreeSet, VecDeque},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result,
|
||||
utils::{future::TryExtExt, stream::IterStream},
|
||||
};
|
||||
use conduwuit_service::{
|
||||
Services,
|
||||
rooms::spaces::{
|
||||
PaginationToken, SummaryAccessibility, get_parent_children_via, summary_to_chunk,
|
||||
},
|
||||
};
|
||||
use futures::{StreamExt, TryFutureExt, future::OptionFuture};
|
||||
use ruma::{
|
||||
OwnedRoomId, OwnedServerName, RoomId, UInt, UserId, api::client::space::get_hierarchy,
|
||||
};
|
||||
use conduwuit::{Err, Result};
|
||||
use ruma::{UInt, api::client::space::get_hierarchy, assign};
|
||||
use service::rooms::summary::Accessibility;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
const MAX_MAX_DEPTH: u32 = 10;
|
||||
|
||||
/// # `GET /_matrix/client/v1/rooms/{room_id}/hierarchy`
|
||||
///
|
||||
/// Paginates over the space tree in a depth-first manner to locate child rooms
|
||||
@@ -29,167 +15,33 @@ pub(crate) async fn get_hierarchy_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_hierarchy::v1::Request>,
|
||||
) -> Result<get_hierarchy::v1::Response> {
|
||||
let limit = body
|
||||
.limit
|
||||
.unwrap_or_else(|| UInt::from(10_u32))
|
||||
.min(UInt::from(100_u32));
|
||||
// We don't do pagination for this route (and therefore ignore `limit`), since
|
||||
// there's no reasonable way to handle a space hierarchy changing during
|
||||
// pagination.
|
||||
|
||||
let max_depth = body
|
||||
.max_depth
|
||||
.unwrap_or_else(|| UInt::from(3_u32))
|
||||
.min(UInt::from(10_u32));
|
||||
.map(|max_depth| max_depth.min(UInt::from(MAX_MAX_DEPTH)));
|
||||
|
||||
let key = body
|
||||
.from
|
||||
.as_ref()
|
||||
.and_then(|s| PaginationToken::from_str(s).ok());
|
||||
let hierarchy = services
|
||||
.rooms
|
||||
.summary
|
||||
.get_room_hierarchy_for_user(
|
||||
body.sender_user(),
|
||||
body.room_id.clone(),
|
||||
max_depth,
|
||||
body.suggested_only,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Should prevent unexpected behaviour in (bad) clients
|
||||
if let Some(ref token) = key {
|
||||
if token.suggested_only != body.suggested_only || token.max_depth != max_depth {
|
||||
return Err!(Request(InvalidParam(
|
||||
"suggested_only and max_depth cannot change on paginated requests"
|
||||
)));
|
||||
}
|
||||
match hierarchy {
|
||||
| Accessibility::Accessible(rooms) =>
|
||||
Ok(assign!(get_hierarchy::v1::Response::new(), { rooms: rooms })),
|
||||
| Accessibility::Inaccessible => {
|
||||
Err!(Request(Forbidden("You may not preview this room."), FORBIDDEN))
|
||||
},
|
||||
| Accessibility::NotFound => {
|
||||
Err!(Request(Forbidden("This room does not exist."), FORBIDDEN))
|
||||
},
|
||||
}
|
||||
|
||||
get_client_hierarchy(
|
||||
&services,
|
||||
body.sender_user(),
|
||||
&body.room_id,
|
||||
limit.try_into().unwrap_or(10),
|
||||
max_depth.try_into().unwrap_or(usize::MAX),
|
||||
body.suggested_only,
|
||||
key.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|t| t.short_room_ids.iter()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_client_hierarchy<'a, ShortRoomIds>(
|
||||
services: &Services,
|
||||
sender_user: &UserId,
|
||||
room_id: &RoomId,
|
||||
limit: usize,
|
||||
max_depth: usize,
|
||||
suggested_only: bool,
|
||||
short_room_ids: ShortRoomIds,
|
||||
) -> Result<get_hierarchy::v1::Response>
|
||||
where
|
||||
ShortRoomIds: Iterator<Item = &'a u64> + Clone + Send + Sync + 'a,
|
||||
{
|
||||
type Via = Vec<OwnedServerName>;
|
||||
type Entry = (OwnedRoomId, Via);
|
||||
type Rooms = VecDeque<Entry>;
|
||||
|
||||
let mut queue: Rooms = [(
|
||||
room_id.to_owned(),
|
||||
room_id
|
||||
.server_name()
|
||||
.map(ToOwned::to_owned)
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)]
|
||||
.into();
|
||||
|
||||
let mut rooms = Vec::with_capacity(limit);
|
||||
let mut parents = BTreeSet::new();
|
||||
while let Some((current_room, via)) = queue.pop_front() {
|
||||
let summary = services
|
||||
.rooms
|
||||
.spaces
|
||||
.get_summary_and_children_client(¤t_room, suggested_only, sender_user, &via)
|
||||
.await?;
|
||||
|
||||
match (summary, current_room == room_id) {
|
||||
| (None | Some(SummaryAccessibility::Inaccessible), false) => {
|
||||
// Just ignore other unavailable rooms
|
||||
},
|
||||
| (None, true) => {
|
||||
return Err!(Request(Forbidden("The requested room was not found")));
|
||||
},
|
||||
| (Some(SummaryAccessibility::Inaccessible), true) => {
|
||||
return Err!(Request(Forbidden("The requested room is inaccessible")));
|
||||
},
|
||||
| (Some(SummaryAccessibility::Accessible(summary)), _) => {
|
||||
let populate = parents.len() >= short_room_ids.clone().count();
|
||||
|
||||
let mut children: Vec<Entry> = get_parent_children_via(&summary, suggested_only)
|
||||
.filter(|(room, _)| !parents.contains(room))
|
||||
.rev()
|
||||
.map(|(key, val)| (key, val.collect()))
|
||||
.collect();
|
||||
|
||||
if populate {
|
||||
rooms.push(summary_to_chunk(summary.clone()));
|
||||
} else {
|
||||
children = children
|
||||
.iter()
|
||||
.rev()
|
||||
.stream()
|
||||
.skip_while(|(room, _)| {
|
||||
services
|
||||
.rooms
|
||||
.short
|
||||
.get_shortroomid(room)
|
||||
.map_ok(|short| {
|
||||
Some(&short) != short_room_ids.clone().nth(parents.len())
|
||||
})
|
||||
.unwrap_or_else(|_| false)
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect::<Vec<Entry>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect();
|
||||
}
|
||||
|
||||
if !populate && queue.is_empty() && children.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
parents.insert(current_room.clone());
|
||||
if rooms.len() >= limit {
|
||||
break;
|
||||
}
|
||||
|
||||
if parents.len() > max_depth {
|
||||
continue;
|
||||
}
|
||||
|
||||
queue.extend(children);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let next_batch: OptionFuture<_> = queue
|
||||
.pop_front()
|
||||
.map(|(room, _)| async move {
|
||||
parents.insert(room);
|
||||
|
||||
let next_short_room_ids: Vec<_> = parents
|
||||
.iter()
|
||||
.stream()
|
||||
.filter_map(|room_id| services.rooms.short.get_shortroomid(room_id).ok())
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
(next_short_room_ids.iter().ne(short_room_ids) && !next_short_room_ids.is_empty())
|
||||
.then_some(PaginationToken {
|
||||
short_room_ids: next_short_room_ids,
|
||||
limit: limit.try_into().ok()?,
|
||||
max_depth: max_depth.try_into().ok()?,
|
||||
suggested_only,
|
||||
})
|
||||
.as_ref()
|
||||
.map(PaginationToken::to_string)
|
||||
})
|
||||
.into();
|
||||
|
||||
Ok(get_hierarchy::v1::Response {
|
||||
next_batch: next_batch.await.flatten(),
|
||||
rooms,
|
||||
})
|
||||
}
|
||||
|
||||
+19
-12
@@ -1,8 +1,10 @@
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, err,
|
||||
matrix::{Event, pdu::PduBuilder},
|
||||
matrix::{Event, pdu::PartialPdu},
|
||||
utils::BoolExt,
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
@@ -194,13 +196,14 @@ async fn send_state_event_for_key_helper(
|
||||
state_key: &str,
|
||||
timestamp: Option<MilliSecondsSinceUnixEpoch>,
|
||||
) -> Result<OwnedEventId> {
|
||||
let json: &mut Raw<AnyStateEventContent> = &mut json.clone();
|
||||
allowed_to_send_state_event(services, room_id, event_type, state_key, json).await?;
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).await;
|
||||
let event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
PartialPdu {
|
||||
event_type: event_type.to_string().into(),
|
||||
content: serde_json::from_str(json.json().get())?,
|
||||
state_key: Some(state_key.into()),
|
||||
@@ -221,7 +224,7 @@ async fn allowed_to_send_state_event(
|
||||
room_id: &RoomId,
|
||||
event_type: &StateEventType,
|
||||
state_key: &str,
|
||||
json: &Raw<AnyStateEventContent>,
|
||||
json: &mut Raw<AnyStateEventContent>,
|
||||
) -> Result {
|
||||
match event_type {
|
||||
| StateEventType::RoomCreate => {
|
||||
@@ -366,7 +369,7 @@ async fn allowed_to_send_state_event(
|
||||
}
|
||||
},
|
||||
| StateEventType::RoomMember => match json.deserialize_as::<RoomMemberEventContent>() {
|
||||
| Ok(membership_content) => {
|
||||
| Ok(mut membership_content) => {
|
||||
let Ok(state_key) = UserId::parse(state_key) else {
|
||||
return Err!(Request(BadJson(
|
||||
"Membership event has invalid or non-existent state key"
|
||||
@@ -376,20 +379,24 @@ async fn allowed_to_send_state_event(
|
||||
if let Some(authorising_user) =
|
||||
membership_content.join_authorized_via_users_server
|
||||
{
|
||||
if membership_content.membership != MembershipState::Join {
|
||||
return Err!(Request(BadJson(
|
||||
"join_authorised_via_users_server is only for member joins"
|
||||
)));
|
||||
}
|
||||
|
||||
// join_authorized_via_users_server must be thrown away, if user is already a
|
||||
// member of the room.
|
||||
if services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(state_key, room_id)
|
||||
.await
|
||||
{
|
||||
return Err!(Request(InvalidParam(
|
||||
"{state_key} is already joined, an authorising user is not required."
|
||||
membership_content.join_authorized_via_users_server = None;
|
||||
*json = Raw::<AnyStateEventContent>::from_json_string(
|
||||
serde_json::to_string(&membership_content)?,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if membership_content.membership != MembershipState::Join {
|
||||
return Err!(Request(BadJson(
|
||||
"join_authorised_via_users_server is only for member joins"
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_strip_room_member() -> Result<()> {
|
||||
//Test setup
|
||||
let body = r#"
|
||||
{
|
||||
"avatar_url": "Something",
|
||||
"displayname": "Someone",
|
||||
"join_authorized_via_users_server": "@someone:domain.tld",
|
||||
"membership": "join"
|
||||
}"#;
|
||||
println!("JSON (original): {body}");
|
||||
let json: &mut Raw<AnyStateEventContent> =
|
||||
&mut Raw::<AnyStateEventContent>::from_json_string(body.to_owned())?;
|
||||
let mut membership_content: RoomMemberEventContent =
|
||||
json.deserialize_as::<RoomMemberEventContent>()?;
|
||||
|
||||
//Begin Test
|
||||
membership_content.join_authorized_via_users_server = None;
|
||||
*json = Raw::<AnyStateEventContent>::from_json_string(serde_json::to_string(
|
||||
&membership_content,
|
||||
)?)?;
|
||||
|
||||
//Compare result
|
||||
let result = json.json().get();
|
||||
println!("JSON (modified): {result}");
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"avatar_url":"Something","displayname":"Someone","membership":"join"}"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -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)
|
||||
@@ -180,18 +184,14 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.ruma_route(&client::get_hierarchy_route)
|
||||
.ruma_route(&client::get_mutual_rooms_route)
|
||||
.ruma_route(&client::get_room_summary)
|
||||
.route(
|
||||
"/_matrix/client/unstable/im.nheko.summary/rooms/{room_id_or_alias}/summary",
|
||||
get(client::get_room_summary_legacy)
|
||||
)
|
||||
.ruma_route(&client::get_suspended_status)
|
||||
.ruma_route(&client::put_suspended_status)
|
||||
.ruma_route(&client::well_known_support)
|
||||
.ruma_route(&client::well_known_client)
|
||||
.ruma_route(&client::get_rtc_transports)
|
||||
.ruma_route(&client::room_initial_sync_route)
|
||||
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
||||
.ruma_route(&client::room_initial_sync_route)
|
||||
.route("/client/server.json", get(client::syncv3_client_server_json))
|
||||
.ruma_route(&admin::rooms::ban::ban_room)
|
||||
.ruma_route(&admin::rooms::list::list_rooms);
|
||||
|
||||
+6
-39
@@ -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()
|
||||
|
||||
@@ -44,15 +44,16 @@ impl<Err, Req, Fut, Fun, $($tx,)*> RumaHandler<($($tx,)* Ruma<Req>,)> for Fun
|
||||
$( $tx: FromRequestParts<State> + Send + Sync + 'static, )*
|
||||
{
|
||||
fn add_routes(&'static self, router: Router<State>) -> Router<State> {
|
||||
Req::METADATA
|
||||
.history
|
||||
use ruma::api::path_builder::PathBuilder;
|
||||
|
||||
Req::PATH_BUILDER
|
||||
.all_paths()
|
||||
.fold(router, |router, path| self.add_route(router, path))
|
||||
}
|
||||
|
||||
fn add_route(&'static self, router: Router<State>, path: &str) -> Router<State> {
|
||||
let action = |$($tx,)* req| self($($tx,)* req).map_ok(RumaResponse);
|
||||
let method = method_to_filter(&Req::METADATA.method);
|
||||
let method = method_to_filter(&Req::METHOD);
|
||||
router.route(path, on(method, action))
|
||||
}
|
||||
}
|
||||
|
||||
+10
-57
@@ -1,13 +1,7 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result, info,
|
||||
utils::stream::{BroadbandExt, IterStream},
|
||||
};
|
||||
use conduwuit_service::rooms::spaces::{
|
||||
Identifier, SummaryAccessibility, get_parent_children_via,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use conduwuit::{Err, Result, info};
|
||||
use ruma::api::federation::space::get_hierarchy;
|
||||
use service::rooms::summary::Accessibility;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -19,10 +13,6 @@ pub(crate) async fn get_hierarchy_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_hierarchy::v1::Request>,
|
||||
) -> Result<get_hierarchy::v1::Response> {
|
||||
if !services.rooms.metadata.exists(&body.room_id).await {
|
||||
return Err!(Request(NotFound("Room does not exist.")));
|
||||
}
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
@@ -36,52 +26,15 @@ pub(crate) async fn get_hierarchy_route(
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
let room_id = &body.room_id;
|
||||
let suggested_only = body.suggested_only;
|
||||
let ref identifier = Identifier::ServerName(body.origin());
|
||||
match services
|
||||
let response = services
|
||||
.rooms
|
||||
.spaces
|
||||
.get_summary_and_children_local(room_id, identifier)
|
||||
.await?
|
||||
{
|
||||
| None => Err!(Request(NotFound("The requested room was not found"))),
|
||||
.summary
|
||||
.get_local_room_summary_for_server(body.origin(), &body.room_id, body.suggested_only)
|
||||
.await;
|
||||
|
||||
| Some(SummaryAccessibility::Inaccessible) => {
|
||||
Err!(Request(NotFound("The requested room is inaccessible")))
|
||||
},
|
||||
|
||||
| Some(SummaryAccessibility::Accessible(room)) => {
|
||||
let (children, inaccessible_children) =
|
||||
get_parent_children_via(&room, suggested_only)
|
||||
.stream()
|
||||
.broad_filter_map(|(child, _via)| async move {
|
||||
match services
|
||||
.rooms
|
||||
.spaces
|
||||
.get_summary_and_children_local(&child, identifier)
|
||||
.await
|
||||
.ok()?
|
||||
{
|
||||
| None => None,
|
||||
|
||||
| Some(SummaryAccessibility::Inaccessible) =>
|
||||
Some((None, Some(child))),
|
||||
|
||||
| Some(SummaryAccessibility::Accessible(summary)) =>
|
||||
Some((Some(summary), None)),
|
||||
}
|
||||
})
|
||||
.unzip()
|
||||
.map(|(children, inaccessible_children): (Vec<_>, Vec<_>)| {
|
||||
(
|
||||
children.into_iter().flatten().map(Into::into).collect(),
|
||||
inaccessible_children.into_iter().flatten().collect(),
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(get_hierarchy::v1::Response { room, children, inaccessible_children })
|
||||
},
|
||||
if let Accessibility::Accessible(response) = response {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err!(Request(NotFound("This room is not accessible.")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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::PartialPdu, warn};
|
||||
use conduwuit_service::Services;
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
@@ -137,7 +137,7 @@ pub(crate) async fn create_join_event_template_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.create_hash_and_sign_event(
|
||||
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
|
||||
PartialPdu::state(body.user_id.to_string(), &RoomMemberEventContent {
|
||||
join_authorized_via_users_server,
|
||||
..RoomMemberEventContent::new(MembershipState::Join)
|
||||
}),
|
||||
@@ -172,7 +172,7 @@ pub(crate) async fn select_authorising_user(
|
||||
.state_accessor
|
||||
.user_can_invite(room_id, user, user_id, state_lock)
|
||||
})
|
||||
.boxed()
|
||||
.boPartialPdu
|
||||
.next()
|
||||
.await
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
@@ -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::PartialPdu, warn};
|
||||
use ruma::{
|
||||
RoomVersionId,
|
||||
api::{client::error::ErrorKind, federation::knock::create_knock_event_template},
|
||||
@@ -102,7 +102,7 @@ pub(crate) async fn create_knock_event_template_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.create_hash_and_sign_event(
|
||||
PduBuilder::state(
|
||||
PartialPdu::state(
|
||||
body.user_id.to_string(),
|
||||
&RoomMemberEventContent::new(MembershipState::Knock),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, info, matrix::pdu::PduBuilder};
|
||||
use conduwuit::{Err, Result, info, matrix::pdu::PartialPdu};
|
||||
use ruma::{
|
||||
api::federation::membership::prepare_leave_event,
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
@@ -53,7 +53,7 @@ pub(crate) async fn create_leave_event_template_route(
|
||||
.rooms
|
||||
.timeline
|
||||
.create_hash_and_sign_event(
|
||||
PduBuilder::state(
|
||||
PartialPdu::state(
|
||||
body.user_id.to_string(),
|
||||
&RoomMemberEventContent::new(MembershipState::Leave),
|
||||
),
|
||||
|
||||
@@ -84,12 +84,14 @@ 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"] }
|
||||
regex.workspace = true
|
||||
reqwest.workspace = true
|
||||
ring.workspace = true
|
||||
assign.workspace = true
|
||||
ruma.workspace = true
|
||||
sanitize-filename.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -114,6 +116,7 @@ tracing.workspace = true
|
||||
url.workspace = true
|
||||
parking_lot.workspace = true
|
||||
lock_api.workspace = true
|
||||
hyper-util.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix.workspace = true
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
};
|
||||
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,
|
||||
@@ -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
|
||||
///
|
||||
@@ -756,6 +761,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 +2191,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 +2452,52 @@ 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,
|
||||
}
|
||||
|
||||
const DEPRECATED_KEYS: &[&str] = &[
|
||||
"cache_capacity",
|
||||
"conduit_cache_capacity_modifier",
|
||||
|
||||
+12
-4
@@ -46,7 +46,7 @@ macro_rules! err {
|
||||
(Request(Forbidden($level:ident!($($args:tt)+)))) => {{
|
||||
let mut buf = String::new();
|
||||
$crate::error::Error::Request(
|
||||
$crate::ruma::api::client::error::ErrorKind::forbidden(),
|
||||
$crate::ruma::api::error::ErrorKind::Forbidden,
|
||||
$crate::err_log!(buf, $level, $($args)+),
|
||||
$crate::http::StatusCode::BAD_REQUEST
|
||||
)
|
||||
@@ -54,7 +54,7 @@ macro_rules! err {
|
||||
|
||||
(Request(Forbidden($($args:tt)+))) => {
|
||||
$crate::error::Error::Request(
|
||||
$crate::ruma::api::client::error::ErrorKind::forbidden(),
|
||||
$crate::ruma::api::error::ErrorKind::Forbidden,
|
||||
$crate::format_maybe!($($args)+),
|
||||
$crate::http::StatusCode::BAD_REQUEST
|
||||
)
|
||||
@@ -63,7 +63,7 @@ macro_rules! err {
|
||||
(Request($variant:ident($level:ident!($($args:tt)+)))) => {{
|
||||
let mut buf = String::new();
|
||||
$crate::error::Error::Request(
|
||||
$crate::ruma::api::client::error::ErrorKind::$variant,
|
||||
$crate::ruma::api::error::ErrorKind::$variant,
|
||||
$crate::err_log!(buf, $level, $($args)+),
|
||||
$crate::http::StatusCode::BAD_REQUEST
|
||||
)
|
||||
@@ -71,12 +71,20 @@ macro_rules! err {
|
||||
|
||||
(Request($variant:ident($($args:tt)+))) => {
|
||||
$crate::error::Error::Request(
|
||||
$crate::ruma::api::client::error::ErrorKind::$variant,
|
||||
$crate::ruma::api::error::ErrorKind::$variant,
|
||||
$crate::format_maybe!($($args)+),
|
||||
$crate::http::StatusCode::BAD_REQUEST
|
||||
)
|
||||
};
|
||||
|
||||
(Request($variant:ident($($args:tt)+), $status_code:ident)) => {
|
||||
$crate::error::Error::Request(
|
||||
$crate::ruma::api::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)+))
|
||||
|
||||
+53
-17
@@ -4,7 +4,7 @@
|
||||
mod response;
|
||||
mod serde;
|
||||
|
||||
use std::{any::Any, borrow::Cow, convert::Infallible, sync::PoisonError};
|
||||
use std::{any::Any, borrow::Cow, convert::Infallible, error::Error as _, sync::PoisonError};
|
||||
|
||||
pub use self::{err::visit, log::*};
|
||||
|
||||
@@ -57,17 +57,13 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
JsParseInt(#[from] ruma::JsParseIntError), // js_int re-export
|
||||
#[error(transparent)]
|
||||
JsTryFromInt(#[from] ruma::JsTryFromIntError), // js_int re-export
|
||||
#[error(transparent)]
|
||||
Path(#[from] axum::extract::rejection::PathRejection),
|
||||
#[error("Mutex poisoned: {0}")]
|
||||
Poison(Cow<'static, str>),
|
||||
#[error("Regex error: {0}")]
|
||||
Regex(#[from] regex::Error),
|
||||
#[error("Request error: {0}")]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
#[error("{0}")]
|
||||
Reqwest(FormattedReqwestError),
|
||||
#[error("{0}")]
|
||||
SerdeDe(Cow<'static, str>),
|
||||
#[error("{0}")]
|
||||
@@ -90,8 +86,8 @@ pub enum Error {
|
||||
// ruma/conduwuit
|
||||
#[error("Arithmetic operation failed: {0}")]
|
||||
Arithmetic(Cow<'static, str>),
|
||||
#[error("{0}: {1}")]
|
||||
BadRequest(ruma::api::client::error::ErrorKind, &'static str), //TODO: remove
|
||||
#[error("{0:?}: {1}")]
|
||||
BadRequest(ruma::api::error::ErrorKind, &'static str), //TODO: remove
|
||||
#[error("{0}")]
|
||||
BadServerResponse(Cow<'static, str>),
|
||||
#[error(transparent)]
|
||||
@@ -107,7 +103,7 @@ pub enum Error {
|
||||
#[error("Feature '{0}' is not available on this server.")]
|
||||
FeatureDisabled(Cow<'static, str>),
|
||||
#[error("Remote server {0} responded with: {1}")]
|
||||
Federation(ruma::OwnedServerName, ruma::api::client::error::Error),
|
||||
Federation(ruma::OwnedServerName, ruma::api::error::Error),
|
||||
#[error("{0} in {1}")]
|
||||
InconsistentRoomState(&'static str, ruma::OwnedRoomId),
|
||||
#[error(transparent)]
|
||||
@@ -120,12 +116,14 @@ pub enum Error {
|
||||
Mxid(#[from] ruma::IdParseError),
|
||||
#[error("from {0}: {1}")]
|
||||
Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError),
|
||||
#[error("{0}: {1}")]
|
||||
Request(ruma::api::client::error::ErrorKind, Cow<'static, str>, http::StatusCode),
|
||||
#[error("{0:?}: {1}")]
|
||||
Request(ruma::api::error::ErrorKind, Cow<'static, str>, http::StatusCode),
|
||||
#[error(transparent)]
|
||||
Ruma(#[from] ruma::api::client::error::Error),
|
||||
Ruma(#[from] ruma::api::error::Error),
|
||||
#[error(transparent)]
|
||||
Signatures(#[from] ruma::signatures::Error),
|
||||
SignatureJson(#[from] ruma::signatures::JsonError),
|
||||
#[error(transparent)]
|
||||
SignatureVerification(#[from] ruma::signatures::VerificationError),
|
||||
#[error(transparent)]
|
||||
StateRes(#[from] crate::state_res::Error),
|
||||
#[error("uiaa")]
|
||||
@@ -166,14 +164,14 @@ pub fn message(&self) -> String {
|
||||
|
||||
/// Returns the Matrix error code / error kind
|
||||
#[inline]
|
||||
pub fn kind(&self) -> ruma::api::client::error::ErrorKind {
|
||||
use ruma::api::client::error::ErrorKind::{FeatureDisabled, Unknown};
|
||||
pub fn kind(&self) -> ruma::api::error::ErrorKind {
|
||||
use ruma::api::error::ErrorKind::{Unknown, Unrecognized};
|
||||
|
||||
match self {
|
||||
| Self::Federation(_, error) | Self::Ruma(error) =>
|
||||
response::ruma_error_kind(error).clone(),
|
||||
| Self::BadRequest(kind, ..) | Self::Request(kind, ..) => kind.clone(),
|
||||
| Self::FeatureDisabled(..) => FeatureDisabled,
|
||||
| Self::FeatureDisabled(..) => Unrecognized,
|
||||
| _ => Unknown,
|
||||
}
|
||||
}
|
||||
@@ -236,3 +234,41 @@ pub fn infallible(_e: &Infallible) {
|
||||
#[must_use]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn sanitized_message(e: Error) -> String { e.sanitized_message() }
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FormattedReqwestError(reqwest::Error);
|
||||
|
||||
impl std::ops::Deref for FormattedReqwestError {
|
||||
type Target = reqwest::Error;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.0 }
|
||||
}
|
||||
|
||||
impl std::error::Error for FormattedReqwestError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FormattedReqwestError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(hyper_error) = self.0.source()
|
||||
&& hyper_error.is::<hyper_util::client::legacy::Error>()
|
||||
&& let Some(real_error) = hyper_error.source()
|
||||
{
|
||||
if let Some(real_reason) = real_error.source() {
|
||||
write!(f, "{real_error}: {real_reason}")
|
||||
} else {
|
||||
write!(f, "{real_error}")
|
||||
}
|
||||
} else {
|
||||
write!(f, "Request error: {}", &self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for FormattedReqwestError {
|
||||
fn from(err: reqwest::Error) -> Self { Self(err) }
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(err: reqwest::Error) -> Self { Self::Reqwest(err.into()) }
|
||||
}
|
||||
|
||||
+12
-23
@@ -3,10 +3,8 @@
|
||||
use http_body_util::Full;
|
||||
use ruma::api::{
|
||||
OutgoingResponse,
|
||||
client::{
|
||||
error::{ErrorBody, ErrorKind},
|
||||
uiaa::UiaaResponse,
|
||||
},
|
||||
client::uiaa::UiaaResponse,
|
||||
error::{ErrorBody, ErrorKind, StandardErrorBody},
|
||||
};
|
||||
|
||||
use super::Error;
|
||||
@@ -51,15 +49,9 @@ fn from(error: Error) -> Self {
|
||||
return Self::AuthResponse(uiaainfo);
|
||||
}
|
||||
|
||||
let body = ErrorBody::Standard {
|
||||
kind: error.kind(),
|
||||
message: error.message(),
|
||||
};
|
||||
let body = ErrorBody::Standard(StandardErrorBody::new(error.kind(), error.message()));
|
||||
|
||||
Self::MatrixError(ruma::api::client::error::Error {
|
||||
status_code: error.status_code(),
|
||||
body,
|
||||
})
|
||||
Self::MatrixError(ruma::api::error::Error::new(error.status_code(), body))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +68,7 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
|
||||
|
||||
match kind {
|
||||
// 429
|
||||
| LimitExceeded { .. } => StatusCode::TOO_MANY_REQUESTS,
|
||||
| LimitExceeded(_) => StatusCode::TOO_MANY_REQUESTS,
|
||||
|
||||
// 413
|
||||
| TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
|
||||
@@ -85,37 +77,34 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
|
||||
| Unrecognized => StatusCode::METHOD_NOT_ALLOWED,
|
||||
|
||||
// 404
|
||||
| NotFound | NotImplemented | FeatureDisabled | SenderIgnored { .. } =>
|
||||
StatusCode::NOT_FOUND,
|
||||
| NotFound => StatusCode::NOT_FOUND,
|
||||
|
||||
// 403
|
||||
| GuestAccessForbidden
|
||||
| ThreepidAuthFailed
|
||||
| UserDeactivated
|
||||
| ThreepidDenied
|
||||
| InviteBlocked
|
||||
| WrongRoomKeysVersion { .. }
|
||||
| WrongRoomKeysVersion(_)
|
||||
| UserSuspended
|
||||
| Forbidden { .. } => StatusCode::FORBIDDEN,
|
||||
| Forbidden => StatusCode::FORBIDDEN,
|
||||
|
||||
// 401
|
||||
| UnknownToken { .. } | MissingToken | Unauthorized | UserLocked =>
|
||||
StatusCode::UNAUTHORIZED,
|
||||
| UnknownToken(_) | MissingToken | Unauthorized | UserLocked => StatusCode::UNAUTHORIZED,
|
||||
|
||||
// 400
|
||||
| _ => StatusCode::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn ruma_error_message(error: &ruma::api::client::error::Error) -> String {
|
||||
if let ErrorBody::Standard { message, .. } = &error.body {
|
||||
pub(super) fn ruma_error_message(error: &ruma::api::error::Error) -> String {
|
||||
if let ErrorBody::Standard(StandardErrorBody { message, .. }) = &error.body {
|
||||
return message.clone();
|
||||
}
|
||||
|
||||
format!("{error}")
|
||||
}
|
||||
|
||||
pub(super) fn ruma_error_kind(e: &ruma::api::client::error::Error) -> &ErrorKind {
|
||||
pub(super) fn ruma_error_kind(e: &ruma::api::error::Error) -> &ErrorKind {
|
||||
e.error_kind().unwrap_or(&ErrorKind::Unknown)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::iter::once;
|
||||
|
||||
use ruma::{RoomVersionId, api::client::discovery::get_capabilities::RoomVersionStability};
|
||||
use ruma::{RoomVersionId, api::client::discovery::get_capabilities::v3::RoomVersionStability};
|
||||
|
||||
use crate::{at, is_equal_to};
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
use ruma::{
|
||||
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, RoomId,
|
||||
RoomVersionId, UserId, events::TimelineEventType,
|
||||
RoomVersionId, UserId, events::TimelineEventType, room_version_rules::RoomVersionRules,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
|
||||
@@ -95,11 +95,11 @@ fn get_content<T>(&self) -> Result<T>
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn redacts_id(&self, room_version: &RoomVersionId) -> Option<OwnedEventId>
|
||||
fn redacts_id(&self, room_version_rules: &RoomVersionRules) -> Option<OwnedEventId>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
redact::redacts_id(self, room_version)
|
||||
redact::redacts_id(self, room_version_rules)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user