mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-04-07 00:05:40 +00:00
Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55ccfdb973 | ||
|
|
a9a39e6d5e | ||
|
|
38bf1ccbcc | ||
|
|
b7a8cbdb42 | ||
|
|
4e1dac32a5 | ||
|
|
7b21c3fd9f | ||
|
|
f566ca1b93 | ||
|
|
debe411e23 | ||
|
|
dc0d6a9220 | ||
|
|
2efdb6fb0d | ||
|
|
576348a445 | ||
|
|
f322b6dca0 | ||
|
|
a1ed77a99c | ||
|
|
01b5dffeee | ||
|
|
ea3c00da43 | ||
|
|
047eba0442 | ||
|
|
11a088be5d | ||
|
|
dc6bd4e541 | ||
|
|
2bf9207cc4 | ||
|
|
b2a87e2fb9 | ||
|
|
7d0686f33c | ||
|
|
082c44f355 | ||
|
|
117c581948 | ||
|
|
cb846a3ad1 | ||
|
|
81b984b2cc | ||
|
|
e2961390ee | ||
|
|
cb75e836e0 | ||
|
|
cb7a988b1b | ||
|
|
aa5400bcef | ||
|
|
ff4dddd673 | ||
|
|
c22b17fb29 | ||
|
|
3da7fa24db | ||
|
|
d15ac1d3c1 | ||
|
|
a9ebdf58e2 | ||
|
|
f1ab27d344 | ||
|
|
8bc6e6ccca | ||
|
|
60a3abe752 | ||
|
|
e3b874d336 | ||
|
|
f3f82831b4 | ||
|
|
26aac1408e | ||
|
|
be8f62396a | ||
|
|
40996a6602 | ||
|
|
9cae531f90 | ||
|
|
56eea935b6 | ||
|
|
fcb646f8c4 | ||
|
|
57b21c1b32 | ||
|
|
8d66500c99 | ||
|
|
abacf1dc20 | ||
|
|
134e5cadaf | ||
|
|
8ec0f0d830 | ||
|
|
0453544036 | ||
|
|
89ad809270 | ||
|
|
ecd3a4eb41 | ||
|
|
5506997ca0 | ||
|
|
abc0683d59 | ||
|
|
dd60beb9fb | ||
|
|
d9520f9382 | ||
|
|
40bb5366bb | ||
|
|
f82bd77073 | ||
|
|
7d84ba5ff2 | ||
|
|
69a8937584 | ||
|
|
b2ec13d342 | ||
|
|
4e55e1ea90 | ||
|
|
f5f3108d5f | ||
|
|
d1e1ee6156 | ||
|
|
ae16a45515 | ||
|
|
077bda23a6 | ||
|
|
a2bf0c1223 | ||
|
|
b9b1ff87f2 | ||
|
|
3c0146d437 | ||
|
|
7485d4aa91 | ||
|
|
39bdb4c5a2 | ||
|
|
55fb3b8848 | ||
|
|
19146166c0 | ||
|
|
f47027006f | ||
|
|
b7a8f71e14 | ||
|
|
c7378d15ab | ||
|
|
7beeab270e | ||
|
|
6a812b7776 | ||
|
|
b1f4bbe89e | ||
|
|
6701f88bf9 | ||
|
|
62b9e8227b | ||
|
|
7369b58d91 | ||
|
|
f6df44b13f | ||
|
|
f243b383cb | ||
|
|
e0b7d03018 | ||
|
|
184ae2ebb9 | ||
|
|
0ea0d09b97 | ||
|
|
6763952ce4 | ||
|
|
e2da8301df | ||
|
|
296a4b92d6 | ||
|
|
00c054d356 | ||
|
|
2558ec0c2a | ||
|
|
56bc3c184e | ||
|
|
5c1b90b463 | ||
|
|
0dbb774559 | ||
|
|
16e0566c84 | ||
|
|
489b6e4ecb | ||
|
|
e71f75a58c | ||
|
|
082ed5b70c | ||
|
|
76fe8c4cdc | ||
|
|
c4a9f7a6d1 | ||
|
|
a047199fb4 | ||
|
|
411c9da743 | ||
|
|
fb54f2058c | ||
|
|
358273226c | ||
|
|
fd9bbb08ed | ||
|
|
53184cd2fc | ||
|
|
25f7d80a8c | ||
|
|
02fa0ba0b8 | ||
|
|
572b228f40 | ||
|
|
b0a61e38da | ||
|
|
401dff20eb | ||
|
|
f2a50e8f62 | ||
|
|
36e80b0af4 | ||
|
|
c9a4c546e2 | ||
|
|
da8b60b4ce | ||
|
|
89afaa94ac | ||
|
|
2b5563cee3 | ||
|
|
6cb9d50383 | ||
|
|
77c0f6e0c6 | ||
|
|
c85e710760 | ||
|
|
59346fc766 | ||
|
|
9c5e735888 | ||
|
|
fe74e82318 | ||
|
|
cb79a3b9d7 | ||
|
|
ebc8df1c4d | ||
|
|
b667a963cf | ||
|
|
5a6b909b37 | ||
|
|
dba9cf0ad2 | ||
|
|
287ddd9bc5 | ||
|
|
79a278b9e8 | ||
|
|
6c5d658ef2 | ||
|
|
70c43abca8 | ||
|
|
6a9b47c52e | ||
|
|
c042de96f8 | ||
|
|
7a6acd1c82 | ||
|
|
d260c4fcc2 | ||
|
|
fa15de9764 | ||
|
|
e6c7a4ae60 | ||
|
|
5bed4ad81d |
@@ -1,9 +1,9 @@
|
||||
# Local build and dev artifacts
|
||||
target/
|
||||
!target/debug/conduwuit
|
||||
|
||||
# Docker files
|
||||
Dockerfile*
|
||||
docker/
|
||||
|
||||
# IDE files
|
||||
.vscode
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
container: ["ubuntu-latest", "ubuntu-previous", "debian-latest", "debian-oldstable"]
|
||||
container: [ "ubuntu-latest", "ubuntu-previous", "debian-latest", "debian-oldstable" ]
|
||||
container:
|
||||
image: "ghcr.io/tcpipuk/act-runner:${{ matrix.container }}"
|
||||
|
||||
@@ -30,6 +30,28 @@ jobs:
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "distribution=$DISTRIBUTION" >> $GITHUB_OUTPUT
|
||||
echo "Debian distribution: $DISTRIBUTION ($VERSION)"
|
||||
- name: Work around llvm-project#153385
|
||||
id: llvm-workaround
|
||||
run: |
|
||||
if [ -f /usr/share/apt/default-sequoia.config ]; then
|
||||
echo "Applying workaround for llvm-project#153385"
|
||||
mkdir -p /etc/crypto-policies/back-ends/
|
||||
cp /usr/share/apt/default-sequoia.config /etc/crypto-policies/back-ends/apt-sequoia.config
|
||||
sed -i 's/\(sha1\.second_preimage_resistance = \)2026-02-01/\12026-06-01/' /etc/crypto-policies/back-ends/apt-sequoia.config
|
||||
else
|
||||
echo "No workaround needed for llvm-project#153385"
|
||||
fi
|
||||
- name: Pick compatible clang version
|
||||
id: clang-version
|
||||
run: |
|
||||
# both latest need to use clang-23, but oldstable and previous can just use clang
|
||||
if [[ "${{ matrix.container }}" == "ubuntu-latest" || "${{ matrix.container }}" == "debian-latest" ]]; then
|
||||
echo "Using clang-23 package for ${{ matrix.container }}"
|
||||
echo "version=clang-23" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Using default clang package for ${{ matrix.container }}"
|
||||
echo "version=clang" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Checkout repository with full history
|
||||
uses: actions/checkout@v6
|
||||
@@ -105,7 +127,7 @@ jobs:
|
||||
run: |
|
||||
apt-get update -y
|
||||
# Build dependencies for rocksdb
|
||||
apt-get install -y clang liburing-dev
|
||||
apt-get install -y liburing-dev ${{ steps.clang-version.outputs.version }}
|
||||
|
||||
- name: Run cargo-deb
|
||||
id: cargo-deb
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,4 +1,4 @@
|
||||
github: [JadedBlueEyes, nexy7574]
|
||||
github: [JadedBlueEyes, nexy7574, gingershaped]
|
||||
custom:
|
||||
- https://ko-fi.com/nexy7574
|
||||
- https://ko-fi.com/JadedBlueEyes
|
||||
|
||||
@@ -23,7 +23,7 @@ repos:
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.41.0
|
||||
rev: v1.43.4
|
||||
hooks:
|
||||
- id: typos
|
||||
- id: typos
|
||||
@@ -31,7 +31,7 @@ repos:
|
||||
stages: [commit-msg]
|
||||
|
||||
- repo: https://github.com/crate-ci/committed
|
||||
rev: v1.1.9
|
||||
rev: v1.1.10
|
||||
hooks:
|
||||
- id: committed
|
||||
|
||||
|
||||
@@ -6,14 +6,13 @@ extend-exclude = ["*.csr", "*.lock", "pnpm-lock.yaml"]
|
||||
extend-ignore-re = [
|
||||
"(?Rm)^.*(#|//|<!--)\\s*spellchecker:disable-line(\\s*-->)$", # Ignore a line by making it trail with a `spellchecker:disable-line` comment
|
||||
"^[0-9a-f]{7,}$", # Commit hashes
|
||||
|
||||
"4BA7",
|
||||
# some heuristics for base64 strings
|
||||
"[A-Za-z0-9+=]{72,}",
|
||||
"([A-Za-z0-9+=]|\\\\\\s\\*){72,}",
|
||||
"[0-9+][A-Za-z0-9+]{30,}[a-z0-9+]",
|
||||
"\\$[A-Z0-9+][A-Za-z0-9+]{6,}[a-z0-9+]",
|
||||
"\\b[a-z0-9+/=][A-Za-z0-9+/=]{7,}[a-z0-9+/=][A-Z]\\b",
|
||||
|
||||
# In the renovate config
|
||||
".ontainer"
|
||||
]
|
||||
|
||||
138
CHANGELOG.md
138
CHANGELOG.md
@@ -1,58 +1,150 @@
|
||||
# Continuwuity v0.5.5 (2026-02-15)
|
||||
|
||||
## Features
|
||||
|
||||
- Added unstable support for [MSC4406:
|
||||
`M_SENDER_IGNORED`](https://github.com/matrix-org/matrix-spec-proposals/pull/4406).
|
||||
Contributed by @nex ([#1308](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1308))
|
||||
- Introduce a resolver command to allow flushing a server from the cache or to flush the complete cache. Contributed by
|
||||
@Omar007 ([#1349](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1349))
|
||||
- Improved the handling of restricted join rules and improved the performance of local-first joins. Contributed by
|
||||
@nex. ([#1368](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1368))
|
||||
- You can now set a custom User Agent for URL previews; the default one has been modified to be less likely to be
|
||||
rejected. Contributed by @trashpanda ([#1372](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1372))
|
||||
- Improved the first-time setup experience for new homeserver administrators:
|
||||
- Account registration is disabled on the first run, except for with a new special registration token that is logged
|
||||
to the console.
|
||||
- Other helpful information is logged to the console as well, including a giant warning if open registration is
|
||||
enabled.
|
||||
- The default index page now says to check the console for setup instructions if no accounts have been created.
|
||||
- Once the first admin account is created, an improved welcome message is sent to the admin room.
|
||||
|
||||
Contributed by @ginger.
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fixed invites sent to other users in the same homeserver not being properly sent down sync. Users with missing or
|
||||
broken invites should clear their client caches after updating to make them appear. ([#1249](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1249))
|
||||
- LDAP-enabled servers will no longer have all admins demoted when LDAP-controlled admins are not configured.
|
||||
Contributed by @Jade ([#1307](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1307))
|
||||
- Fixed sliding sync not resolving wildcard state key requests, enabling Video/Audio calls in Element X. ([#1370](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1370))
|
||||
|
||||
## Misc
|
||||
|
||||
- #1344
|
||||
|
||||
# Continuwuity v0.5.4 (2026-02-08)
|
||||
|
||||
## Features
|
||||
|
||||
- The announcement checker will now announce errors it encounters in the first run to the admin room, plus a few other
|
||||
misc improvements. Contributed by @Jade ([#1288](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1288))
|
||||
- Drastically improved the performance and reliability of account deactivations. Contributed by
|
||||
@nex ([#1314](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1314))
|
||||
- Refuse to process requests for and events in rooms that we no longer have any local users in (reduces state resets
|
||||
and improves performance). Contributed by
|
||||
@nex ([#1316](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1316))
|
||||
- Added server-specific admin API routes to ban and unban rooms, for use with moderation bots. Contributed by @nex
|
||||
([#1301](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1301))
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fix the generated configuration containing uncommented optional sections. Contributed by
|
||||
@Jade ([#1290](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1290))
|
||||
- Fixed specification non-compliance when handling remote media errors. Contributed by
|
||||
@nex ([#1298](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1298))
|
||||
- UIAA requests which check for out-of-band success (sent by matrix-js-sdk) will no longer create unhelpful errors in
|
||||
the logs. Contributed by @ginger ([#1305](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1305))
|
||||
- Use exists instead of contains to save writing to a buffer in `src/service/users/mod.rs`: `is_login_disabled`.
|
||||
Contributed
|
||||
by @aprilgrimoire. ([#1340](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1340))
|
||||
- Fixed backtraces being swallowed during panics. Contributed by
|
||||
@jade ([#1337](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1337))
|
||||
- Fixed a potential vulnerability that could allow an evil remote server to return malicious events during the room join
|
||||
and knock process. Contributed by @nex, reported by violet & [mat](https://matdoes.dev).
|
||||
- Fixed a race condition that could result in outlier PDUs being incorrectly marked as visible to a remote server.
|
||||
Contributed by @nex, reported by violet & [mat](https://matdoes.dev).
|
||||
- ACLs are no longer case-sensitive. Contributed by @nex, reported by [vel](matrix:u/vel:nhjkl.com?action=chat).
|
||||
|
||||
## Docs
|
||||
|
||||
- Fixed Fedora install instructions. Contributed by
|
||||
@julian45 ([#1342](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1342))
|
||||
|
||||
# Continuwuity 0.5.3 (2026-01-12)
|
||||
|
||||
## Features
|
||||
|
||||
- Improve the display of nested configuration with the `!admin server show-config` command. Contributed by @Jade (#1279)
|
||||
- Improve the display of nested configuration with the `!admin server show-config` command. Contributed by
|
||||
@Jade ([#1279](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1279))
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fixed `M_BAD_JSON` error when sending invites to other servers or when providing joins. Contributed by @nex (#1286)
|
||||
|
||||
- Fixed `M_BAD_JSON` error when sending invites to other servers or when providing joins. Contributed by
|
||||
@nex ([#1286](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1286))
|
||||
|
||||
## Docs
|
||||
|
||||
- Improve admin command documentation generation. Contributed by @ginger (#1280)
|
||||
|
||||
- Improve admin command documentation generation. Contributed by
|
||||
@ginger ([#1280](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1280))
|
||||
|
||||
## Misc
|
||||
|
||||
- Improve timeout-related code for federation and URL previews. Contributed by @Jade
|
||||
|
||||
- Improve timeout-related code for federation and URL previews. Contributed by
|
||||
@Jade ([#1278](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1278))
|
||||
|
||||
# Continuwuity 0.5.2 (2026-01-09)
|
||||
|
||||
## Features
|
||||
|
||||
- Added support for issuing additional registration tokens, stored in the database, which supplement the existing registration token hardcoded in the config file. These tokens may optionally expire after a certain number of uses or after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is superseded by this feature and **has been removed**. Use the new `!admin token` command family to manage registration tokens. Contributed by @ginger (#783).
|
||||
- Implemented a configuration defined admin list independent of the admin room. Contributed by @Terryiscool160. (#1253)
|
||||
- Added support for invite and join anti-spam via Draupnir and Meowlnir, similar to that of synapse-http-antispam. Contributed by @nex. (#1263)
|
||||
- Implemented account locking functionality, to complement user suspension. Contributed by @nex. (#1266)
|
||||
- Added admin command to forcefully log out all of a user's existing sessions. Contributed by @nex. (#1271)
|
||||
- Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex. (#1272)
|
||||
- Add support for custom room create event timestamps, to allow generating custom prefixes in hashed room IDs. Contributed by @nex. (#1277)
|
||||
- Certain potentially dangerous admin commands are now restricted to only be usable in the admin room and server console. Contributed by @ginger.
|
||||
- Added support for issuing additional registration tokens, stored in the database, which supplement the existing
|
||||
registration token hardcoded in the config file. These tokens may optionally expire after a certain number of uses or
|
||||
after a certain amount of time has passed. Additionally, the `registration_token_file` configuration option is
|
||||
superseded by this feature and **has been removed**. Use the new `!admin token` command family to manage registration
|
||||
tokens. Contributed by @ginger (#783).
|
||||
- Implemented a configuration defined admin list independent of the admin room. Contributed by
|
||||
@Terryiscool160. ([#1253](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1253))
|
||||
- Added support for invite and join anti-spam via Draupnir and Meowlnir, similar to that of synapse-http-antispam.
|
||||
Contributed by @nex. ([#1263](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1263))
|
||||
- Implemented account locking functionality, to complement user suspension. Contributed by
|
||||
@nex. ([#1266](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1266))
|
||||
- Added admin command to forcefully log out all of a user's existing sessions. Contributed by
|
||||
@nex. ([#1271](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1271))
|
||||
- Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex. (
|
||||
[#1272](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1272))
|
||||
- Add support for custom room create event timestamps, to allow generating custom prefixes in hashed room IDs.
|
||||
Contributed by @nex. ([#1277](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1277))
|
||||
- Certain potentially dangerous admin commands are now restricted to only be usable in the admin room and server
|
||||
console. Contributed by @ginger.
|
||||
|
||||
## Bugfixes
|
||||
|
||||
- Fixed unreliable room summary fetching and improved error messages. Contributed by @nex. (#1257)
|
||||
- Client requested timeout parameter is now applied to e2ee key lookups and claims. Related federation requests are now also concurrent. Contributed by @nex. (#1261)
|
||||
- Fixed the whoami endpoint returning HTTP 404 instead of HTTP 403, which confused some appservices. Contributed by @nex. (#1276)
|
||||
- Fixed unreliable room summary fetching and improved error messages. Contributed by
|
||||
@nex. ([#1257](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1257))
|
||||
- Client requested timeout parameter is now applied to e2ee key lookups and claims. Related federation requests are now
|
||||
also concurrent. Contributed by @nex. ([#1261](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1261))
|
||||
- Fixed the whoami endpoint returning HTTP 404 instead of HTTP 403, which confused some appservices. Contributed by
|
||||
@nex. ([#1276](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1276))
|
||||
|
||||
## Misc
|
||||
|
||||
- The `console` feature is now enabled by default, allowing the server console to be used for running admin commands directly. To automatically open the console on startup, set the `admin_console_automatic` config option to `true`. Contributed by @ginger.
|
||||
- The `console` feature is now enabled by default, allowing the server console to be used for running admin commands
|
||||
directly. To automatically open the console on startup, set the `admin_console_automatic` config option to `true`.
|
||||
Contributed by @ginger.
|
||||
- We now (finally) document our container image mirrors. Contributed by @Jade
|
||||
|
||||
|
||||
# Continuwuity 0.5.0 (2025-12-30)
|
||||
|
||||
**This release contains a CRITICAL vulnerability patch, and you must update as soon as possible**
|
||||
|
||||
## Features
|
||||
|
||||
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (@Jade). (#1251)
|
||||
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (
|
||||
@Jade). ([#1251](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1251))
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Don't allow admin room upgrades, as this can break the admin room (@timedout) (#1245)
|
||||
- Fix invalid creators in power levels during upgrade to v12 (@timedout) (#1245)
|
||||
- Don't allow admin room upgrades, as this can break the admin room (
|
||||
@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))
|
||||
- Fix invalid creators in power levels during upgrade to v12 (
|
||||
@timedout) ([#1245](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1245))
|
||||
|
||||
1169
Cargo.lock
generated
1169
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@@ -12,7 +12,7 @@ license = "Apache-2.0"
|
||||
# See also `rust-toolchain.toml`
|
||||
readme = "README.md"
|
||||
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||
version = "0.5.3"
|
||||
version = "0.5.5"
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "conduwuit"
|
||||
@@ -158,7 +158,7 @@ features = ["raw_value"]
|
||||
|
||||
# Used for appservice registration files
|
||||
[workspace.dependencies.serde-saphyr]
|
||||
version = "0.0.10"
|
||||
version = "0.0.19"
|
||||
|
||||
# Used to load forbidden room/user regex from config
|
||||
[workspace.dependencies.serde_regex]
|
||||
@@ -342,7 +342,7 @@ version = "0.1.2"
|
||||
# Used for matrix spec type definitions and helpers
|
||||
[workspace.dependencies.ruma]
|
||||
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
||||
rev = "f9e74cb206cfa45cf5f17d39282253b43a15fcd5"
|
||||
rev = "b496b7f38d517149361a882e75d3fd4faf210441"
|
||||
features = [
|
||||
"compat",
|
||||
"rand",
|
||||
@@ -378,7 +378,8 @@ features = [
|
||||
"unstable-msc4210", # remove legacy mentions
|
||||
"unstable-extensible-events",
|
||||
"unstable-pdu",
|
||||
"unstable-msc4155"
|
||||
"unstable-msc4155",
|
||||
"unstable-msc4143", # livekit well_known response
|
||||
]
|
||||
|
||||
[workspace.dependencies.rust-rocksdb]
|
||||
@@ -548,6 +549,12 @@ 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.14.0"
|
||||
|
||||
#
|
||||
# Patches
|
||||
#
|
||||
|
||||
@@ -57,9 +57,10 @@ ### What are the project's goals?
|
||||
|
||||
### Can I try it out?
|
||||
|
||||
Check out the [documentation](https://continuwuity.org) for installation instructions.
|
||||
Check out the [documentation](https://continuwuity.org) for installation instructions, or join one of these vetted public homeservers running Continuwuity to get a feel for things!
|
||||
|
||||
There are currently no open registration Continuwuity instances available.
|
||||
- https://continuwuity.rocks -- A public demo server operated by the Continuwuity Team.
|
||||
- https://federated.nexus -- Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo.
|
||||
|
||||
### What are we working on?
|
||||
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Path to Complement's source code
|
||||
#
|
||||
# The `COMPLEMENT_SRC` environment variable is set in the Nix dev shell, which
|
||||
# points to a store path containing the Complement source code. It's likely you
|
||||
# want to just pass that as the first argument to use it here.
|
||||
# The root path where complement is available.
|
||||
COMPLEMENT_SRC="${COMPLEMENT_SRC:-$1}"
|
||||
|
||||
# A `.jsonl` file to write test logs to
|
||||
@@ -15,7 +11,10 @@ LOG_FILE="${2:-complement_test_logs.jsonl}"
|
||||
# A `.jsonl` file to write test results to
|
||||
RESULTS_FILE="${3:-complement_test_results.jsonl}"
|
||||
|
||||
COMPLEMENT_BASE_IMAGE="${COMPLEMENT_BASE_IMAGE:-complement-conduwuit:main}"
|
||||
# The base docker image to use for complement tests
|
||||
# You can build the default with `docker build -t continuwuity:complement -f ./docker/complement.Dockerfile .`
|
||||
# after running `cargo build`. Only the debug binary is used.
|
||||
COMPLEMENT_BASE_IMAGE="${COMPLEMENT_BASE_IMAGE:-continuwuity:complement}"
|
||||
|
||||
# Complement tests that are skipped due to flakiness/reliability issues or we don't implement such features and won't for a long time
|
||||
SKIPPED_COMPLEMENT_TESTS='TestPartialStateJoin.*|TestRoomDeleteAlias/Parallel/Regular_users_can_add_and_delete_aliases_when_m.*|TestRoomDeleteAlias/Parallel/Can_delete_canonical_alias|TestUnbanViaInvite.*|TestRoomState/Parallel/GET_/publicRooms_lists.*"|TestRoomDeleteAlias/Parallel/Users_with_sufficient_power-level_can_delete_other.*'
|
||||
@@ -34,25 +33,6 @@ toplevel="$(git rev-parse --show-toplevel)"
|
||||
|
||||
pushd "$toplevel" > /dev/null
|
||||
|
||||
if [ ! -f "complement_oci_image.tar.gz" ]; then
|
||||
echo "building complement conduwuit image"
|
||||
|
||||
# if using macOS, use linux-complement
|
||||
#bin/nix-build-and-cache just .#linux-complement
|
||||
bin/nix-build-and-cache just .#complement
|
||||
#nix build -L .#complement
|
||||
|
||||
echo "complement conduwuit image tar.gz built at \"result\""
|
||||
|
||||
echo "loading into docker"
|
||||
docker load < result
|
||||
popd > /dev/null
|
||||
else
|
||||
echo "skipping building a complement conduwuit image as complement_oci_image.tar.gz was already found, loading this"
|
||||
|
||||
docker load < complement_oci_image.tar.gz
|
||||
popd > /dev/null
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "running go test with:"
|
||||
@@ -72,24 +52,16 @@ env \
|
||||
set -o pipefail
|
||||
|
||||
# Post-process the results into an easy-to-compare format, sorted by Test name for reproducible results
|
||||
cat "$LOG_FILE" | jq -s -c 'sort_by(.Test)[]' | jq -c '
|
||||
jq -s -c 'sort_by(.Test)[]' < "$LOG_FILE" | jq -c '
|
||||
select(
|
||||
(.Action == "pass" or .Action == "fail" or .Action == "skip")
|
||||
and .Test != null
|
||||
) | {Action: .Action, Test: .Test}
|
||||
' > "$RESULTS_FILE"
|
||||
|
||||
#if command -v gotestfmt &> /dev/null; then
|
||||
# echo "using gotestfmt on $LOG_FILE"
|
||||
# grep '{"Time":' "$LOG_FILE" | gotestfmt > "complement_test_logs_gotestfmt.log"
|
||||
#fi
|
||||
|
||||
echo ""
|
||||
echo ""
|
||||
echo "complement logs saved at $LOG_FILE"
|
||||
echo "complement results saved at $RESULTS_FILE"
|
||||
#if command -v gotestfmt &> /dev/null; then
|
||||
# echo "complement logs in gotestfmt pretty format outputted at complement_test_logs_gotestfmt.log (use an editor/terminal/pager that interprets ANSI colours and UTF-8 emojis)"
|
||||
#fi
|
||||
echo ""
|
||||
echo ""
|
||||
|
||||
67
complement/complement-entrypoint.sh
Normal file
67
complement/complement-entrypoint.sh
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
set -xe
|
||||
# If we have no $SERVER_NAME set, abort
|
||||
if [ -z "$SERVER_NAME" ]; then
|
||||
echo "SERVER_NAME is not set, aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If /complement/ca/ca.crt or /complement/ca/ca.key are missing, abort
|
||||
if [ ! -f /complement/ca/ca.crt ] || [ ! -f /complement/ca/ca.key ]; then
|
||||
echo "/complement/ca/ca.crt or /complement/ca/ca.key is missing, aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add the root cert to the local trust store
|
||||
echo 'Installing Complement CA certificate to local trust store'
|
||||
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/complement-ca.crt
|
||||
update-ca-certificates
|
||||
|
||||
# Sign a certificate for our $SERVER_NAME
|
||||
echo "Generating and signing certificate for $SERVER_NAME"
|
||||
openssl genrsa -out "/$SERVER_NAME.key" 2048
|
||||
|
||||
echo "Generating CSR for $SERVER_NAME"
|
||||
openssl req -new -sha256 \
|
||||
-key "/$SERVER_NAME.key" \
|
||||
-out "/$SERVER_NAME.csr" \
|
||||
-subj "/C=US/ST=CA/O=Continuwuity, Inc./CN=$SERVER_NAME"\
|
||||
-addext "subjectAltName=DNS:$SERVER_NAME"
|
||||
openssl req -in "$SERVER_NAME.csr" -noout -text
|
||||
|
||||
echo "Signing certificate for $SERVER_NAME with Complement CA"
|
||||
cat <<EOF > ./cert.ext
|
||||
authorityKeyIdentifier=keyid,issuer
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = digitalSignature, keyEncipherment, dataEncipherment, nonRepudiation
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
DNS.1 = *.docker.internal
|
||||
DNS.2 = hs1
|
||||
DNS.3 = hs2
|
||||
DNS.4 = hs3
|
||||
DNS.5 = hs4
|
||||
DNS.6 = $SERVER_NAME
|
||||
IP.1 = 127.0.0.1
|
||||
EOF
|
||||
openssl x509 \
|
||||
-req \
|
||||
-in "/$SERVER_NAME.csr" \
|
||||
-CA /complement/ca/ca.crt \
|
||||
-CAkey /complement/ca/ca.key \
|
||||
-CAcreateserial \
|
||||
-out "/$SERVER_NAME.crt" \
|
||||
-days 1 \
|
||||
-sha256 \
|
||||
-extfile ./cert.ext
|
||||
|
||||
# Tell continuwuity where to find the certs
|
||||
export CONTINUWUITY_TLS__KEY="/$SERVER_NAME.key"
|
||||
export CONTINUWUITY_TLS__CERTS="/$SERVER_NAME.crt"
|
||||
# And who it is
|
||||
export CONTINUWUITY_SERVER_NAME="$SERVER_NAME"
|
||||
|
||||
echo "Starting Continuwuity with SERVER_NAME=$SERVER_NAME"
|
||||
# Start continuwuity
|
||||
/usr/local/bin/conduwuit --config /etc/continuwuity/config.toml
|
||||
53
complement/complement.config.toml
Normal file
53
complement/complement.config.toml
Normal file
@@ -0,0 +1,53 @@
|
||||
# ============================================= #
|
||||
# Complement pre-filled configuration file #
|
||||
#
|
||||
# DANGER: THIS FILE FORCES INSECURE VALUES. #
|
||||
# DO NOT USE OUTSIDE THE TEST SUITE ENV! #
|
||||
# ============================================= #
|
||||
[global]
|
||||
address = "0.0.0.0"
|
||||
allow_device_name_federation = true
|
||||
allow_guest_registration = true
|
||||
allow_public_room_directory_over_federation = true
|
||||
allow_public_room_directory_without_auth = true
|
||||
allow_registration = true
|
||||
database_path = "/database"
|
||||
log = "trace,h2=debug,hyper=debug"
|
||||
port = [8008, 8448]
|
||||
trusted_servers = []
|
||||
only_query_trusted_key_servers = false
|
||||
query_trusted_key_servers_first = false
|
||||
query_trusted_key_servers_first_on_join = false
|
||||
yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true
|
||||
ip_range_denylist = []
|
||||
url_preview_domain_contains_allowlist = ["*"]
|
||||
url_preview_domain_explicit_denylist = ["*"]
|
||||
media_compat_file_link = false
|
||||
media_startup_check = true
|
||||
prune_missing_media = true
|
||||
log_colors = true
|
||||
admin_room_notices = false
|
||||
allow_check_for_updates = false
|
||||
intentionally_unknown_config_option_for_testing = true
|
||||
rocksdb_log_level = "info"
|
||||
rocksdb_max_log_files = 1
|
||||
rocksdb_recovery_mode = 0
|
||||
rocksdb_paranoid_file_checks = true
|
||||
log_guest_registrations = false
|
||||
allow_legacy_media = true
|
||||
startup_netburst = true
|
||||
startup_netburst_keep = -1
|
||||
allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure = true
|
||||
dns_timeout = 60
|
||||
dns_attempts = 20
|
||||
request_conn_timeout = 60
|
||||
request_timeout = 120
|
||||
well_known_conn_timeout = 60
|
||||
well_known_timeout = 60
|
||||
federation_idle_timeout = 300
|
||||
sender_timeout = 300
|
||||
sender_idle_timeout = 300
|
||||
sender_retry_backoff_limit = 300
|
||||
|
||||
[global.tls]
|
||||
dual_protocol = true
|
||||
@@ -433,7 +433,7 @@
|
||||
# If you would like registration only via token reg, please configure
|
||||
# `registration_token`.
|
||||
#
|
||||
#allow_registration = false
|
||||
#allow_registration = true
|
||||
|
||||
# If registration is enabled, and this setting is true, new users
|
||||
# registered after the first admin user will be automatically suspended
|
||||
@@ -1474,6 +1474,10 @@
|
||||
#
|
||||
#url_preview_check_root_domain = false
|
||||
|
||||
# User agent that is used specifically when fetching url previews.
|
||||
#
|
||||
#url_preview_user_agent = "continuwuity/<version> (bot; +https://continuwuity.org)"
|
||||
|
||||
# List of forbidden room aliases and room IDs as strings of regex
|
||||
# patterns.
|
||||
#
|
||||
@@ -1820,6 +1824,17 @@
|
||||
#
|
||||
#support_mxid =
|
||||
|
||||
# A list of MatrixRTC foci URLs which will be served as part of the
|
||||
# MSC4143 client endpoint at /.well-known/matrix/client. If you're
|
||||
# setting up livekit, you'd want something like:
|
||||
# rtc_focus_server_urls = [
|
||||
# { type = "livekit", livekit_service_url = "https://livekit.example.com" },
|
||||
# ]
|
||||
#
|
||||
# To disable, set this to be an empty vector (`[]`).
|
||||
#
|
||||
#rtc_focus_server_urls = []
|
||||
|
||||
[global.blurhashing]
|
||||
|
||||
# blurhashing x component, 4 is recommended by https://blurha.sh/
|
||||
@@ -1926,9 +1941,9 @@
|
||||
#
|
||||
#admin_filter = ""
|
||||
|
||||
[global.antispam]
|
||||
#[global.antispam]
|
||||
|
||||
[global.antispam.meowlnir]
|
||||
#[global.antispam.meowlnir]
|
||||
|
||||
# The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
|
||||
#
|
||||
@@ -1953,7 +1968,7 @@
|
||||
#
|
||||
#check_all_joins = false
|
||||
|
||||
[global.antispam.draupnir]
|
||||
#[global.antispam.draupnir]
|
||||
|
||||
# The base URL on which to contact Draupnir (before /api/).
|
||||
#
|
||||
|
||||
@@ -48,7 +48,7 @@ EOF
|
||||
|
||||
# Developer tool versions
|
||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||
ENV BINSTALL_VERSION=1.16.6
|
||||
ENV BINSTALL_VERSION=1.17.5
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
11
docker/complement.Dockerfile
Normal file
11
docker/complement.Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM ubuntu:latest
|
||||
EXPOSE 8008
|
||||
EXPOSE 8448
|
||||
RUN apt-get update && apt-get install -y ca-certificates liburing2 && rm -rf /var/lib/apt/lists/*
|
||||
RUN mkdir -p /etc/continuwuity /var/lib/continuwuity /usr/local/bin/
|
||||
COPY complement/complement-entrypoint.sh /usr/local/bin/complement-entrypoint.sh
|
||||
COPY complement/complement.config.toml /etc/continuwuity/config.toml
|
||||
COPY target/debug/conduwuit /usr/local/bin/conduwuit
|
||||
RUN chmod +x /usr/local/bin/conduwuit /usr/local/bin/complement-entrypoint.sh
|
||||
#HEALTHCHECK --interval=30s --timeout=5s CMD curl --fail http://localhost:8008/_continuwuity/server_version || exit 1
|
||||
ENTRYPOINT ["/usr/local/bin/complement-entrypoint.sh"]
|
||||
@@ -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.16.6
|
||||
ENV BINSTALL_VERSION=1.17.5
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -34,6 +34,14 @@
|
||||
"name": "troubleshooting",
|
||||
"label": "Troubleshooting"
|
||||
},
|
||||
"security",
|
||||
{
|
||||
"type": "dir-section-header",
|
||||
"name": "community",
|
||||
"label": "Community",
|
||||
"collapsible": true,
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"type": "divider"
|
||||
},
|
||||
@@ -63,7 +71,5 @@
|
||||
},
|
||||
{
|
||||
"type": "divider"
|
||||
},
|
||||
"community",
|
||||
"security"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -19,16 +19,21 @@
|
||||
{
|
||||
"text": "Admin Command Reference",
|
||||
"link": "/reference/admin/"
|
||||
},
|
||||
{
|
||||
"text": "Server Reference",
|
||||
"link": "/reference/server"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Community",
|
||||
"link": "/community"
|
||||
"items": [
|
||||
{
|
||||
"text": "Community Guidelines",
|
||||
"link": "/community/guidelines"
|
||||
},
|
||||
{
|
||||
"text": "Become a Partnered Homeserver!",
|
||||
"link": "/community/ops-guidelines"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Security",
|
||||
|
||||
12
docs/community/_meta.json
Normal file
12
docs/community/_meta.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"type": "file",
|
||||
"name": "guidelines",
|
||||
"label": "Community Guidelines"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "ops-guidelines",
|
||||
"label": "Partnered Homeserver Guidelines"
|
||||
}
|
||||
]
|
||||
32
docs/community/ops-guidelines.mdx
Normal file
32
docs/community/ops-guidelines.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
# Partnered Homeserver Operator Requirements
|
||||
> _So you want to be an officially sanctioned public Continuwuity homeserver operator?_
|
||||
|
||||
Thank you for your interest in the project! There's a few things we need from you first to make sure your homeserver meets our quality standards and that you are prepared to handle the additional workload introduced by operating a public chat service.
|
||||
|
||||
## Stuff you must have
|
||||
if you don't do these things we will tell you to go away
|
||||
|
||||
- Your homeserver must be running an up-to-date version of Continuwuity
|
||||
- You must have a CAPTCHA, external registration system, or apply-to-join system that provides one-time-use invite codes (we do not accept fully open nor static token registration)
|
||||
- Your homeserver must have support details listed in [`/.well-known/matrix/support`](https://spec.matrix.org/v1.17/client-server-api/#getwell-knownmatrixsupport)
|
||||
- Your rules and guidelines must align with [the project's own code of conduct](guidelines).
|
||||
- You must be reasonably responsive (i.e. don't leave us hanging for a week if we alert you to an issue on your server)
|
||||
- Your homeserver's community rooms (if any) must be protected by a moderation bot subscribed to policy lists like the Community Moderation Effort (you can get one from https://asgard.chat if you don't want to run your own)
|
||||
|
||||
## Stuff we encourage you to have
|
||||
not strictly required but we will consider your request more strongly if you have it
|
||||
|
||||
- You should have automated moderation tooling that can automatically suspend abusive users on your homeserver who are added to policy lists
|
||||
- You should have multiple server administrators (increased bus factor)
|
||||
- You should have a terms of service and privacy policy prominently available
|
||||
|
||||
## Stuff you get
|
||||
|
||||
- Prominent listing in our README!
|
||||
- A gold star sticker
|
||||
- Access to a low noise room for more direct communication with maintainers and collaboration with fellow operators
|
||||
- Read-only access to the continuwuity internal ban list
|
||||
- Early notice of upcoming releases
|
||||
|
||||
## Sound good?
|
||||
To get started, ping a team member in [our main chatroom](https://matrix.to/#/#continuwuity:continuwuity.org) and ask to be added to the list.
|
||||
@@ -1,17 +1,18 @@
|
||||
# RPM Installation Guide
|
||||
|
||||
Continuwuity is available as RPM packages for Fedora, RHEL, and compatible distributions.
|
||||
Continuwuity is available as RPM packages for Fedora and compatible distributions.
|
||||
We do not currently have infrastructure to build RPMs for RHEL and compatible distributions, but this is a work in progress.
|
||||
|
||||
The RPM packaging files are maintained in the `fedora/` directory:
|
||||
- `continuwuity.spec.rpkg` - RPM spec file using rpkg macros for building from git
|
||||
- `continuwuity.service` - Systemd service file for the server
|
||||
- `RPM-GPG-KEY-continuwuity.asc` - GPG public key for verifying signed packages
|
||||
|
||||
RPM packages built by CI are signed with our GPG key (Ed25519, ID: `5E0FF73F411AAFCA`).
|
||||
RPM packages built by CI are signed with our GPG key (RSA, ID: `6595 E8DB 9191 D39A 46D6 A514 4BA7 F590 DF0B AA1D`). # spellchecker:disable-line
|
||||
|
||||
```bash
|
||||
# Import the signing key
|
||||
sudo rpm --import https://forgejo.ellis.link/continuwuation/continuwuity/raw/branch/main/fedora/RPM-GPG-KEY-continuwuity.asc
|
||||
sudo rpm --import https://forgejo.ellis.link/api/packages/continuwuation/rpm/repository.key
|
||||
|
||||
# Verify a downloaded package
|
||||
rpm --checksig continuwuity-*.rpm
|
||||
@@ -23,7 +24,7 @@ ## Installation methods
|
||||
|
||||
```bash
|
||||
# Add the repository and install
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable/continuwuation.repo
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable.repo
|
||||
sudo dnf install continuwuity
|
||||
```
|
||||
|
||||
@@ -31,7 +32,7 @@ # Add the repository and install
|
||||
|
||||
```bash
|
||||
# Add the dev repository and install
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/dev/continuwuation.repo
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/dev.repo
|
||||
sudo dnf install continuwuity
|
||||
```
|
||||
|
||||
@@ -39,23 +40,10 @@ # Add the dev repository and install
|
||||
|
||||
```bash
|
||||
# Branch names are sanitized (slashes become hyphens, lowercase only)
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/tom-new-feature/continuwuation.repo
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/tom-new-feature.repo
|
||||
sudo dnf install continuwuity
|
||||
```
|
||||
|
||||
**Direct installation** without adding repository
|
||||
|
||||
```bash
|
||||
# Latest stable release
|
||||
sudo dnf install https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable/continuwuity
|
||||
|
||||
# Latest development build
|
||||
sudo dnf install https://forgejo.ellis.link/api/packages/continuwuation/rpm/dev/continuwuity
|
||||
|
||||
# Specific feature branch
|
||||
sudo dnf install https://forgejo.ellis.link/api/packages/continuwuation/rpm/branch-name/continuwuity
|
||||
```
|
||||
|
||||
**Manual repository configuration** (alternative method)
|
||||
|
||||
```bash
|
||||
@@ -65,7 +53,7 @@ # Specific feature branch
|
||||
baseurl=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgkey=https://forgejo.ellis.link/continuwuation/continuwuity/raw/branch/main/fedora/RPM-GPG-KEY-continuwuity.asc
|
||||
gpgkey=https://forgejo.ellis.link/api/packages/continuwuation/rpm/repository.key
|
||||
EOF
|
||||
|
||||
sudo dnf install continuwuity
|
||||
|
||||
@@ -269,7 +269,7 @@ # If federation is enabled
|
||||
```
|
||||
|
||||
- To check if your server can communicate with other homeservers, use the
|
||||
[Matrix Federation Tester](https://federationtester.matrix.org/). If you can
|
||||
[Matrix Federation Tester](https://federationtester.mtrnord.blog/). If you can
|
||||
register but cannot join federated rooms, check your configuration and verify
|
||||
that port 8448 is open and forwarded correctly.
|
||||
|
||||
|
||||
@@ -19,6 +19,16 @@
|
||||
src: /assets/logo.svg
|
||||
alt: continuwuity logo
|
||||
|
||||
beforeFeatures:
|
||||
- title: Matrix for Discord users
|
||||
details: New to Matrix? Learn how Matrix compares to Discord
|
||||
link: https://joinmatrix.org/guide/matrix-vs-discord/
|
||||
buttonText: Find Out the Difference
|
||||
- title: How Matrix Works
|
||||
details: Learn how Matrix works under the hood, and what that means
|
||||
link: https://matrix.org/docs/matrix-concepts/elements-of-matrix/
|
||||
buttonText: Read the Guide
|
||||
|
||||
features:
|
||||
- title: 🚀 High Performance
|
||||
details: Built with Rust for exceptional speed and efficiency. Designed to run smoothly even on modest hardware.
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"mention_room": true,
|
||||
"date": "2025-12-30",
|
||||
"message": "Continuwuity v0.5.1 has been released. **The release contains a fix for the critical vulnerability [GHSA-m5p2-vccg-8c9v](https://github.com/continuwuity/continuwuity/security/advisories/GHSA-m5p2-vccg-8c9v) (embargoed) affecting all Conduit-derived servers. Update as soon as possible.**\n\nThis has been *actively exploited* to attempt account takeover and forge events bricking the Continuwuity rooms. The new space is accessible at [Continuwuity (room list)](https://matrix.to/#/!8cR4g-i9ucof69E4JHNg9LbPVkGprHb3SzcrGBDDJgk?via=continuwuity.org&via=starstruck.systems&via=gingershaped.computer)\n"
|
||||
"id": 9,
|
||||
"mention_room": false,
|
||||
"date": "2026-02-09",
|
||||
"message": "Yesterday we released [v0.5.4](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.4). Bugfixes, performance improvements and more moderation features! There's also a security fix, so please update as soon as possible. Don't forget to join [our announcements channel](https://matrix.to/#/!jIdNjSM5X-V5JVx2h2kAhUZIIQ08GyzPL55NFZAH1vM/%2489TY9CqRg4-ff1MGo3Ulc5r5X4pakfdzT-99RD8Docc?via=ellis.link&via=explodie.org&via=matrix.org) to get important information sooner <3 "
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -118,10 +118,6 @@ ## `!admin debug time`
|
||||
|
||||
Print the current time
|
||||
|
||||
## `!admin debug list-dependencies`
|
||||
|
||||
List dependencies
|
||||
|
||||
## `!admin debug database-stats`
|
||||
|
||||
Get database statistics
|
||||
|
||||
@@ -112,6 +112,19 @@ ### `!admin query resolver overrides-cache`
|
||||
|
||||
Query the overrides cache
|
||||
|
||||
### `!admin query resolver flush-cache`
|
||||
|
||||
Flush a given server from the resolver caches or flush them completely
|
||||
|
||||
* Examples:
|
||||
* Flush a specific server:
|
||||
|
||||
`!admin query resolver flush-cache matrix.example.com`
|
||||
|
||||
* Flush all resolver caches completely:
|
||||
|
||||
`!admin query resolver flush-cache --all`
|
||||
|
||||
## `!admin query pusher`
|
||||
|
||||
pusher service
|
||||
|
||||
@@ -16,10 +16,6 @@ ## `!admin server reload-config`
|
||||
|
||||
Reload configuration values
|
||||
|
||||
## `!admin server list-features`
|
||||
|
||||
List the features built into the server
|
||||
|
||||
## `!admin server memory-usage`
|
||||
|
||||
Print database memory usage statistics
|
||||
|
||||
@@ -20,6 +20,16 @@ ### Lost access to admin room
|
||||
|
||||
## General potential issues
|
||||
|
||||
### Configuration not working as expected
|
||||
|
||||
Sometimes you can make a mistake in your configuration that
|
||||
means things don't get passed to Continuwuity correctly.
|
||||
This is particularly easy to do with environment variables.
|
||||
To check what configuration Continuwuity actually sees, you can
|
||||
use the `!admin server show-config` command in your admin room.
|
||||
Beware that this prints out any secrets in your configuration,
|
||||
so you might want to delete the result afterwards!
|
||||
|
||||
### Potential DNS issues when using Docker
|
||||
|
||||
Docker's DNS setup for containers in a non-default network intercepts queries to
|
||||
@@ -139,7 +149,7 @@ ### Database corruption
|
||||
|
||||
## Debugging
|
||||
|
||||
Note that users should not really be debugging things. If you find yourself
|
||||
Note that users should not really need to debug things. If you find yourself
|
||||
debugging and find the issue, please let us know and/or how we can fix it.
|
||||
Various debug commands can be found in `!admin debug`.
|
||||
|
||||
@@ -178,6 +188,31 @@ ### Pinging servers
|
||||
and simply fetches a string on a static JSON endpoint. It is very low cost both
|
||||
bandwidth and computationally.
|
||||
|
||||
### Enabling backtraces for errors
|
||||
|
||||
Continuwuity can capture backtraces (stack traces) for errors to help diagnose
|
||||
issues. Backtraces show the exact sequence of function calls that led to an
|
||||
error, which is invaluable for debugging.
|
||||
|
||||
To enable backtraces, set the `RUST_BACKTRACE` environment variable before starting Continuwuity:
|
||||
|
||||
```bash
|
||||
# For both panics and errors
|
||||
RUST_BACKTRACE=1 ./conduwuit
|
||||
|
||||
```
|
||||
|
||||
For systemd deployments, add this to your service file:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
Environment="RUST_BACKTRACE=1"
|
||||
```
|
||||
|
||||
Backtrace capture has a performance cost. Avoid leaving it on.
|
||||
You can also enable it only for panics by setting
|
||||
`RUST_BACKTRACE=1` and `RUST_LIB_BACKTRACE=0`.
|
||||
|
||||
### Allocator memory stats
|
||||
|
||||
When using jemalloc with jemallocator's `stats` feature (`--enable-stats`), you
|
||||
|
||||
@@ -20,7 +20,7 @@ rec {
|
||||
# we need to keep the `web` directory which would be filtered out by the regular source filtering function
|
||||
#
|
||||
# https://crane.dev/API.html#cranelibcleancargosource
|
||||
isWebTemplate = path: _type: builtins.match ".*src/web.*" path != null;
|
||||
isWebTemplate = path: _type: builtins.match ".*(src/(web|service)|docs).*" path != null;
|
||||
isRust = craneLib.filterCargoSources;
|
||||
isNix = path: _type: builtins.match ".+/nix.*" path != null;
|
||||
webOrRustNotNix = p: t: !(isNix p t) && (isWebTemplate p t || isRust p t);
|
||||
|
||||
1854
package-lock.json
generated
1854
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,10 +22,9 @@
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"devDependencies": {
|
||||
"@rspress/core": "^2.0.0-rc.1",
|
||||
"@rspress/plugin-client-redirects": "^2.0.0-alpha.12",
|
||||
"@rspress/plugin-preview": "^2.0.0-beta.35",
|
||||
"@rspress/plugin-sitemap": "^2.0.0-beta.23",
|
||||
"@rspress/core": "^2.0.0",
|
||||
"@rspress/plugin-client-redirects": "^2.0.0",
|
||||
"@rspress/plugin-sitemap": "^2.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { defineConfig } from '@rspress/core';
|
||||
import { pluginPreview } from '@rspress/plugin-preview';
|
||||
import { pluginSitemap } from '@rspress/plugin-sitemap';
|
||||
import { pluginClientRedirects } from '@rspress/plugin-client-redirects';
|
||||
|
||||
@@ -41,7 +40,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
|
||||
plugins: [pluginPreview(), pluginSitemap({
|
||||
plugins: [pluginSitemap({
|
||||
siteUrl: 'https://continuwuity.org', // TODO: Set automatically in build pipeline
|
||||
}),
|
||||
pluginClientRedirects({
|
||||
@@ -54,6 +53,9 @@ export default defineConfig({
|
||||
}, {
|
||||
from: '/server_reference',
|
||||
to: '/reference/server'
|
||||
}, {
|
||||
from: '/community$',
|
||||
to: '/community/guidelines'
|
||||
}
|
||||
]
|
||||
})],
|
||||
|
||||
@@ -87,7 +87,6 @@ serde-saphyr.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
ctor.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -819,32 +819,6 @@ pub(super) async fn time(&self) -> Result {
|
||||
self.write_str(&now).await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn list_dependencies(&self, names: bool) -> Result {
|
||||
if names {
|
||||
let out = info::cargo::dependencies_names().join(" ");
|
||||
return self.write_str(&out).await;
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
let deps = info::cargo::dependencies();
|
||||
writeln!(out, "| name | version | features |")?;
|
||||
writeln!(out, "| ---- | ------- | -------- |")?;
|
||||
for (name, dep) in deps {
|
||||
let version = dep.try_req().unwrap_or("*");
|
||||
let feats = dep.req_features();
|
||||
let feats = if !feats.is_empty() {
|
||||
feats.join(" ")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
writeln!(out, "| {name} | {version} | {feats} |")?;
|
||||
}
|
||||
|
||||
self.write_str(&out).await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn database_stats(
|
||||
&self,
|
||||
|
||||
@@ -206,12 +206,6 @@ pub enum DebugCommand {
|
||||
/// Print the current time
|
||||
Time,
|
||||
|
||||
/// List dependencies
|
||||
ListDependencies {
|
||||
#[arg(short, long)]
|
||||
names: bool,
|
||||
},
|
||||
|
||||
/// Get database statistics
|
||||
DatabaseStats {
|
||||
property: Option<String>,
|
||||
|
||||
@@ -30,11 +30,8 @@
|
||||
|
||||
pub(crate) const PAGE_SIZE: usize = 100;
|
||||
|
||||
use ctor::{ctor, dtor};
|
||||
|
||||
conduwuit::mod_ctor! {}
|
||||
conduwuit::mod_dtor! {}
|
||||
conduwuit::rustc_flags_capture! {}
|
||||
|
||||
pub use crate::admin::AdminCommand;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use clap::Subcommand;
|
||||
use conduwuit::{Result, utils::time};
|
||||
use conduwuit::{Err, Result, utils::time};
|
||||
use futures::StreamExt;
|
||||
use ruma::OwnedServerName;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#[admin_command_dispatch]
|
||||
#[derive(Debug, Subcommand)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
/// Resolver service and caches
|
||||
pub enum ResolverCommand {
|
||||
/// Query the destinations cache
|
||||
@@ -18,6 +19,14 @@ pub enum ResolverCommand {
|
||||
OverridesCache {
|
||||
name: Option<String>,
|
||||
},
|
||||
|
||||
/// Flush a specific server from the resolver caches or everything
|
||||
FlushCache {
|
||||
name: Option<OwnedServerName>,
|
||||
|
||||
#[arg(short, long)]
|
||||
all: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
@@ -69,3 +78,18 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn flush_cache(&self, name: Option<OwnedServerName>, all: bool) -> Result {
|
||||
if all {
|
||||
self.services.resolver.cache.clear().await;
|
||||
writeln!(self, "Resolver caches cleared!").await
|
||||
} else if let Some(name) = name {
|
||||
self.services.resolver.cache.del_destination(&name);
|
||||
self.services.resolver.cache.del_override(&name);
|
||||
self.write_str(&format!("Cleared {name} from resolver caches!"))
|
||||
.await
|
||||
} else {
|
||||
Err!("Missing name. Supply a name or use --all to flush the whole cache.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
|
||||
use crate::{PAGE_SIZE, admin_command, get_room_info};
|
||||
|
||||
#[allow(clippy::fn_params_excessive_bools)]
|
||||
#[admin_command]
|
||||
pub(super) async fn list_rooms(
|
||||
&self,
|
||||
page: Option<usize>,
|
||||
exclude_disabled: bool,
|
||||
exclude_banned: bool,
|
||||
include_empty: bool,
|
||||
no_details: bool,
|
||||
) -> Result {
|
||||
// TODO: i know there's a way to do this with clap, but i can't seem to find it
|
||||
@@ -28,6 +30,20 @@ pub(super) async fn list_rooms(
|
||||
.then_some(room_id)
|
||||
})
|
||||
.then(|room_id| get_room_info(self.services, room_id))
|
||||
.then(|(room_id, total_members, name)| async move {
|
||||
let local_members: Vec<_> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.active_local_users_in_room(&room_id)
|
||||
.collect()
|
||||
.await;
|
||||
let local_members = local_members.len();
|
||||
(room_id, total_members, local_members, name)
|
||||
})
|
||||
.filter_map(|(room_id, total_members, local_members, name)| async move {
|
||||
(include_empty || local_members > 0).then_some((room_id, total_members, name))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ pub enum RoomCommand {
|
||||
#[arg(long)]
|
||||
exclude_banned: bool,
|
||||
|
||||
/// Includes disconnected/empty rooms (rooms with zero members)
|
||||
#[arg(long)]
|
||||
include_empty: bool,
|
||||
|
||||
#[arg(long)]
|
||||
/// Whether to only output room IDs without supplementary room
|
||||
/// information
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{fmt::Write, path::PathBuf, sync::Arc};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use conduwuit::{
|
||||
Err, Result, info,
|
||||
Err, Result,
|
||||
utils::{stream::IterStream, time},
|
||||
warn,
|
||||
};
|
||||
@@ -59,34 +59,6 @@ pub(super) async fn reload_config(&self, path: Option<PathBuf>) -> Result {
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn list_features(&self, available: bool, enabled: bool, comma: bool) -> Result {
|
||||
let delim = if comma { "," } else { " " };
|
||||
if enabled && !available {
|
||||
let features = info::rustc::features().join(delim);
|
||||
let out = format!("`\n{features}\n`");
|
||||
return self.write_str(&out).await;
|
||||
}
|
||||
|
||||
if available && !enabled {
|
||||
let features = info::cargo::features().join(delim);
|
||||
let out = format!("`\n{features}\n`");
|
||||
return self.write_str(&out).await;
|
||||
}
|
||||
|
||||
let mut features = String::new();
|
||||
let enabled = info::rustc::features();
|
||||
let available = info::cargo::features();
|
||||
for feature in available {
|
||||
let active = enabled.contains(&feature.as_str());
|
||||
let emoji = if active { "✅" } else { "❌" };
|
||||
let remark = if active { "[enabled]" } else { "" };
|
||||
writeln!(features, "{emoji} {feature} {remark}")?;
|
||||
}
|
||||
|
||||
self.write_str(&features).await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn memory_usage(&self) -> Result {
|
||||
let services_usage = self.services.memory_usage().await?;
|
||||
|
||||
@@ -21,18 +21,6 @@ pub enum ServerCommand {
|
||||
path: Option<PathBuf>,
|
||||
},
|
||||
|
||||
/// List the features built into the server
|
||||
ListFeatures {
|
||||
#[arg(short, long)]
|
||||
available: bool,
|
||||
|
||||
#[arg(short, long)]
|
||||
enabled: bool,
|
||||
|
||||
#[arg(short, long)]
|
||||
comma: bool,
|
||||
},
|
||||
|
||||
/// Print database memory usage statistics
|
||||
MemoryUsage,
|
||||
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
fmt::Write as _,
|
||||
};
|
||||
|
||||
use api::client::{
|
||||
full_user_deactivate, join_room_by_id_helper, leave_all_rooms, leave_room, remote_leave_room,
|
||||
update_avatar_url, update_displayname,
|
||||
};
|
||||
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room, remote_leave_room};
|
||||
use conduwuit::{
|
||||
Err, Result, debug, debug_warn, error, info, is_equal_to,
|
||||
Err, Result, debug_warn, error, info,
|
||||
matrix::{Event, pdu::PduBuilder},
|
||||
utils::{self, ReadyExt},
|
||||
warn,
|
||||
@@ -143,7 +140,6 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
|
||||
self.services.globals.server_name().to_owned(),
|
||||
room_server_name.to_owned(),
|
||||
],
|
||||
None,
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
@@ -171,27 +167,8 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
|
||||
|
||||
// we dont add a device since we're not the user, just the creator
|
||||
|
||||
// if this account creation is from the CLI / --execute, invite the first user
|
||||
// to admin room
|
||||
if let Ok(admin_room) = self.services.admin.get_admin_room().await {
|
||||
if self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(&admin_room)
|
||||
.await
|
||||
.is_ok_and(is_equal_to!(1))
|
||||
{
|
||||
self.services
|
||||
.admin
|
||||
.make_user_admin(&user_id)
|
||||
.boxed()
|
||||
.await?;
|
||||
warn!("Granting {user_id} admin privileges as the first user");
|
||||
}
|
||||
} else {
|
||||
debug!("create_user admin command called without an admin room being available");
|
||||
}
|
||||
// Make the first user to register an administrator and disable first-run mode.
|
||||
self.services.firstrun.empower_first_user(&user_id).await?;
|
||||
|
||||
self.write_str(&format!("Created user with user_id: {user_id} and password: `{password}`"))
|
||||
.await
|
||||
@@ -227,9 +204,6 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
|
||||
full_user_deactivate(self.services, &user_id, &all_joined_rooms)
|
||||
.boxed()
|
||||
.await?;
|
||||
update_displayname(self.services, &user_id, None, &all_joined_rooms).await;
|
||||
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms).await;
|
||||
leave_all_rooms(self.services, &user_id).await;
|
||||
}
|
||||
|
||||
self.write_str(&format!("User {user_id} has been deactivated"))
|
||||
@@ -406,10 +380,6 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
|
||||
full_user_deactivate(self.services, &user_id, &all_joined_rooms)
|
||||
.boxed()
|
||||
.await?;
|
||||
update_displayname(self.services, &user_id, None, &all_joined_rooms).await;
|
||||
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms)
|
||||
.await;
|
||||
leave_all_rooms(self.services, &user_id).await;
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -559,7 +529,6 @@ pub(super) async fn force_join_list_of_local_users(
|
||||
&room_id,
|
||||
Some(String::from(BULK_JOIN_REASON)),
|
||||
&servers,
|
||||
None,
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
@@ -645,7 +614,6 @@ pub(super) async fn force_join_all_local_users(
|
||||
&room_id,
|
||||
Some(String::from(BULK_JOIN_REASON)),
|
||||
&servers,
|
||||
None,
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
@@ -685,8 +653,7 @@ pub(super) async fn force_join_room(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers, None, &None)
|
||||
.await?;
|
||||
join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers, &None).await?;
|
||||
|
||||
self.write_str(&format!("{user_id} has been joined to {room_id}.",))
|
||||
.await
|
||||
|
||||
@@ -91,7 +91,6 @@ serde.workspace = true
|
||||
sha1.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
ctor.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
1
src/api/admin/mod.rs
Normal file
1
src/api/admin/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod rooms;
|
||||
132
src/api/admin/rooms/ban.rs
Normal file
132
src/api/admin/rooms/ban.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
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 crate::{Ruma, client::leave_room};
|
||||
|
||||
/// # `PUT /_continuwuity/admin/rooms/{roomID}/ban`
|
||||
///
|
||||
/// Bans or unbans a room.
|
||||
pub(crate) async fn ban_room(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<rooms::ban::v1::Request>,
|
||||
) -> Result<rooms::ban::v1::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
if !services.users.is_admin(sender_user).await {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
if body.banned {
|
||||
// Don't ban again if already banned
|
||||
if services.rooms.metadata.is_banned(&body.room_id).await {
|
||||
return Err!(Request(InvalidParam("Room is already banned")));
|
||||
}
|
||||
info!(%sender_user, "Banning room {}", body.room_id);
|
||||
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("{sender_user} banned {} (ban in progress)", body.room_id))
|
||||
.await;
|
||||
|
||||
let mut users = services
|
||||
.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();
|
||||
let mut failed_evicted = Vec::new();
|
||||
|
||||
while let Some(ref user_id) = users.next().await {
|
||||
info!("Evicting user {} from room {}", user_id, body.room_id);
|
||||
match leave_room(&services, user_id, &body.room_id, None)
|
||||
.boxed()
|
||||
.await
|
||||
{
|
||||
| Ok(()) => {
|
||||
services.rooms.state_cache.forget(&body.room_id, user_id);
|
||||
evicted.push(user_id.clone());
|
||||
},
|
||||
| Err(e) => {
|
||||
warn!("Failed to evict user {} from room {}: {}", user_id, body.room_id, e);
|
||||
failed_evicted.push(user_id.clone());
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let aliases: Vec<OwnedRoomAliasId> = services
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
for alias in &aliases {
|
||||
info!("Removing alias {} for banned room {}", alias, body.room_id);
|
||||
services
|
||||
.rooms
|
||||
.alias
|
||||
.remove_alias(alias, &services.globals.server_user)
|
||||
.await?;
|
||||
}
|
||||
|
||||
services.rooms.directory.set_not_public(&body.room_id); // remove from the room directory
|
||||
services.rooms.metadata.ban_room(&body.room_id, true); // prevent further joins
|
||||
services.rooms.metadata.disable_room(&body.room_id, true); // disable federation
|
||||
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Finished banning {}: Removed {} users ({} failed) and {} aliases",
|
||||
body.room_id,
|
||||
evicted.len(),
|
||||
failed_evicted.len(),
|
||||
aliases.len()
|
||||
))
|
||||
.await;
|
||||
if !evicted.is_empty() || !failed_evicted.is_empty() || !aliases.is_empty() {
|
||||
let msg = services
|
||||
.admin
|
||||
.text_or_file(RoomMessageEventContent::text_markdown(format!(
|
||||
"Removed users:\n{}\n\nFailed to remove users:\n{}\n\nRemoved aliases: {}",
|
||||
evicted
|
||||
.iter()
|
||||
.map(|u| u.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
failed_evicted
|
||||
.iter()
|
||||
.map(|u| u.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
aliases
|
||||
.iter()
|
||||
.map(|a| a.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
)))
|
||||
.await;
|
||||
services.admin.send_message(msg).await.ok();
|
||||
}
|
||||
|
||||
Ok(rooms::ban::v1::Response::new(evicted, failed_evicted, aliases))
|
||||
} else {
|
||||
// Don't unban if not banned
|
||||
if !services.rooms.metadata.is_banned(&body.room_id).await {
|
||||
return Err!(Request(InvalidParam("Room is not banned")));
|
||||
}
|
||||
info!(%sender_user, "Unbanning room {}", body.room_id);
|
||||
services.rooms.metadata.disable_room(&body.room_id, false);
|
||||
services.rooms.metadata.ban_room(&body.room_id, false);
|
||||
services
|
||||
.admin
|
||||
.notice(&format!("{sender_user} unbanned {}", body.room_id))
|
||||
.await;
|
||||
Ok(rooms::ban::v1::Response::new(Vec::new(), Vec::new(), Vec::new()))
|
||||
}
|
||||
}
|
||||
35
src/api/admin/rooms/list.rs
Normal file
35
src/api/admin/rooms/list.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use futures::StreamExt;
|
||||
use ruma::{OwnedRoomId, continuwuity_admin_api::rooms};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_continuwuity/admin/rooms/list`
|
||||
///
|
||||
/// Lists all rooms known to this server, excluding banned ones.
|
||||
pub(crate) async fn list_rooms(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<rooms::list::v1::Request>,
|
||||
) -> Result<rooms::list::v1::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
if !services.users.is_admin(sender_user).await {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
let mut rooms: Vec<OwnedRoomId> = services
|
||||
.rooms
|
||||
.metadata
|
||||
.iter_ids()
|
||||
.filter_map(|room_id| async move {
|
||||
if !services.rooms.metadata.is_banned(room_id).await {
|
||||
Some(room_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
rooms.sort();
|
||||
Ok(rooms::list::v1::Response::new(rooms))
|
||||
}
|
||||
2
src/api/admin/rooms/mod.rs
Normal file
2
src/api/admin/rooms/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod ban;
|
||||
pub mod list;
|
||||
@@ -3,7 +3,7 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Error, Event, Result, debug_info, err, error, info, is_equal_to,
|
||||
Err, Error, Event, Result, debug_info, err, error, info,
|
||||
matrix::pdu::PduBuilder,
|
||||
utils::{self, ReadyExt, stream::BroadbandExt},
|
||||
warn,
|
||||
@@ -26,6 +26,7 @@
|
||||
events::{
|
||||
GlobalAccountDataEventType, StateEventType,
|
||||
room::{
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
message::RoomMessageEventContent,
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
},
|
||||
@@ -147,7 +148,12 @@ pub(crate) async fn register_route(
|
||||
let is_guest = body.kind == RegistrationKind::Guest;
|
||||
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||
|
||||
if !services.config.allow_registration && body.appservice_info.is_none() {
|
||||
// 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!(
|
||||
@@ -184,17 +190,10 @@ pub(crate) async fn register_route(
|
||||
)));
|
||||
}
|
||||
|
||||
if is_guest
|
||||
&& (!services.config.allow_guest_registration
|
||||
|| (services.config.allow_registration
|
||||
&& services
|
||||
.registration_tokens
|
||||
.get_config_file_token()
|
||||
.is_some()))
|
||||
{
|
||||
if is_guest && !services.config.allow_guest_registration {
|
||||
info!(
|
||||
"Guest registration disabled / registration enabled with token configured, \
|
||||
rejecting guest registration attempt, initial device name: \"{}\"",
|
||||
"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.")));
|
||||
@@ -308,54 +307,63 @@ pub(crate) async fn register_route(
|
||||
let skip_auth = body.appservice_info.is_some() || is_guest;
|
||||
|
||||
// Populate required UIAA flows
|
||||
if services
|
||||
.registration_tokens
|
||||
.iterate_tokens()
|
||||
.next()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
// Registration token required
|
||||
|
||||
if services.firstrun.is_first_run() {
|
||||
// Registration token forced while in first-run mode
|
||||
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
|
||||
} else {
|
||||
if services
|
||||
.registration_tokens
|
||||
.iterate_tokens()
|
||||
.next()
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
// Registration token required
|
||||
uiaainfo.flows.push(AuthFlow {
|
||||
stages: vec![AuthType::RegistrationToken],
|
||||
});
|
||||
}
|
||||
|
||||
// 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 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 {
|
||||
@@ -513,39 +521,29 @@ pub(crate) async fn register_route(
|
||||
}
|
||||
}
|
||||
|
||||
// If this is the first real user, grant them admin privileges except for guest
|
||||
// users
|
||||
// Note: the server user is generated first
|
||||
if !is_guest {
|
||||
if let Ok(admin_room) = services.admin.get_admin_room().await {
|
||||
if services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(&admin_room)
|
||||
.await
|
||||
.is_ok_and(is_equal_to!(1))
|
||||
{
|
||||
services.admin.make_user_admin(&user_id).boxed().await?;
|
||||
warn!("Granting {user_id} admin privileges as the first user");
|
||||
} else if services.config.suspend_on_register {
|
||||
// This is not an admin, suspend them.
|
||||
// Note that we can still do auto joins for suspended users
|
||||
// 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
|
||||
.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();
|
||||
}
|
||||
.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -582,7 +580,6 @@ pub(crate) async fn register_route(
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||
None,
|
||||
&body.appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
@@ -815,9 +812,6 @@ pub(crate) async fn deactivate_route(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
super::update_displayname(&services, sender_user, None, &all_joined_rooms).await;
|
||||
super::update_avatar_url(&services, sender_user, None, None, &all_joined_rooms).await;
|
||||
|
||||
full_user_deactivate(&services, sender_user, &all_joined_rooms)
|
||||
.boxed()
|
||||
.await?;
|
||||
@@ -907,9 +901,6 @@ pub async fn full_user_deactivate(
|
||||
) -> Result<()> {
|
||||
services.users.deactivate_account(user_id).await.ok();
|
||||
|
||||
super::update_displayname(services, user_id, None, all_joined_rooms).await;
|
||||
super::update_avatar_url(services, user_id, None, None, all_joined_rooms).await;
|
||||
|
||||
services
|
||||
.users
|
||||
.all_profile_keys(user_id)
|
||||
@@ -918,9 +909,11 @@ pub async fn full_user_deactivate(
|
||||
})
|
||||
.await;
|
||||
|
||||
for room_id in all_joined_rooms {
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).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
|
||||
@@ -948,30 +941,33 @@ pub async fn full_user_deactivate(
|
||||
if user_can_demote_self {
|
||||
let mut power_levels_content = room_power_levels.unwrap_or_default();
|
||||
power_levels_content.users.remove(user_id);
|
||||
|
||||
// ignore errors so deactivation doesn't fail
|
||||
match services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(String::new(), &power_levels_content),
|
||||
user_id,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await
|
||||
{
|
||||
| Err(e) => {
|
||||
warn!(%room_id, %user_id, "Failed to demote user's own power level: {e}");
|
||||
},
|
||||
| _ => {
|
||||
info!("Demoted {user_id} in {room_id} as part of account deactivation");
|
||||
},
|
||||
}
|
||||
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::leave_all_rooms(services, user_id).boxed().await;
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
use crate::{
|
||||
Ruma,
|
||||
client::message::{event_filter, ignored_filter, lazy_loading_witness, visibility_filter},
|
||||
client::{
|
||||
is_ignored_pdu,
|
||||
message::{event_filter, ignored_filter, lazy_loading_witness, visibility_filter},
|
||||
},
|
||||
};
|
||||
|
||||
const LIMIT_MAX: usize = 100;
|
||||
@@ -78,6 +81,9 @@ pub(crate) async fn get_context_route(
|
||||
return Err!(Request(NotFound("Event not found.")));
|
||||
}
|
||||
|
||||
// Return M_SENDER_IGNORED if the sender of base_event is ignored (MSC4406)
|
||||
is_ignored_pdu(&services, &base_pdu, sender_user).await?;
|
||||
|
||||
let base_count = base_id.pdu_count();
|
||||
|
||||
let base_event = ignored_filter(&services, (base_count, base_pdu), sender_user);
|
||||
|
||||
@@ -91,8 +91,11 @@ pub(crate) async fn upload_keys_route(
|
||||
.users
|
||||
.get_device_keys(sender_user, sender_device)
|
||||
.await
|
||||
.and_then(|keys| keys.deserialize().map_err(Into::into))
|
||||
{
|
||||
if existing_keys.json().get() == device_keys.json().get() {
|
||||
// NOTE: also serves as a workaround for a nheko bug which omits cross-signing
|
||||
// NOTE: signatures when re-uploading the same DeviceKeys.
|
||||
if existing_keys.keys == deser_device_keys.keys {
|
||||
debug!(
|
||||
%sender_user,
|
||||
%sender_device,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, debug, debug_info, debug_warn, err, error, info,
|
||||
Err, Result, debug, debug_info, debug_warn, err, error, info, is_true,
|
||||
matrix::{
|
||||
StateKey,
|
||||
event::{gen_event_id, gen_event_id_canonical_json},
|
||||
@@ -15,6 +15,7 @@
|
||||
utils::{
|
||||
self, shuffle,
|
||||
stream::{IterStream, ReadyExt},
|
||||
to_canonical_object,
|
||||
},
|
||||
warn,
|
||||
};
|
||||
@@ -25,7 +26,7 @@
|
||||
api::{
|
||||
client::{
|
||||
error::ErrorKind,
|
||||
membership::{ThirdPartySigned, join_room_by_id, join_room_by_id_or_alias},
|
||||
membership::{join_room_by_id, join_room_by_id_or_alias},
|
||||
},
|
||||
federation::{self},
|
||||
},
|
||||
@@ -33,7 +34,7 @@
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
join_rules::{AllowRule, JoinRule},
|
||||
join_rules::JoinRule,
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
},
|
||||
@@ -47,9 +48,13 @@
|
||||
timeline::pdu_fits,
|
||||
},
|
||||
};
|
||||
use tokio::join;
|
||||
|
||||
use super::{banned_room_check, validate_remote_member_event_stub};
|
||||
use crate::Ruma;
|
||||
use crate::{
|
||||
Ruma,
|
||||
server::{select_authorising_user, user_can_perform_restricted_join},
|
||||
};
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
|
||||
///
|
||||
@@ -115,7 +120,6 @@ pub(crate) async fn join_room_by_id_route(
|
||||
&body.room_id,
|
||||
body.reason.clone(),
|
||||
&servers,
|
||||
body.third_party_signed.as_ref(),
|
||||
&body.appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
@@ -247,7 +251,6 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||
&room_id,
|
||||
body.reason.clone(),
|
||||
&servers,
|
||||
body.third_party_signed.as_ref(),
|
||||
appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
@@ -262,7 +265,6 @@ pub async fn join_room_by_id_helper(
|
||||
room_id: &RoomId,
|
||||
reason: Option<String>,
|
||||
servers: &[OwnedServerName],
|
||||
third_party_signed: Option<&ThirdPartySigned>,
|
||||
appservice_info: &Option<RegistrationInfo>,
|
||||
) -> Result<join_room_by_id::v3::Response> {
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).await;
|
||||
@@ -350,17 +352,9 @@ pub async fn join_room_by_id_helper(
|
||||
}
|
||||
|
||||
if server_in_room {
|
||||
join_room_by_id_helper_local(
|
||||
services,
|
||||
sender_user,
|
||||
room_id,
|
||||
reason,
|
||||
servers,
|
||||
third_party_signed,
|
||||
state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
join_room_by_id_helper_local(services, sender_user, room_id, reason, servers, state_lock)
|
||||
.boxed()
|
||||
.await?;
|
||||
} else {
|
||||
// Ask a remote server if we are not participating in this room
|
||||
join_room_by_id_helper_remote(
|
||||
@@ -369,7 +363,6 @@ pub async fn join_room_by_id_helper(
|
||||
room_id,
|
||||
reason,
|
||||
servers,
|
||||
third_party_signed,
|
||||
state_lock,
|
||||
)
|
||||
.boxed()
|
||||
@@ -385,7 +378,6 @@ async fn join_room_by_id_helper_remote(
|
||||
room_id: &RoomId,
|
||||
reason: Option<String>,
|
||||
servers: &[OwnedServerName],
|
||||
_third_party_signed: Option<&ThirdPartySigned>,
|
||||
state_lock: RoomMutexGuard,
|
||||
) -> Result {
|
||||
info!("Joining {room_id} over federation.");
|
||||
@@ -395,11 +387,10 @@ async fn join_room_by_id_helper_remote(
|
||||
|
||||
info!("make_join finished");
|
||||
|
||||
let Some(room_version_id) = make_join_response.room_version else {
|
||||
return Err!(BadServerResponse("Remote room version is not supported by conduwuit"));
|
||||
};
|
||||
let room_version_id = make_join_response.room_version.unwrap_or(RoomVersionId::V1);
|
||||
|
||||
if !services.server.supported_room_version(&room_version_id) {
|
||||
// How did we get here?
|
||||
return Err!(BadServerResponse(
|
||||
"Remote room version {room_version_id} is not supported by conduwuit"
|
||||
));
|
||||
@@ -428,10 +419,6 @@ async fn join_room_by_id_helper_remote(
|
||||
}
|
||||
};
|
||||
|
||||
join_event_stub.insert(
|
||||
"origin".to_owned(),
|
||||
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
|
||||
);
|
||||
join_event_stub.insert(
|
||||
"origin_server_ts".to_owned(),
|
||||
CanonicalJsonValue::Integer(
|
||||
@@ -743,87 +730,45 @@ async fn join_room_by_id_helper_local(
|
||||
room_id: &RoomId,
|
||||
reason: Option<String>,
|
||||
servers: &[OwnedServerName],
|
||||
_third_party_signed: Option<&ThirdPartySigned>,
|
||||
state_lock: RoomMutexGuard,
|
||||
) -> Result {
|
||||
debug_info!("We can join locally");
|
||||
let join_rules = services.rooms.state_accessor.get_join_rules(room_id).await;
|
||||
info!("Joining room locally");
|
||||
|
||||
let mut restricted_join_authorized = None;
|
||||
match join_rules {
|
||||
| JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted) => {
|
||||
for restriction in restricted.allow {
|
||||
match restriction {
|
||||
| AllowRule::RoomMembership(membership) => {
|
||||
if services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(sender_user, &membership.room_id)
|
||||
.await
|
||||
{
|
||||
restricted_join_authorized = Some(true);
|
||||
break;
|
||||
}
|
||||
},
|
||||
| AllowRule::UnstableSpamChecker => {
|
||||
match services
|
||||
.antispam
|
||||
.meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned())
|
||||
.await
|
||||
{
|
||||
| Ok(()) => {
|
||||
restricted_join_authorized = Some(true);
|
||||
break;
|
||||
},
|
||||
| Err(_) =>
|
||||
return Err!(Request(Forbidden(
|
||||
"Antispam rejected join request."
|
||||
))),
|
||||
}
|
||||
},
|
||||
| _ => {},
|
||||
}
|
||||
let (room_version, join_rules, is_invited) = join!(
|
||||
services.rooms.state.get_room_version(room_id),
|
||||
services.rooms.state_accessor.get_join_rules(room_id),
|
||||
services.rooms.state_cache.is_invited(sender_user, room_id)
|
||||
);
|
||||
|
||||
let room_version = room_version?;
|
||||
let mut auth_user: Option<OwnedUserId> = None;
|
||||
if !is_invited && matches!(join_rules, JoinRule::Restricted(_) | JoinRule::KnockRestricted(_))
|
||||
{
|
||||
use RoomVersionId::*;
|
||||
if !matches!(room_version, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
|
||||
// This is a restricted room, check if we can complete the join requirements
|
||||
// locally.
|
||||
let needs_auth_user =
|
||||
user_can_perform_restricted_join(services, sender_user, room_id, &room_version)
|
||||
.await;
|
||||
if needs_auth_user.is_ok_and(is_true!()) {
|
||||
// If there was an error or the value is false, we'll try joining over
|
||||
// federation. Since it's Ok(true), we can authorise this locally.
|
||||
// If we can't select a local user, this will remain None, the join will fail,
|
||||
// and we'll fall back to federation.
|
||||
auth_user = select_authorising_user(services, room_id, sender_user, &state_lock)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
| _ => {},
|
||||
}
|
||||
let join_authorized_via_users_server = if restricted_join_authorized.is_none() {
|
||||
None
|
||||
} else {
|
||||
match restricted_join_authorized.unwrap() {
|
||||
| true => services
|
||||
.rooms
|
||||
.state_cache
|
||||
.local_users_in_room(room_id)
|
||||
.filter(|user| {
|
||||
trace!("Checking if {user} can invite {sender_user} to {room_id}");
|
||||
services.rooms.state_accessor.user_can_invite(
|
||||
room_id,
|
||||
user,
|
||||
sender_user,
|
||||
&state_lock,
|
||||
)
|
||||
})
|
||||
.boxed()
|
||||
.next()
|
||||
.await
|
||||
.map(ToOwned::to_owned),
|
||||
| false => {
|
||||
warn!(
|
||||
"Join authorization failed for restricted join in room {room_id} for user \
|
||||
{sender_user}"
|
||||
);
|
||||
return Err!(Request(Forbidden("You are not authorized to join this room.")));
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
join_authorized_via_users_server: auth_user,
|
||||
..RoomMemberEventContent::new(MembershipState::Join)
|
||||
};
|
||||
|
||||
@@ -839,6 +784,7 @@ async fn join_room_by_id_helper_local(
|
||||
)
|
||||
.await
|
||||
else {
|
||||
info!("Joined room locally");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -846,138 +792,13 @@ async fn join_room_by_id_helper_local(
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
warn!(
|
||||
info!(
|
||||
?error,
|
||||
servers = %servers.len(),
|
||||
"Could not join restricted room locally, attempting remote join",
|
||||
remote_servers = %servers.len(),
|
||||
"Could not join room locally, attempting remote join",
|
||||
);
|
||||
let Ok((make_join_response, remote_server)) =
|
||||
make_join_request(services, sender_user, room_id, servers).await
|
||||
else {
|
||||
return Err(error);
|
||||
};
|
||||
|
||||
let Some(room_version_id) = make_join_response.room_version else {
|
||||
return Err!(BadServerResponse("Remote room version is not supported by conduwuit"));
|
||||
};
|
||||
|
||||
if !services.server.supported_room_version(&room_version_id) {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote room version {room_version_id} is not supported by conduwuit"
|
||||
));
|
||||
}
|
||||
|
||||
let mut join_event_stub: CanonicalJsonObject =
|
||||
serde_json::from_str(make_join_response.event.get()).map_err(|e| {
|
||||
err!(BadServerResponse("Invalid make_join event json received from server: {e:?}"))
|
||||
})?;
|
||||
|
||||
validate_remote_member_event_stub(
|
||||
&MembershipState::Join,
|
||||
sender_user,
|
||||
room_id,
|
||||
&join_event_stub,
|
||||
)?;
|
||||
|
||||
let join_authorized_via_users_server = join_event_stub
|
||||
.get("content")
|
||||
.map(|s| {
|
||||
s.as_object()?
|
||||
.get("join_authorised_via_users_server")?
|
||||
.as_str()
|
||||
})
|
||||
.and_then(|s| OwnedUserId::try_from(s.unwrap_or_default()).ok());
|
||||
|
||||
join_event_stub.insert(
|
||||
"origin".to_owned(),
|
||||
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
|
||||
);
|
||||
join_event_stub.insert(
|
||||
"origin_server_ts".to_owned(),
|
||||
CanonicalJsonValue::Integer(
|
||||
utils::millis_since_unix_epoch()
|
||||
.try_into()
|
||||
.expect("Timestamp is valid js_int value"),
|
||||
),
|
||||
);
|
||||
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,
|
||||
..RoomMemberEventContent::new(MembershipState::Join)
|
||||
})
|
||||
.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");
|
||||
},
|
||||
}
|
||||
|
||||
// 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)?;
|
||||
|
||||
// Generate event id
|
||||
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
|
||||
|
||||
// Add event_id back
|
||||
join_event_stub
|
||||
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
|
||||
|
||||
// It has enough fields to be called a proper event now
|
||||
let join_event = join_event_stub;
|
||||
|
||||
let send_join_response = services
|
||||
.sending
|
||||
.send_synapse_request(
|
||||
&remote_server,
|
||||
federation::membership::create_join_event::v2::Request {
|
||||
room_id: room_id.to_owned(),
|
||||
event_id: event_id.clone(),
|
||||
omit_members: false,
|
||||
pdu: services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(join_event.clone())
|
||||
.await,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(signed_raw) = send_join_response.room_state.event {
|
||||
let (signed_event_id, signed_value) =
|
||||
gen_event_id_canonical_json(&signed_raw, &room_version_id).map_err(|e| {
|
||||
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
|
||||
})?;
|
||||
|
||||
if signed_event_id != event_id {
|
||||
return Err!(Request(BadJson(
|
||||
warn!(%signed_event_id, %event_id, "Server {remote_server} sent event with wrong event ID")
|
||||
)));
|
||||
}
|
||||
|
||||
drop(state_lock);
|
||||
services
|
||||
.rooms
|
||||
.event_handler
|
||||
.handle_incoming_pdu(&remote_server, room_id, &signed_event_id, signed_value, true)
|
||||
.boxed()
|
||||
.await?;
|
||||
} else {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
join_room_by_id_helper_remote(services, sender_user, room_id, reason, servers, state_lock)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn make_join_request(
|
||||
@@ -986,17 +807,16 @@ async fn make_join_request(
|
||||
room_id: &RoomId,
|
||||
servers: &[OwnedServerName],
|
||||
) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> {
|
||||
let mut make_join_response_and_server =
|
||||
Err!(BadServerResponse("No server available to assist in joining."));
|
||||
|
||||
let mut make_join_counter: usize = 0;
|
||||
let mut incompatible_room_version_count: usize = 0;
|
||||
let mut make_join_counter: usize = 1;
|
||||
|
||||
for remote_server in servers {
|
||||
if services.globals.server_is_ours(remote_server) {
|
||||
continue;
|
||||
}
|
||||
info!("Asking {remote_server} for make_join ({make_join_counter})");
|
||||
info!(
|
||||
"Asking {remote_server} for make_join (attempt {make_join_counter}/{})",
|
||||
servers.len()
|
||||
);
|
||||
let make_join_response = services
|
||||
.sending
|
||||
.send_federation_request(
|
||||
@@ -1012,44 +832,56 @@ async fn make_join_request(
|
||||
trace!("make_join response: {:?}", make_join_response);
|
||||
make_join_counter = make_join_counter.saturating_add(1);
|
||||
|
||||
if let Err(ref e) = make_join_response {
|
||||
if matches!(
|
||||
e.kind(),
|
||||
ErrorKind::IncompatibleRoomVersion { .. } | ErrorKind::UnsupportedRoomVersion
|
||||
) {
|
||||
incompatible_room_version_count =
|
||||
incompatible_room_version_count.saturating_add(1);
|
||||
}
|
||||
|
||||
if incompatible_room_version_count > 15 {
|
||||
info!(
|
||||
"15 servers have responded with M_INCOMPATIBLE_ROOM_VERSION or \
|
||||
M_UNSUPPORTED_ROOM_VERSION, assuming that conduwuit does not support the \
|
||||
room version {room_id}: {e}"
|
||||
);
|
||||
make_join_response_and_server =
|
||||
Err!(BadServerResponse("Room version is not supported by Conduwuit"));
|
||||
return make_join_response_and_server;
|
||||
}
|
||||
|
||||
if make_join_counter > 40 {
|
||||
warn!(
|
||||
"40 servers failed to provide valid make_join response, assuming no server \
|
||||
can assist in joining."
|
||||
);
|
||||
make_join_response_and_server =
|
||||
Err!(BadServerResponse("No server available to assist in joining."));
|
||||
|
||||
return make_join_response_and_server;
|
||||
}
|
||||
}
|
||||
|
||||
make_join_response_and_server = make_join_response.map(|r| (r, remote_server.clone()));
|
||||
|
||||
if make_join_response_and_server.is_ok() {
|
||||
break;
|
||||
match make_join_response {
|
||||
| Ok(response) => {
|
||||
info!("Received make_join response from {remote_server}");
|
||||
if let Err(e) = validate_remote_member_event_stub(
|
||||
&MembershipState::Join,
|
||||
sender_user,
|
||||
room_id,
|
||||
&to_canonical_object(&response.event)?,
|
||||
) {
|
||||
warn!("make_join response from {remote_server} failed validation: {e}");
|
||||
continue;
|
||||
}
|
||||
return Ok((response, remote_server.clone()));
|
||||
},
|
||||
| Err(e) => match e.kind() {
|
||||
| ErrorKind::UnableToAuthorizeJoin => {
|
||||
info!(
|
||||
"{remote_server} was unable to verify the joining user satisfied \
|
||||
restricted join requirements: {e}. Will continue trying."
|
||||
);
|
||||
},
|
||||
| ErrorKind::UnableToGrantJoin => {
|
||||
info!(
|
||||
"{remote_server} believes the joining user satisfies restricted join \
|
||||
rules, but is unable to authorise a join for us. Will continue trying."
|
||||
);
|
||||
},
|
||||
| ErrorKind::IncompatibleRoomVersion { room_version } => {
|
||||
warn!(
|
||||
"{remote_server} reports the room we are trying to join is \
|
||||
v{room_version}, which we do not support."
|
||||
);
|
||||
return Err(e);
|
||||
},
|
||||
| ErrorKind::Forbidden { .. } => {
|
||||
warn!("{remote_server} refuses to let us join: {e}.");
|
||||
return Err(e);
|
||||
},
|
||||
| ErrorKind::NotFound => {
|
||||
info!(
|
||||
"{remote_server} does not know about {room_id}: {e}. Will continue \
|
||||
trying."
|
||||
);
|
||||
},
|
||||
| _ => {
|
||||
info!("{remote_server} failed to make_join: {e}. Will continue trying.");
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
make_join_response_and_server
|
||||
info!("All {} servers were unable to assist in joining {room_id} :(", servers.len());
|
||||
Err!(BadServerResponse("No server available to assist in joining."))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
result::FlatOk,
|
||||
trace,
|
||||
utils::{self, shuffle, stream::IterStream},
|
||||
utils::{self, shuffle, stream::IterStream, to_canonical_object},
|
||||
warn,
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
@@ -253,7 +253,6 @@ async fn knock_room_by_id_helper(
|
||||
room_id,
|
||||
reason.clone(),
|
||||
servers,
|
||||
None,
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
@@ -741,6 +740,17 @@ async fn make_knock_request(
|
||||
|
||||
trace!("make_knock response: {make_knock_response:?}");
|
||||
make_knock_counter = make_knock_counter.saturating_add(1);
|
||||
if let Ok(r) = &make_knock_response {
|
||||
if let Err(e) = validate_remote_member_event_stub(
|
||||
&MembershipState::Knock,
|
||||
sender_user,
|
||||
room_id,
|
||||
&to_canonical_object(&r.event)?,
|
||||
) {
|
||||
warn!("make_knock response from {remote_server} failed validation: {e}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
make_knock_response_and_server = make_knock_response.map(|r| (r, remote_server.clone()));
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ pub(crate) fn validate_remote_member_event_stub(
|
||||
};
|
||||
if event_membership != &membership.as_str() {
|
||||
return Err!(BadServerResponse(
|
||||
"Remote server returned member event with incorrect room_id"
|
||||
"Remote server returned member event with incorrect membership type"
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, at, debug_warn,
|
||||
Err, Error, Result, at, debug_warn,
|
||||
matrix::{
|
||||
event::{Event, Matches},
|
||||
pdu::PduCount,
|
||||
@@ -26,7 +26,7 @@
|
||||
DeviceId, RoomId, UserId,
|
||||
api::{
|
||||
Direction,
|
||||
client::{filter::RoomEventFilter, message::get_message_events},
|
||||
client::{error::ErrorKind, filter::RoomEventFilter, message::get_message_events},
|
||||
},
|
||||
events::{
|
||||
AnyStateEvent, StateEventType,
|
||||
@@ -279,23 +279,30 @@ pub(crate) async fn ignored_filter(
|
||||
|
||||
is_ignored_pdu(services, pdu, user_id)
|
||||
.await
|
||||
.unwrap_or(true)
|
||||
.eq(&false)
|
||||
.then_some(item)
|
||||
}
|
||||
|
||||
/// Determine whether a PDU should be ignored for a given recipient user.
|
||||
/// Returns True if this PDU should be ignored, returns False otherwise.
|
||||
///
|
||||
/// The error SenderIgnored is returned if the sender or the sender's server is
|
||||
/// ignored by the relevant user. If the error cannot be returned to the user,
|
||||
/// it should equate to a true value (i.e. ignored).
|
||||
#[inline]
|
||||
pub(crate) async fn is_ignored_pdu<Pdu>(
|
||||
services: &Services,
|
||||
event: &Pdu,
|
||||
recipient_user: &UserId,
|
||||
) -> bool
|
||||
) -> Result<bool>
|
||||
where
|
||||
Pdu: Event + Send + Sync,
|
||||
{
|
||||
// exclude Synapse's dummy events from bloating up response bodies. clients
|
||||
// don't need to see this.
|
||||
if event.kind().to_cow_str() == "org.matrix.dummy_event" {
|
||||
return true;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let sender_user = event.sender();
|
||||
@@ -310,21 +317,27 @@ pub(crate) async fn is_ignored_pdu<Pdu>(
|
||||
|
||||
if !type_ignored {
|
||||
// We cannot safely ignore this type
|
||||
return false;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if server_ignored {
|
||||
// the sender's server is ignored, so ignore this event
|
||||
return true;
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::SenderIgnored { sender: None },
|
||||
"The sender's server is ignored by this server.",
|
||||
));
|
||||
}
|
||||
|
||||
if user_ignored && !services.config.send_messages_from_ignored_users_to_client {
|
||||
// the recipient of this PDU has the sender ignored, and we're not
|
||||
// configured to send ignored messages to clients
|
||||
return true;
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::SenderIgnored { sender: Some(event.sender().to_owned()) },
|
||||
"You have ignored this sender.",
|
||||
));
|
||||
}
|
||||
|
||||
false
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -371,11 +384,3 @@ pub(crate) async fn is_ignored_invite(
|
||||
.invite_filter_level(&sender_user, recipient_user)
|
||||
.await == FilterLevel::Ignore
|
||||
}
|
||||
|
||||
#[cfg_attr(debug_assertions, ctor::ctor)]
|
||||
fn _is_sorted() {
|
||||
debug_assert!(
|
||||
IGNORED_MESSAGE_TYPES.is_sorted(),
|
||||
"IGNORED_MESSAGE_TYPES must be sorted by the developer"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result, at, debug_warn,
|
||||
Err, Result, at, debug_warn, err,
|
||||
matrix::{Event, event::RelationTypeEqual, pdu::PduCount},
|
||||
utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt},
|
||||
};
|
||||
@@ -18,7 +18,7 @@
|
||||
events::{TimelineEventType, relation::RelationType},
|
||||
};
|
||||
|
||||
use crate::Ruma;
|
||||
use crate::{Ruma, client::is_ignored_pdu};
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
|
||||
pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route(
|
||||
@@ -118,6 +118,14 @@ async fn paginate_relations_with_filter(
|
||||
debug_warn!(req_evt = %target, %room_id, "Event relations requested by {sender_user} but is not allowed to see it, returning 404");
|
||||
return Err!(Request(NotFound("Event not found.")));
|
||||
}
|
||||
let target_pdu = services
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu(target)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("Event not found."))))?;
|
||||
// Return M_SENDER_IGNORED if the sender of base_event is ignored (MSC4406)
|
||||
is_ignored_pdu(services, &target_pdu, sender_user).await?;
|
||||
|
||||
let start: PduCount = from
|
||||
.map(str::parse)
|
||||
@@ -159,6 +167,7 @@ async fn paginate_relations_with_filter(
|
||||
.ready_take_while(|(count, _)| Some(*count) != to)
|
||||
.take(limit)
|
||||
.wide_filter_map(|item| visibility_filter(services, sender_user, item))
|
||||
.wide_filter_map(|item| ignored_filter(services, item, sender_user))
|
||||
.then(async |mut pdu| {
|
||||
if let Err(e) = services
|
||||
.rooms
|
||||
@@ -214,3 +223,17 @@ async fn visibility_filter<Pdu: Event + Send + Sync>(
|
||||
.await
|
||||
.then_some(item)
|
||||
}
|
||||
|
||||
async fn ignored_filter<Pdu: Event + Send + Sync>(
|
||||
services: &Services,
|
||||
item: (PduCount, Pdu),
|
||||
sender_user: &UserId,
|
||||
) -> Option<(PduCount, Pdu)> {
|
||||
let (_, pdu) = &item;
|
||||
|
||||
if is_ignored_pdu(services, pdu, sender_user).await.ok()? {
|
||||
None
|
||||
} else {
|
||||
Some(item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub(crate) async fn get_room_event_route(
|
||||
|
||||
let (mut event, visible) = try_join(event, visible).await?;
|
||||
|
||||
if !visible || is_ignored_pdu(services, &event, body.sender_user()).await {
|
||||
if !visible || is_ignored_pdu(services, &event, body.sender_user()).await? {
|
||||
return Err!(Request(Forbidden("You don't have permission to view this event.")));
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ pub(super) async fn ldap_login(
|
||||
) -> Result<OwnedUserId> {
|
||||
let (user_dn, is_ldap_admin) = match services.config.ldap.bind_dn.as_ref() {
|
||||
| Some(bind_dn) if bind_dn.contains("{username}") =>
|
||||
(bind_dn.replace("{username}", lowercased_user_id.localpart()), false),
|
||||
(bind_dn.replace("{username}", lowercased_user_id.localpart()), None),
|
||||
| _ => {
|
||||
debug!("Searching user in LDAP");
|
||||
|
||||
@@ -144,12 +144,16 @@ pub(super) async fn ldap_login(
|
||||
.await?;
|
||||
}
|
||||
|
||||
let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await;
|
||||
// Only sync admin status if LDAP can actually determine it.
|
||||
// None means LDAP cannot determine admin status (manual config required).
|
||||
if let Some(is_ldap_admin) = is_ldap_admin {
|
||||
let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await;
|
||||
|
||||
if is_ldap_admin && !is_conduwuit_admin {
|
||||
Box::pin(services.admin.make_user_admin(lowercased_user_id)).await?;
|
||||
} else if !is_ldap_admin && is_conduwuit_admin {
|
||||
Box::pin(services.admin.revoke_admin(lowercased_user_id)).await?;
|
||||
if is_ldap_admin && !is_conduwuit_admin {
|
||||
Box::pin(services.admin.make_user_admin(lowercased_user_id)).await?;
|
||||
} else if !is_ldap_admin && is_conduwuit_admin {
|
||||
Box::pin(services.admin.revoke_admin(lowercased_user_id)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(user_id)
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
api::client::sync::sync_events::{self, DeviceLists, UnreadNotificationsCount},
|
||||
directory::RoomTypeFilter,
|
||||
events::{
|
||||
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType, TimelineEventType,
|
||||
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, AnySyncStateEvent, StateEventType,
|
||||
TimelineEventType,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
typing::TypingEventContent,
|
||||
},
|
||||
@@ -533,6 +534,9 @@ async fn process_rooms<'a, Rooms>(
|
||||
}
|
||||
});
|
||||
|
||||
let required_state =
|
||||
collect_required_state(services, room_id, required_state_request).await;
|
||||
|
||||
let room_events: Vec<_> = timeline_pdus
|
||||
.iter()
|
||||
.stream()
|
||||
@@ -551,21 +555,6 @@ async fn process_rooms<'a, Rooms>(
|
||||
}
|
||||
}
|
||||
|
||||
let required_state = required_state_request
|
||||
.iter()
|
||||
.stream()
|
||||
.filter_map(|state| async move {
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &state.0, &state.1)
|
||||
.await
|
||||
.map(Event::into_format)
|
||||
.ok()
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
// Heroes
|
||||
let heroes: Vec<_> = services
|
||||
.rooms
|
||||
@@ -689,6 +678,51 @@ async fn process_rooms<'a, Rooms>(
|
||||
Ok(rooms)
|
||||
}
|
||||
|
||||
/// Collect the required state events for a room
|
||||
async fn collect_required_state(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
required_state_request: &BTreeSet<TypeStateKey>,
|
||||
) -> Vec<Raw<AnySyncStateEvent>> {
|
||||
let mut required_state = Vec::new();
|
||||
let mut wildcard_types: HashSet<&StateEventType> = HashSet::new();
|
||||
|
||||
for (event_type, state_key) in required_state_request {
|
||||
if wildcard_types.contains(event_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if state_key.as_str() == "*" {
|
||||
wildcard_types.insert(event_type);
|
||||
if let Ok(keys) = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_keys(room_id, event_type)
|
||||
.await
|
||||
{
|
||||
for key in keys {
|
||||
if let Ok(event) = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, event_type, &key)
|
||||
.await
|
||||
{
|
||||
required_state.push(Event::into_format(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Ok(event) = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(room_id, event_type, state_key)
|
||||
.await
|
||||
{
|
||||
required_state.push(Event::into_format(event));
|
||||
}
|
||||
}
|
||||
required_state
|
||||
}
|
||||
|
||||
async fn collect_typing_events(
|
||||
services: &Services,
|
||||
sender_user: &UserId,
|
||||
|
||||
@@ -27,6 +27,7 @@ pub(crate) async fn well_known_client(
|
||||
identity_server: None,
|
||||
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
|
||||
tile_server: None,
|
||||
rtc_foci: services.config.well_known.rtc_focus_server_urls.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#![type_length_limit = "16384"] //TODO: reduce me
|
||||
#![allow(clippy::toplevel_ref_arg)]
|
||||
|
||||
extern crate conduwuit_core as conduwuit;
|
||||
extern crate conduwuit_service as service;
|
||||
pub mod client;
|
||||
pub mod router;
|
||||
pub mod server;
|
||||
|
||||
extern crate conduwuit_core as conduwuit;
|
||||
extern crate conduwuit_service as service;
|
||||
pub mod admin;
|
||||
|
||||
pub(crate) use self::router::{Ruma, RumaResponse, State};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
use self::handler::RouterExt;
|
||||
pub(super) use self::{args::Args as Ruma, response::RumaResponse};
|
||||
use crate::{client, server};
|
||||
use crate::{admin, client, server};
|
||||
|
||||
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
let config = &server.config;
|
||||
@@ -187,7 +187,9 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.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));
|
||||
.route("/client/server.json", get(client::syncv3_client_server_json))
|
||||
.ruma_route(&admin::rooms::ban::ban_room)
|
||||
.ruma_route(&admin::rooms::list::list_rooms);
|
||||
|
||||
if config.allow_federation {
|
||||
router = router
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Event, PduCount, Result,
|
||||
Err, Event, PduCount, Result, info,
|
||||
result::LogErr,
|
||||
utils::{IterStream, ReadyExt, stream::TryTools},
|
||||
};
|
||||
@@ -34,6 +34,18 @@ pub(crate) async fn get_backfill_route(
|
||||
}
|
||||
.check()
|
||||
.await?;
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = body.origin().as_str(),
|
||||
"Refusing to serve backfill for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
let limit = body
|
||||
.limit
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Result, err};
|
||||
use conduwuit::{Err, Result, err, info};
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, RoomId, api::federation::event::get_event};
|
||||
|
||||
use super::AccessCheck;
|
||||
@@ -38,6 +38,19 @@ pub(crate) async fn get_event_route(
|
||||
.check()
|
||||
.await?;
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = body.origin().as_str(),
|
||||
"Refusing to serve state for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
Ok(get_event::v1::Response {
|
||||
origin: services.globals.server_name().to_owned(),
|
||||
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{borrow::Borrow, iter::once};
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Error, Result, utils::stream::ReadyExt};
|
||||
use conduwuit::{Err, Error, Result, info, utils::stream::ReadyExt};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
RoomId,
|
||||
@@ -29,6 +29,19 @@ pub(crate) async fn get_event_authorization_route(
|
||||
.check()
|
||||
.await?;
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = body.origin().as_str(),
|
||||
"Refusing to serve state for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
let event = services
|
||||
.rooms
|
||||
.timeline
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Result, debug, debug_error, utils::to_canonical_object};
|
||||
use ruma::api::federation::event::get_missing_events;
|
||||
use conduwuit::{Err, Event, Result, debug, info, trace, utils::to_canonical_object, warn};
|
||||
use ruma::{OwnedEventId, api::federation::event::get_missing_events};
|
||||
use serde_json::{json, value::RawValue};
|
||||
|
||||
use super::AccessCheck;
|
||||
use crate::Ruma;
|
||||
@@ -26,65 +29,95 @@ pub(crate) async fn get_missing_events_route(
|
||||
.check()
|
||||
.await?;
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = body.origin().as_str(),
|
||||
"Refusing to serve state for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
let limit = body
|
||||
.limit
|
||||
.try_into()
|
||||
.unwrap_or(LIMIT_DEFAULT)
|
||||
.min(LIMIT_MAX);
|
||||
|
||||
let mut queued_events = body.latest_events.clone();
|
||||
// the vec will never have more entries the limit
|
||||
let mut events = Vec::with_capacity(limit);
|
||||
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
|
||||
|
||||
let mut i: usize = 0;
|
||||
while i < queued_events.len() && events.len() < limit {
|
||||
let Ok(pdu) = services.rooms.timeline.get_pdu(&queued_events[i]).await else {
|
||||
debug!(
|
||||
body.origin = body.origin.as_ref().map(tracing::field::display),
|
||||
"Event {} does not exist locally, skipping", &queued_events[i]
|
||||
);
|
||||
i = i.saturating_add(1);
|
||||
let mut queue: VecDeque<OwnedEventId> = VecDeque::from(body.latest_events.clone());
|
||||
let mut results: Vec<Box<RawValue>> = Vec::with_capacity(limit);
|
||||
let mut seen: HashSet<OwnedEventId> = HashSet::from_iter(body.earliest_events.clone());
|
||||
|
||||
while let Some(next_event_id) = queue.pop_front() {
|
||||
if seen.contains(&next_event_id) {
|
||||
trace!(%next_event_id, "already seen event, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
if results.len() >= limit {
|
||||
debug!(%next_event_id, "reached limit of events to return, breaking");
|
||||
break;
|
||||
}
|
||||
|
||||
let mut pdu = match services.rooms.timeline.get_pdu(&next_event_id).await {
|
||||
| Ok(pdu) => pdu,
|
||||
| Err(e) => {
|
||||
warn!("could not find event {next_event_id} while walking missing events: {e}");
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
if body.earliest_events.contains(&queued_events[i]) {
|
||||
i = i.saturating_add(1);
|
||||
continue;
|
||||
if pdu.room_id_or_hash() != body.room_id {
|
||||
return Err!(Request(Unknown(
|
||||
"Event {next_event_id} is not in room {}",
|
||||
body.room_id
|
||||
)));
|
||||
}
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.server_can_see_event(body.origin(), &body.room_id, &queued_events[i])
|
||||
.server_can_see_event(body.origin(), &body.room_id, pdu.event_id())
|
||||
.await
|
||||
{
|
||||
debug!(
|
||||
body.origin = body.origin.as_ref().map(tracing::field::display),
|
||||
"Server cannot see {:?} in {:?}, skipping", pdu.event_id, pdu.room_id
|
||||
);
|
||||
i = i.saturating_add(1);
|
||||
continue;
|
||||
debug!(%next_event_id, origin = %body.origin(), "redacting event origin cannot see");
|
||||
pdu.redact(&room_version, json!({}))?;
|
||||
}
|
||||
|
||||
let Ok(event) = to_canonical_object(&pdu) else {
|
||||
debug_error!(
|
||||
body.origin = body.origin.as_ref().map(tracing::field::display),
|
||||
"Failed to convert PDU in database to canonical JSON: {pdu:?}"
|
||||
);
|
||||
i = i.saturating_add(1);
|
||||
continue;
|
||||
};
|
||||
|
||||
let prev_events = pdu.prev_events.iter().map(ToOwned::to_owned);
|
||||
|
||||
let event = services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(event)
|
||||
.await;
|
||||
|
||||
queued_events.extend(prev_events);
|
||||
events.push(event);
|
||||
trace!(
|
||||
%next_event_id,
|
||||
prev_events = ?pdu.prev_events().collect::<Vec<_>>(),
|
||||
"adding event to results and queueing prev events"
|
||||
);
|
||||
queue.extend(pdu.prev_events.clone());
|
||||
seen.insert(next_event_id.clone());
|
||||
if body.latest_events.contains(&next_event_id) {
|
||||
continue; // Don't include latest_events in results,
|
||||
// but do include their prev_events in the queue
|
||||
}
|
||||
results.push(
|
||||
services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(to_canonical_object(pdu)?)
|
||||
.await,
|
||||
);
|
||||
trace!(
|
||||
%next_event_id,
|
||||
queue_len = queue.len(),
|
||||
seen_len = seen.len(),
|
||||
results_len = results.len(),
|
||||
"event added to results"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(get_missing_events::v1::Response { events })
|
||||
if !queue.is_empty() {
|
||||
debug!("limit reached before queue was empty");
|
||||
}
|
||||
results.reverse(); // return oldest first
|
||||
Ok(get_missing_events::v1::Response { events: results })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result,
|
||||
Err, Result, info,
|
||||
utils::stream::{BroadbandExt, IterStream},
|
||||
};
|
||||
use conduwuit_service::rooms::spaces::{
|
||||
@@ -23,6 +23,19 @@ pub(crate) async fn get_hierarchy_route(
|
||||
return Err!(Request(NotFound("Room does not exist.")));
|
||||
}
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = body.origin().as_str(),
|
||||
"Refusing to serve state for room we aren't participating in"
|
||||
);
|
||||
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());
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use conduwuit::{
|
||||
Err, Error, PduEvent, Result, err,
|
||||
Err, Error, PduEvent, Result, err, error,
|
||||
matrix::{Event, event::gen_event_id},
|
||||
utils::{self, hash::sha256},
|
||||
warn,
|
||||
@@ -199,20 +199,27 @@ pub(crate) async fn create_invite_route(
|
||||
|
||||
for appservice in services.appservice.read().await.values() {
|
||||
if appservice.is_user_match(&recipient_user) {
|
||||
let request = ruma::api::appservice::event::push_events::v1::Request {
|
||||
events: vec![pdu.to_format()],
|
||||
txn_id: general_purpose::URL_SAFE_NO_PAD
|
||||
.encode(sha256::hash(pdu.event_id.as_bytes()))
|
||||
.into(),
|
||||
ephemeral: Vec::new(),
|
||||
to_device: Vec::new(),
|
||||
};
|
||||
services
|
||||
.sending
|
||||
.send_appservice_request(
|
||||
appservice.registration.clone(),
|
||||
ruma::api::appservice::event::push_events::v1::Request {
|
||||
events: vec![pdu.to_format()],
|
||||
txn_id: general_purpose::URL_SAFE_NO_PAD
|
||||
.encode(sha256::hash(pdu.event_id.as_bytes()))
|
||||
.into(),
|
||||
ephemeral: Vec::new(),
|
||||
to_device: Vec::new(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.send_appservice_request(appservice.registration.clone(), request)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(
|
||||
"failed to notify appservice {} about incoming invite: {e}",
|
||||
appservice.registration.id
|
||||
);
|
||||
err!(BadServerResponse(
|
||||
"Failed to notify appservice about incoming invite."
|
||||
))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
},
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use service::rooms::state::RoomMutexGuard;
|
||||
use tokio::join;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -30,6 +32,18 @@ pub(crate) async fn create_join_event_template_route(
|
||||
if !services.rooms.metadata.exists(&body.room_id).await {
|
||||
return Err!(Request(NotFound("Room is unknown to this server.")));
|
||||
}
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = body.origin().as_str(),
|
||||
"Refusing to serve make_join for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
if body.user_id.server_name() != body.origin() {
|
||||
return Err!(Request(BadJson("Not allowed to join on behalf of another server/user.")));
|
||||
@@ -73,16 +87,24 @@ pub(crate) async fn create_join_event_template_route(
|
||||
}
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
let is_invited = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_invited(&body.user_id, &body.room_id)
|
||||
.await;
|
||||
let (is_invited, is_joined) = join!(
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_invited(&body.user_id, &body.room_id),
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(&body.user_id, &body.room_id)
|
||||
);
|
||||
let join_authorized_via_users_server: Option<OwnedUserId> = {
|
||||
use RoomVersionId::*;
|
||||
if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) || is_invited {
|
||||
// room version does not support restricted join rules, or the user is currently
|
||||
// already invited
|
||||
if is_joined || is_invited {
|
||||
// User is already joined or invited and consequently does not need an
|
||||
// authorising user
|
||||
None
|
||||
} else if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
|
||||
// room version does not support restricted join rules
|
||||
None
|
||||
} else if user_can_perform_restricted_join(
|
||||
&services,
|
||||
@@ -92,32 +114,10 @@ pub(crate) async fn create_join_event_template_route(
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let Some(auth_user) = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.local_users_in_room(&body.room_id)
|
||||
.filter(|user| {
|
||||
services.rooms.state_accessor.user_can_invite(
|
||||
&body.room_id,
|
||||
user,
|
||||
&body.user_id,
|
||||
&state_lock,
|
||||
)
|
||||
})
|
||||
.boxed()
|
||||
.next()
|
||||
.await
|
||||
.map(ToOwned::to_owned)
|
||||
else {
|
||||
info!(
|
||||
"No local user is able to authorize the join of {} into {}",
|
||||
&body.user_id, &body.room_id
|
||||
);
|
||||
return Err!(Request(UnableToGrantJoin(
|
||||
"No user on this server is able to assist in joining."
|
||||
)));
|
||||
};
|
||||
Some(auth_user)
|
||||
Some(
|
||||
select_authorising_user(&services, &body.room_id, &body.user_id, &state_lock)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -147,9 +147,7 @@ pub(crate) async fn create_join_event_template_route(
|
||||
)
|
||||
.await?;
|
||||
drop(state_lock);
|
||||
|
||||
// room v3 and above removed the "event_id" field from remote PDU format
|
||||
maybe_strip_event_id(&mut pdu_json, &room_version_id)?;
|
||||
pdu_json.remove("event_id");
|
||||
|
||||
Ok(prepare_join_event::v1::Response {
|
||||
room_version: Some(room_version_id),
|
||||
@@ -157,6 +155,38 @@ pub(crate) async fn create_join_event_template_route(
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempts to find a user who is able to issue an invite in the target room.
|
||||
pub(crate) async fn select_authorising_user(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
state_lock: &RoomMutexGuard,
|
||||
) -> Result<OwnedUserId> {
|
||||
let auth_user = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.local_users_in_room(room_id)
|
||||
.filter(|user| {
|
||||
services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_invite(room_id, user, user_id, state_lock)
|
||||
})
|
||||
.boxed()
|
||||
.next()
|
||||
.await
|
||||
.map(ToOwned::to_owned);
|
||||
|
||||
match auth_user {
|
||||
| Some(auth_user) => Ok(auth_user),
|
||||
| None => {
|
||||
Err!(Request(UnableToGrantJoin(
|
||||
"No user on this server is able to assist in joining."
|
||||
)))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether the given user can join the given room via a restricted join.
|
||||
pub(crate) async fn user_can_perform_restricted_join(
|
||||
services: &Services,
|
||||
@@ -168,12 +198,9 @@ pub(crate) async fn user_can_perform_restricted_join(
|
||||
|
||||
// restricted rooms are not supported on <=v7
|
||||
if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if services.rooms.state_cache.is_joined(user_id, room_id).await {
|
||||
// joining user is already joined, there is nothing we need to do
|
||||
return Ok(false);
|
||||
// This should be impossible as it was checked earlier on, but retain this check
|
||||
// for safety.
|
||||
unreachable!("user_can_perform_restricted_join got incompatible room version");
|
||||
}
|
||||
|
||||
let Ok(join_rules_event_content) = services
|
||||
@@ -193,17 +220,31 @@ pub(crate) async fn user_can_perform_restricted_join(
|
||||
let (JoinRule::Restricted(r) | JoinRule::KnockRestricted(r)) =
|
||||
join_rules_event_content.join_rule
|
||||
else {
|
||||
// This is not a restricted room
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if r.allow.is_empty() {
|
||||
debug_info!("{room_id} is restricted but the allow key is empty");
|
||||
return Ok(false);
|
||||
// This will never be authorisable, return forbidden.
|
||||
return Err!(Request(Forbidden("You are not invited to this room.")));
|
||||
}
|
||||
|
||||
let mut could_satisfy = true;
|
||||
for allow_rule in &r.allow {
|
||||
match allow_rule {
|
||||
| AllowRule::RoomMembership(membership) => {
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &membership.room_id)
|
||||
.await
|
||||
{
|
||||
// Since we can't check this room, mark could_satisfy as false
|
||||
// so that we can return M_UNABLE_TO_AUTHORIZE_JOIN later.
|
||||
could_satisfy = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if services
|
||||
.rooms
|
||||
.state_cache
|
||||
@@ -227,6 +268,8 @@ pub(crate) async fn user_can_perform_restricted_join(
|
||||
| Err(_) => Err!(Request(Forbidden("Antispam rejected join request."))),
|
||||
},
|
||||
| _ => {
|
||||
// We don't recognise this join rule, so we cannot satisfy the request.
|
||||
could_satisfy = false;
|
||||
debug_info!(
|
||||
"Unsupported allow rule in restricted join for room {}: {:?}",
|
||||
room_id,
|
||||
@@ -236,9 +279,23 @@ pub(crate) async fn user_can_perform_restricted_join(
|
||||
}
|
||||
}
|
||||
|
||||
Err!(Request(UnableToAuthorizeJoin(
|
||||
"Joining user is not known to be in any required room."
|
||||
)))
|
||||
if could_satisfy {
|
||||
// We were able to check all the restrictions and can be certain that the
|
||||
// prospective member is not permitted to join.
|
||||
Err!(Request(Forbidden(
|
||||
"You do not belong to any of the rooms or spaces required to join this room."
|
||||
)))
|
||||
} else {
|
||||
// We were unable to check all the restrictions. This usually means we aren't in
|
||||
// one of the rooms this one is restricted to, ergo can't check its state for
|
||||
// the user's membership, and consequently the user *might* be able to join if
|
||||
// they ask another server.
|
||||
Err!(Request(UnableToAuthorizeJoin(
|
||||
"You do not belong to any of the recognised rooms or spaces required to join this \
|
||||
room, but this server is unable to verify every requirement. You may be able to \
|
||||
join via another server."
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_strip_event_id(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use RoomVersionId::*;
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Error, Result, debug_warn, matrix::pdu::PduBuilder, warn};
|
||||
use conduwuit::{Err, Error, Result, debug_warn, info, matrix::pdu::PduBuilder, warn};
|
||||
use ruma::{
|
||||
RoomVersionId,
|
||||
api::{client::error::ErrorKind, federation::knock::create_knock_event_template},
|
||||
@@ -20,6 +20,18 @@ pub(crate) async fn create_knock_event_template_route(
|
||||
if !services.rooms.metadata.exists(&body.room_id).await {
|
||||
return Err!(Request(NotFound("Room is unknown to this server.")));
|
||||
}
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = body.origin().as_str(),
|
||||
"Refusing to serve make_knock for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
if body.user_id.server_name() != body.origin() {
|
||||
return Err!(Request(BadJson("Not allowed to knock on behalf of another server/user.")));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
|
||||
use conduwuit::{Err, Result, info, matrix::pdu::PduBuilder};
|
||||
use ruma::{
|
||||
api::federation::membership::prepare_leave_event,
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
@@ -20,6 +20,19 @@ pub(crate) async fn create_leave_event_template_route(
|
||||
return Err!(Request(NotFound("Room is unknown to this server.")));
|
||||
}
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = body.origin().as_str(),
|
||||
"Refusing to serve make_leave for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
if body.user_id.server_name() != body.origin() {
|
||||
return Err!(Request(Forbidden(
|
||||
"Not allowed to leave on behalf of another server/user."
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||
use ruma::{
|
||||
CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
|
||||
ServerName,
|
||||
CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName,
|
||||
api::federation::membership::create_join_event,
|
||||
events::{
|
||||
StateEventType,
|
||||
@@ -37,6 +36,18 @@ async fn create_join_event(
|
||||
if !services.rooms.metadata.exists(room_id).await {
|
||||
return Err!(Request(NotFound("Room is unknown to this server.")));
|
||||
}
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = origin.as_str(),
|
||||
"Refusing to serve send_join for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
// ACL check origin server
|
||||
services
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result, err,
|
||||
Err, Result, err, info,
|
||||
matrix::{event::gen_event_id_canonical_json, pdu::PduEvent},
|
||||
warn,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use ruma::{
|
||||
OwnedServerName, OwnedUserId,
|
||||
OwnedUserId,
|
||||
RoomVersionId::*,
|
||||
api::federation::knock::send_knock,
|
||||
events::{
|
||||
@@ -54,6 +54,19 @@ pub(crate) async fn create_knock_event_v1_route(
|
||||
return Err!(Request(NotFound("Room is unknown to this server.")));
|
||||
}
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = body.origin().as_str(),
|
||||
"Refusing to serve send_knock for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
// ACL check origin server
|
||||
services
|
||||
.rooms
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, err, matrix::event::gen_event_id_canonical_json};
|
||||
use conduwuit::{Err, Result, err, info, matrix::event::gen_event_id_canonical_json};
|
||||
use conduwuit_service::Services;
|
||||
use futures::FutureExt;
|
||||
use ruma::{
|
||||
@@ -50,6 +50,19 @@ async fn create_leave_event(
|
||||
return Err!(Request(NotFound("Room is unknown to this server.")));
|
||||
}
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = origin.as_str(),
|
||||
"Refusing to serve backfill for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
// ACL check origin
|
||||
services
|
||||
.rooms
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{borrow::Borrow, iter::once};
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Result, at, err, utils::IterStream};
|
||||
use conduwuit::{Err, Result, at, err, info, utils::IterStream};
|
||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||
use ruma::{OwnedEventId, api::federation::event::get_room_state};
|
||||
|
||||
@@ -24,6 +24,19 @@ pub(crate) async fn get_room_state_route(
|
||||
.check()
|
||||
.await?;
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = body.origin().as_str(),
|
||||
"Refusing to serve state for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
let shortstatehash = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{borrow::Borrow, iter::once};
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Result, at, err};
|
||||
use conduwuit::{Err, Result, at, err, info};
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use ruma::{OwnedEventId, api::federation::event::get_room_state_ids};
|
||||
|
||||
@@ -25,6 +25,19 @@ pub(crate) async fn get_room_state_ids_route(
|
||||
.check()
|
||||
.await?;
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||
.await
|
||||
{
|
||||
info!(
|
||||
origin = body.origin().as_str(),
|
||||
"Refusing to serve state for room we aren't participating in"
|
||||
);
|
||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||
}
|
||||
|
||||
let shortstatehash = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use conduwuit::{Err, Result, implement, is_false};
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, StreamExt, future::OptionFuture, join};
|
||||
use futures::{FutureExt, future::OptionFuture, join};
|
||||
use ruma::{EventId, RoomId, ServerName};
|
||||
|
||||
pub(super) struct AccessCheck<'a> {
|
||||
@@ -31,15 +31,6 @@ pub(super) async fn check(&self) -> Result {
|
||||
.state_cache
|
||||
.server_in_room(self.origin, self.room_id);
|
||||
|
||||
// if any user on our homeserver is trying to knock this room, we'll need to
|
||||
// acknowledge bans or leaves
|
||||
let user_is_knocking = self
|
||||
.services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_members_knocked(self.room_id)
|
||||
.count();
|
||||
|
||||
let server_can_see: OptionFuture<_> = self
|
||||
.event_id
|
||||
.map(|event_id| {
|
||||
@@ -51,14 +42,14 @@ pub(super) async fn check(&self) -> Result {
|
||||
})
|
||||
.into();
|
||||
|
||||
let (world_readable, server_in_room, server_can_see, acl_check, user_is_knocking) =
|
||||
join!(world_readable, server_in_room, server_can_see, acl_check, user_is_knocking);
|
||||
let (world_readable, server_in_room, server_can_see, acl_check) =
|
||||
join!(world_readable, server_in_room, server_can_see, acl_check);
|
||||
|
||||
if !acl_check {
|
||||
return Err!(Request(Forbidden("Server access denied.")));
|
||||
}
|
||||
|
||||
if !world_readable && !server_in_room && user_is_knocking == 0 {
|
||||
if !world_readable && !server_in_room {
|
||||
return Err!(Request(Forbidden("Server is not in room.")));
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
const NAME_MAX: usize = 128;
|
||||
const KEY_SEGS: usize = 8;
|
||||
|
||||
#[crate::ctor]
|
||||
#[ctor::ctor]
|
||||
fn _static_initialization() {
|
||||
acq_epoch().expect("pre-initialization of jemalloc failed");
|
||||
acq_epoch().expect("pre-initialization of jemalloc failed");
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
use regex::RegexSet;
|
||||
use ruma::{
|
||||
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
|
||||
api::client::discovery::discover_support::ContactRole,
|
||||
api::client::discovery::{discover_homeserver::RtcFocusInfo, discover_support::ContactRole},
|
||||
};
|
||||
use serde::{Deserialize, de::IgnoredAny};
|
||||
use url::Url;
|
||||
@@ -559,7 +559,7 @@ pub struct Config {
|
||||
///
|
||||
/// If you would like registration only via token reg, please configure
|
||||
/// `registration_token`.
|
||||
#[serde(default)]
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_registration: bool,
|
||||
|
||||
/// If registration is enabled, and this setting is true, new users
|
||||
@@ -1696,6 +1696,11 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub url_preview_check_root_domain: bool,
|
||||
|
||||
/// User agent that is used specifically when fetching url previews.
|
||||
///
|
||||
/// default: "continuwuity/<version> (bot; +https://continuwuity.org)"
|
||||
pub url_preview_user_agent: Option<String>,
|
||||
|
||||
/// List of forbidden room aliases and room IDs as strings of regex
|
||||
/// patterns.
|
||||
///
|
||||
@@ -2111,6 +2116,19 @@ pub struct WellKnownConfig {
|
||||
/// If no email or mxid is specified, all of the server's admins will be
|
||||
/// listed.
|
||||
pub support_mxid: Option<OwnedUserId>,
|
||||
|
||||
/// A list of MatrixRTC foci URLs which will be served as part of the
|
||||
/// MSC4143 client endpoint at /.well-known/matrix/client. If you're
|
||||
/// setting up livekit, you'd want something like:
|
||||
/// rtc_focus_server_urls = [
|
||||
/// { type = "livekit", livekit_service_url = "https://livekit.example.com" },
|
||||
/// ]
|
||||
///
|
||||
/// To disable, set this to be an empty vector (`[]`).
|
||||
///
|
||||
/// default: []
|
||||
#[serde(default = "default_rtc_focus_urls")]
|
||||
pub rtc_focus_server_urls: Vec<RtcFocusInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Default)]
|
||||
@@ -2261,7 +2279,11 @@ struct ListeningAddr {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.antispam")]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.antispam",
|
||||
optional = "true"
|
||||
)]
|
||||
pub struct Antispam {
|
||||
/// display: nested
|
||||
pub meowlnir: Option<MeowlnirConfig>,
|
||||
@@ -2272,7 +2294,8 @@ pub struct Antispam {
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.antispam.meowlnir"
|
||||
section = "global.antispam.meowlnir",
|
||||
optional = "true"
|
||||
)]
|
||||
pub struct MeowlnirConfig {
|
||||
/// The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
|
||||
@@ -2301,7 +2324,8 @@ pub struct MeowlnirConfig {
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.antispam.draupnir"
|
||||
section = "global.antispam.draupnir",
|
||||
optional = "true"
|
||||
)]
|
||||
pub struct DraupnirConfig {
|
||||
/// The base URL on which to contact Draupnir (before /api/).
|
||||
@@ -2602,6 +2626,9 @@ fn default_rocksdb_stats_level() -> u8 { 1 }
|
||||
#[inline]
|
||||
pub fn default_default_room_version() -> RoomVersionId { RoomVersionId::V11 }
|
||||
|
||||
#[must_use]
|
||||
pub fn default_rtc_focus_urls() -> Vec<RtcFocusInfo> { vec![] }
|
||||
|
||||
fn default_ip_range_denylist() -> Vec<String> {
|
||||
vec![
|
||||
"127.0.0.0/8".to_owned(),
|
||||
|
||||
@@ -62,7 +62,7 @@ macro_rules! debug_info {
|
||||
pub static DEBUGGER: LazyLock<bool> =
|
||||
LazyLock::new(|| env::var("_").unwrap_or_default().ends_with("gdb"));
|
||||
|
||||
#[cfg_attr(debug_assertions, crate::ctor)]
|
||||
#[cfg_attr(debug_assertions, ctor::ctor)]
|
||||
#[cfg_attr(not(debug_assertions), allow(dead_code))]
|
||||
fn set_panic_trap() {
|
||||
if !*DEBUGGER {
|
||||
|
||||
@@ -115,7 +115,7 @@ macro_rules! err {
|
||||
macro_rules! err_log {
|
||||
($out:ident, $level:ident, $($fields:tt)+) => {{
|
||||
use $crate::tracing::{
|
||||
callsite, callsite2, metadata, valueset, Callsite,
|
||||
callsite, callsite2, metadata, valueset_all, Callsite,
|
||||
Level,
|
||||
};
|
||||
|
||||
@@ -133,7 +133,7 @@ macro_rules! err_log {
|
||||
fields: $($fields)+,
|
||||
};
|
||||
|
||||
($crate::error::visit)(&mut $out, LEVEL, &__CALLSITE, &mut valueset!(__CALLSITE.metadata().fields(), $($fields)+));
|
||||
($crate::error::visit)(&mut $out, LEVEL, &__CALLSITE, &mut valueset_all!(__CALLSITE.metadata().fields(), $($fields)+));
|
||||
($out).into()
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,25 @@
|
||||
|
||||
impl axum::response::IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let status = self.status_code();
|
||||
if status.is_server_error() {
|
||||
error!(
|
||||
error = %self,
|
||||
error_debug = ?self,
|
||||
kind = ?self.kind(),
|
||||
status = %status,
|
||||
"Server error"
|
||||
);
|
||||
} else if status.is_client_error() {
|
||||
use crate::debug_error;
|
||||
debug_error!(
|
||||
error = %self,
|
||||
kind = ?self.kind(),
|
||||
status = %status,
|
||||
"Client error"
|
||||
);
|
||||
}
|
||||
|
||||
let response: UiaaResponse = self.into();
|
||||
response
|
||||
.try_into_http_response::<BytesMut>()
|
||||
@@ -66,7 +85,8 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
|
||||
| Unrecognized => StatusCode::METHOD_NOT_ALLOWED,
|
||||
|
||||
// 404
|
||||
| NotFound | NotImplemented | FeatureDisabled => StatusCode::NOT_FOUND,
|
||||
| NotFound | NotImplemented | FeatureDisabled | SenderIgnored { .. } =>
|
||||
StatusCode::NOT_FOUND,
|
||||
|
||||
// 403
|
||||
| GuestAccessForbidden
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
//! Information about the build related to Cargo. This is a frontend interface
|
||||
//! informed by proc-macros that capture raw information at build time which is
|
||||
//! further processed at runtime either during static initialization or as
|
||||
//! necessary.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use cargo_toml::{DepsSet, Manifest};
|
||||
use conduwuit_macros::cargo_manifest;
|
||||
|
||||
use crate::Result;
|
||||
|
||||
// Raw captures of the cargo manifest for each crate. This is provided by a
|
||||
// proc-macro at build time since the source directory and the cargo toml's may
|
||||
// not be present during execution.
|
||||
|
||||
#[cargo_manifest]
|
||||
const WORKSPACE_MANIFEST: &'static str = ();
|
||||
#[cargo_manifest(crate = "macros")]
|
||||
const MACROS_MANIFEST: &'static str = ();
|
||||
#[cargo_manifest(crate = "core")]
|
||||
const CORE_MANIFEST: &'static str = ();
|
||||
#[cargo_manifest(crate = "database")]
|
||||
const DATABASE_MANIFEST: &'static str = ();
|
||||
#[cargo_manifest(crate = "service")]
|
||||
const SERVICE_MANIFEST: &'static str = ();
|
||||
#[cargo_manifest(crate = "admin")]
|
||||
const ADMIN_MANIFEST: &'static str = ();
|
||||
#[cargo_manifest(crate = "router")]
|
||||
const ROUTER_MANIFEST: &'static str = ();
|
||||
#[cargo_manifest(crate = "main")]
|
||||
const MAIN_MANIFEST: &'static str = ();
|
||||
|
||||
/// Processed list of features across all project crates. This is generated from
|
||||
/// the data in the MANIFEST strings and contains all possible project features.
|
||||
/// For *enabled* features see the info::rustc module instead.
|
||||
static FEATURES: OnceLock<Vec<String>> = OnceLock::new();
|
||||
|
||||
/// Processed list of dependencies. This is generated from the data captured in
|
||||
/// the MANIFEST.
|
||||
static DEPENDENCIES: OnceLock<DepsSet> = OnceLock::new();
|
||||
|
||||
#[must_use]
|
||||
pub fn dependencies_names() -> Vec<&'static str> {
|
||||
dependencies().keys().map(String::as_str).collect()
|
||||
}
|
||||
|
||||
pub fn dependencies() -> &'static DepsSet {
|
||||
DEPENDENCIES.get_or_init(|| {
|
||||
init_dependencies().unwrap_or_else(|e| panic!("Failed to initialize dependencies: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
/// List of all possible features for the project. For *enabled* features in
|
||||
/// this build see the companion function in info::rustc.
|
||||
pub fn features() -> &'static Vec<String> {
|
||||
FEATURES.get_or_init(|| {
|
||||
init_features().unwrap_or_else(|e| panic!("Failed initialize features: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
fn init_features() -> Result<Vec<String>> {
|
||||
let mut features = Vec::new();
|
||||
append_features(&mut features, WORKSPACE_MANIFEST)?;
|
||||
append_features(&mut features, MACROS_MANIFEST)?;
|
||||
append_features(&mut features, CORE_MANIFEST)?;
|
||||
append_features(&mut features, DATABASE_MANIFEST)?;
|
||||
append_features(&mut features, SERVICE_MANIFEST)?;
|
||||
append_features(&mut features, ADMIN_MANIFEST)?;
|
||||
append_features(&mut features, ROUTER_MANIFEST)?;
|
||||
append_features(&mut features, MAIN_MANIFEST)?;
|
||||
features.sort();
|
||||
features.dedup();
|
||||
|
||||
Ok(features)
|
||||
}
|
||||
|
||||
fn append_features(features: &mut Vec<String>, manifest: &str) -> Result<()> {
|
||||
let manifest = Manifest::from_str(manifest)?;
|
||||
features.extend(manifest.features.keys().cloned());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_dependencies() -> Result<DepsSet> {
|
||||
let manifest = Manifest::from_str(WORKSPACE_MANIFEST)?;
|
||||
let deps_set = manifest
|
||||
.workspace
|
||||
.as_ref()
|
||||
.expect("manifest has workspace section")
|
||||
.dependencies
|
||||
.clone();
|
||||
|
||||
Ok(deps_set)
|
||||
}
|
||||
@@ -1,12 +1,5 @@
|
||||
//! Information about the project. This module contains version, build, system,
|
||||
//! etc information which can be queried by admins or used by developers.
|
||||
|
||||
pub mod cargo;
|
||||
pub mod room_version;
|
||||
pub mod rustc;
|
||||
pub mod version;
|
||||
|
||||
pub use conduwuit_macros::rustc_flags_capture;
|
||||
|
||||
pub const MODULE_ROOT: &str = const_str::split!(std::module_path!(), "::")[0];
|
||||
pub const CRATE_PREFIX: &str = const_str::split!(MODULE_ROOT, '_')[0];
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
//! Information about the build related to rustc. This is a frontend interface
|
||||
//! informed by proc-macros at build time. Since the project is split into
|
||||
//! several crates, lower-level information is supplied from each crate during
|
||||
//! static initialization.
|
||||
|
||||
use std::{collections::BTreeMap, sync::OnceLock};
|
||||
|
||||
use crate::utils::exchange;
|
||||
|
||||
/// Raw capture of rustc flags used to build each crate in the project. Informed
|
||||
/// by rustc_flags_capture macro (one in each crate's mod.rs). This is
|
||||
/// done during static initialization which is why it's mutex-protected and pub.
|
||||
/// Should not be written to by anything other than our macro.
|
||||
///
|
||||
/// We specifically use a std mutex here because parking_lot cannot be used
|
||||
/// after thread local storage is destroyed on MacOS.
|
||||
pub static FLAGS: std::sync::Mutex<BTreeMap<&str, &[&str]>> =
|
||||
std::sync::Mutex::new(BTreeMap::new());
|
||||
|
||||
/// Processed list of enabled features across all project crates. This is
|
||||
/// generated from the data in FLAGS.
|
||||
static FEATURES: OnceLock<Vec<&'static str>> = OnceLock::new();
|
||||
|
||||
/// List of features enabled for the project.
|
||||
pub fn features() -> &'static Vec<&'static str> { FEATURES.get_or_init(init_features) }
|
||||
|
||||
fn init_features() -> Vec<&'static str> {
|
||||
let mut features = Vec::new();
|
||||
FLAGS
|
||||
.lock()
|
||||
.expect("locked")
|
||||
.iter()
|
||||
.for_each(|(_, flags)| append_features(&mut features, flags));
|
||||
|
||||
features.sort_unstable();
|
||||
features.dedup();
|
||||
features
|
||||
}
|
||||
|
||||
fn append_features(features: &mut Vec<&'static str>, flags: &[&'static str]) {
|
||||
let mut next_is_cfg = false;
|
||||
for flag in flags {
|
||||
let is_cfg = *flag == "--cfg";
|
||||
let is_feature = flag.starts_with("feature=");
|
||||
if exchange(&mut next_is_cfg, is_cfg) && is_feature {
|
||||
if let Some(feature) = flag
|
||||
.split_once('=')
|
||||
.map(|(_, feature)| feature.trim_matches('"'))
|
||||
{
|
||||
features.push(feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,11 @@
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static BRANDING: &str = "continuwuity";
|
||||
static WEBSITE: &str = "https://continuwuity.org";
|
||||
static SEMANTIC: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
static VERSION: OnceLock<String> = OnceLock::new();
|
||||
static VERSION_UA: OnceLock<String> = OnceLock::new();
|
||||
static USER_AGENT: OnceLock<String> = OnceLock::new();
|
||||
|
||||
#[inline]
|
||||
@@ -19,11 +21,18 @@ pub fn name() -> &'static str { BRANDING }
|
||||
|
||||
#[inline]
|
||||
pub fn version() -> &'static str { VERSION.get_or_init(init_version) }
|
||||
#[inline]
|
||||
pub fn version_ua() -> &'static str { VERSION_UA.get_or_init(init_version_ua) }
|
||||
|
||||
#[inline]
|
||||
pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) }
|
||||
|
||||
fn init_user_agent() -> String { format!("{}/{}", name(), version()) }
|
||||
fn init_user_agent() -> String { format!("{}/{} (bot; +{WEBSITE})", name(), version_ua()) }
|
||||
|
||||
fn init_version_ua() -> String {
|
||||
conduwuit_build_metadata::version_tag()
|
||||
.map_or_else(|| SEMANTIC.to_owned(), |extra| format!("{SEMANTIC}+{extra}"))
|
||||
}
|
||||
|
||||
fn init_version() -> String {
|
||||
conduwuit_build_metadata::version_tag()
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
pub use config::Config;
|
||||
pub use error::Error;
|
||||
pub use info::{
|
||||
rustc_flags_capture, version,
|
||||
version,
|
||||
version::{name, version},
|
||||
};
|
||||
pub use matrix::{
|
||||
@@ -30,12 +30,10 @@
|
||||
};
|
||||
pub use parking_lot::{Mutex as SyncMutex, RwLock as SyncRwLock};
|
||||
pub use server::Server;
|
||||
pub use utils::{ctor, dtor, implement, result, result::Result};
|
||||
pub use utils::{implement, result, result::Result};
|
||||
|
||||
pub use crate as conduwuit_core;
|
||||
|
||||
rustc_flags_capture! {}
|
||||
|
||||
#[cfg(any(not(conduwuit_mods), not(feature = "conduwuit_mods")))]
|
||||
pub mod mods {
|
||||
#[macro_export]
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
pub mod with_lock;
|
||||
|
||||
pub use ::conduwuit_macros::implement;
|
||||
pub use ::ctor::{ctor, dtor};
|
||||
|
||||
pub use self::{
|
||||
arrayvec::ArrayVecExt,
|
||||
|
||||
@@ -64,7 +64,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
ctor.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
extern crate conduwuit_core as conduwuit;
|
||||
extern crate rust_rocksdb as rocksdb;
|
||||
|
||||
use ctor::{ctor, dtor};
|
||||
|
||||
conduwuit::mod_ctor! {}
|
||||
conduwuit::mod_dtor! {}
|
||||
conduwuit::rustc_flags_capture! {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod benches;
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
use std::{fs::read_to_string, path::PathBuf};
|
||||
|
||||
use proc_macro::{Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::{Error, ItemConst, Meta};
|
||||
|
||||
use crate::{Result, utils};
|
||||
|
||||
pub(super) fn manifest(item: ItemConst, args: &[Meta]) -> Result<TokenStream> {
|
||||
let member = utils::get_named_string(args, "crate");
|
||||
let path = manifest_path(member.as_deref())?;
|
||||
let manifest = read_to_string(&path).unwrap_or_default();
|
||||
let val = manifest.as_str();
|
||||
let name = item.ident;
|
||||
let ret = quote! {
|
||||
const #name: &'static str = #val;
|
||||
};
|
||||
|
||||
Ok(ret.into())
|
||||
}
|
||||
|
||||
#[allow(clippy::option_env_unwrap)]
|
||||
fn manifest_path(member: Option<&str>) -> Result<PathBuf> {
|
||||
let Some(path) = option_env!("CARGO_MANIFEST_DIR") else {
|
||||
return Err(Error::new(
|
||||
Span::call_site().into(),
|
||||
"missing CARGO_MANIFEST_DIR in environment",
|
||||
));
|
||||
};
|
||||
|
||||
let mut path: PathBuf = path.into();
|
||||
|
||||
// conduwuit/src/macros/ -> conduwuit/src/
|
||||
path.pop();
|
||||
|
||||
if let Some(member) = member {
|
||||
// conduwuit/$member/Cargo.toml
|
||||
path.push(member);
|
||||
} else {
|
||||
// conduwuit/src/ -> conduwuit/
|
||||
path.pop();
|
||||
}
|
||||
|
||||
path.push("Cargo.toml");
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
@@ -73,7 +73,13 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
|
||||
.expect("written to config file");
|
||||
}
|
||||
|
||||
file.write_fmt(format_args!("\n[{section}]\n"))
|
||||
let optional = settings.get("optional").is_some_and(|v| v == "true");
|
||||
let section_header = if optional {
|
||||
format!("\n#[{section}]\n")
|
||||
} else {
|
||||
format!("\n[{section}]\n")
|
||||
};
|
||||
file.write_fmt(format_args!("{section_header}"))
|
||||
.expect("written to config file");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
mod admin;
|
||||
mod cargo;
|
||||
mod config;
|
||||
mod debug;
|
||||
mod implement;
|
||||
mod refutable;
|
||||
mod rustc;
|
||||
mod utils;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use syn::{
|
||||
Error, Item, ItemConst, ItemEnum, ItemFn, ItemStruct, Meta,
|
||||
Error, Item, ItemEnum, ItemFn, ItemStruct, Meta,
|
||||
parse::{Parse, Parser},
|
||||
parse_macro_input,
|
||||
};
|
||||
@@ -26,19 +24,11 @@ pub fn admin_command_dispatch(args: TokenStream, input: TokenStream) -> TokenStr
|
||||
attribute_macro::<ItemEnum, _>(args, input, admin::command_dispatch)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn cargo_manifest(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
attribute_macro::<ItemConst, _>(args, input, cargo::manifest)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn recursion_depth(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
attribute_macro::<Item, _>(args, input, debug::recursion_depth)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn rustc_flags_capture(args: TokenStream) -> TokenStream { rustc::flags_capture(args) }
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn refutable(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
attribute_macro::<ItemFn, _>(args, input, refutable::refutable)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
pub(super) fn flags_capture(args: TokenStream) -> TokenStream {
|
||||
let cargo_crate_name = std::env::var("CARGO_CRATE_NAME");
|
||||
let crate_name = match cargo_crate_name.as_ref() {
|
||||
| Err(_) => return args,
|
||||
| Ok(crate_name) => crate_name.trim_start_matches("conduwuit_"),
|
||||
};
|
||||
|
||||
let flag = std::env::args().collect::<Vec<_>>();
|
||||
let flag_len = flag.len();
|
||||
let ret = quote! {
|
||||
pub static RUSTC_FLAGS: [&str; #flag_len] = [#( #flag ),*];
|
||||
|
||||
#[ctor]
|
||||
fn _set_rustc_flags() {
|
||||
conduwuit_core::info::rustc::FLAGS.lock().expect("locked").insert(#crate_name, &RUSTC_FLAGS);
|
||||
}
|
||||
|
||||
// static strings have to be yanked on module unload
|
||||
#[dtor]
|
||||
fn _unset_rustc_flags() {
|
||||
conduwuit_core::info::rustc::FLAGS.lock().expect("locked").remove(#crate_name);
|
||||
}
|
||||
};
|
||||
|
||||
ret.into()
|
||||
}
|
||||
@@ -207,7 +207,6 @@ clap.workspace = true
|
||||
console-subscriber.optional = true
|
||||
console-subscriber.workspace = true
|
||||
const-str.workspace = true
|
||||
ctor.workspace = true
|
||||
log.workspace = true
|
||||
opentelemetry.optional = true
|
||||
opentelemetry.workspace = true
|
||||
@@ -231,6 +230,7 @@ tracing-opentelemetry.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-journald = { workspace = true, optional = true }
|
||||
parking_lot.workspace = true
|
||||
|
||||
|
||||
[target.'cfg(all(not(target_env = "msvc"), target_os = "linux"))'.dependencies]
|
||||
|
||||
36
src/main/deadlock.rs
Normal file
36
src/main/deadlock.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use std::{thread, time::Duration};
|
||||
|
||||
/// Runs a loop that checks for deadlocks every 10 seconds.
|
||||
///
|
||||
/// Note that this requires the `deadlock_detection` parking_lot feature to be
|
||||
/// enabled.
|
||||
pub(crate) fn deadlock_detection_thread() {
|
||||
loop {
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
let deadlocks = parking_lot::deadlock::check_deadlock();
|
||||
if deadlocks.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
eprintln!("{} deadlocks detected", deadlocks.len());
|
||||
for (i, threads) in deadlocks.iter().enumerate() {
|
||||
eprintln!("Deadlock #{i}");
|
||||
for t in threads {
|
||||
eprintln!("Thread Id {:#?}", t.thread_id());
|
||||
eprintln!("{:#?}", t.backtrace());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns the deadlock detection thread.
|
||||
///
|
||||
/// This thread will run in the background and check for deadlocks every 10
|
||||
/// seconds. When a deadlock is detected, it will print detailed information to
|
||||
/// stderr.
|
||||
pub(crate) fn spawn() {
|
||||
thread::Builder::new()
|
||||
.name("deadlock_detector".to_owned())
|
||||
.spawn(deadlock_detection_thread)
|
||||
.expect("failed to spawn deadlock detection thread");
|
||||
}
|
||||
@@ -2,32 +2,35 @@
|
||||
|
||||
use std::sync::{Arc, atomic::Ordering};
|
||||
|
||||
use conduwuit_core::{debug_info, error, rustc_flags_capture};
|
||||
use conduwuit_core::{debug_info, error};
|
||||
|
||||
mod clap;
|
||||
mod deadlock;
|
||||
mod logging;
|
||||
mod mods;
|
||||
mod panic;
|
||||
mod restart;
|
||||
mod runtime;
|
||||
mod sentry;
|
||||
mod server;
|
||||
mod signal;
|
||||
|
||||
use ctor::{ctor, dtor};
|
||||
use server::Server;
|
||||
|
||||
rustc_flags_capture! {}
|
||||
|
||||
pub use conduwuit_core::{Error, Result};
|
||||
use server::Server;
|
||||
|
||||
pub use crate::clap::Args;
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
panic::init();
|
||||
|
||||
let args = clap::parse();
|
||||
run_with_args(&args)
|
||||
}
|
||||
|
||||
pub fn run_with_args(args: &Args) -> Result<()> {
|
||||
// Spawn deadlock detection thread
|
||||
deadlock::spawn();
|
||||
|
||||
let runtime = runtime::new(args)?;
|
||||
let server = Server::new(args, Some(runtime.handle()))?;
|
||||
|
||||
|
||||
34
src/main/panic.rs
Normal file
34
src/main/panic.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::{backtrace::Backtrace, panic};
|
||||
|
||||
/// Initialize the panic hook to capture backtraces at the point of panic.
|
||||
/// This is needed to capture the backtrace before the unwind destroys it.
|
||||
pub(crate) fn init() {
|
||||
let default_hook = panic::take_hook();
|
||||
|
||||
panic::set_hook(Box::new(move |info| {
|
||||
let backtrace = Backtrace::force_capture();
|
||||
|
||||
let location_str = info.location().map_or_else(String::new, |loc| {
|
||||
format!(" at {}:{}:{}", loc.file(), loc.line(), loc.column())
|
||||
});
|
||||
|
||||
let message = if let Some(s) = info.payload().downcast_ref::<&str>() {
|
||||
(*s).to_owned()
|
||||
} else if let Some(s) = info.payload().downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else {
|
||||
"Box<dyn Any>".to_owned()
|
||||
};
|
||||
|
||||
let thread_name = std::thread::current()
|
||||
.name()
|
||||
.map_or_else(|| "<unnamed>".to_owned(), ToOwned::to_owned);
|
||||
|
||||
eprintln!(
|
||||
"\nthread '{thread_name}' panicked{location_str}: \
|
||||
{message}\n\nBacktrace:\n{backtrace}"
|
||||
);
|
||||
|
||||
default_hook(info);
|
||||
}));
|
||||
}
|
||||
@@ -122,7 +122,6 @@ tokio.workspace = true
|
||||
tower.workspace = true
|
||||
tower-http.workspace = true
|
||||
tracing.workspace = true
|
||||
ctor.workspace = true
|
||||
|
||||
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
|
||||
sd-notify.workspace = true
|
||||
|
||||
@@ -12,12 +12,10 @@
|
||||
|
||||
use conduwuit::{Error, Result, Server};
|
||||
use conduwuit_service::Services;
|
||||
use ctor::{ctor, dtor};
|
||||
use futures::{Future, FutureExt, TryFutureExt};
|
||||
|
||||
conduwuit::mod_ctor! {}
|
||||
conduwuit::mod_dtor! {}
|
||||
conduwuit::rustc_flags_capture! {}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "Rust" fn start(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user