Compare commits

..
88 changed files with 1276 additions and 2775 deletions
@@ -44,7 +44,7 @@ runs:
- name: Login to builtin registry
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4
with:
registry: ${{ env.BUILTIN_REGISTRY }}
username: ${{ inputs.registry_user }}
@@ -79,7 +79,7 @@ runs:
- name: Login to builtin registry
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4
with:
registry: ${{ env.BUILTIN_REGISTRY }}
username: ${{ inputs.registry_user }}
+1 -1
View File
@@ -71,7 +71,7 @@ runs:
- name: Install timelord-cli and git-warp-time
if: steps.check-binaries.outputs.need-install == 'true'
uses: https://github.com/taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2
uses: https://github.com/taiki-e/install-action@41049aa56687c35e0afa74eed4f09cec4f9afabf # v2
with:
tool: git-warp-time,timelord-cli@3.0.1
+1 -1
View File
@@ -55,7 +55,7 @@ jobs:
# repositories: continuwuity
- name: Install regsync
uses: https://github.com/regclient/actions/regsync-installer@5c882eb04fcca27ebb4f5904e0da01f0780063ea # main
uses: https://github.com/regclient/actions/regsync-installer@c48159175b58fb90cccd0f98410927a81e04a166 # main
- name: Check what images need mirroring
run: |
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:43.288.0@sha256:bcde7a061ab42d1ae6d4500769d80727e5694bb889dbcbf1005e9f4e741ab76d
image: ghcr.io/renovatebot/renovate:43.281.1@sha256:34c2dd58f58e8976be2024a24fec23bbee805f0bf887837d9aaee7daeb09ccfc
options: --tmpfs /tmp:exec
steps:
- name: Checkout
+1 -1
View File
@@ -24,7 +24,7 @@ repos:
- id: check-added-large-files
- repo: https://github.com/crate-ci/typos
rev: v1.49.0
rev: v1.48.0
hooks:
- id: typos
- id: typos
+115 -102
View File
@@ -1,11 +1,3 @@
# Continuwuity v26.7.2 (2026-07-30)
## Bugfixes
- Sliding sync no longer continues streaming data to a user after they are removed from a room. Contributed by @eleboucher. (SEC10)
- Valid redaction events no longer arbitrarily soft-fail and are applied correctly as expected.
- Fixed a regression that caused the server to process events for rooms it no longer has any local users in, which caused users to be reset back *into* rooms.
# Continuwuity 26.7.0 (2026-07-27)
## Features
@@ -21,8 +13,7 @@ ## Features
## Bugfixes
- Fix joining restricted rooms over federation failing with signature verification error. (fix-federation-signature)
- Fixed the client space hierarchy endpoint returning a 500 "Space hierarchy is unreasonably large" error for cyclic space graphs (e.g. a space containing itself). Rooms are now deduplicated during
traversal as required by the spec, and the traversal depth is bounded even when the client does not specify `max_depth`. (space-hierarchy-cycle)
- Fixed the client space hierarchy endpoint returning a 500 "Space hierarchy is unreasonably large" error for cyclic space graphs (e.g. a space containing itself). Rooms are now deduplicated during traversal as required by the spec, and the traversal depth is bounded even when the client does not specify `max_depth`. (space-hierarchy-cycle)
- Fixed simplified sliding sync holding account data for up to 30 seconds, which made encryption setup and cross-signing resets appear to hang. (sss-account-data-longpoll)
- Fixed local invites and invite acceptances not being reflected in sync promptly. Contributed by @eleboucher (wake-local-member-sync)
- Fixed the deeplink redirect for deleting devices. Contributed by @koen (#1965)
@@ -30,8 +21,7 @@ ## Bugfixes
- Exempt m.room.create from auth_events check. Contributed by @eleboucher (#1987)
- Fixed `create` being returned as a supported prompt value regardless of if registration is enabled or not. Contributed by @ginger (#1994)
- Fixed high CPU usage when multiple clients from the same account were connected at once. Each sync woke the account's other sync loops, causing them to wake each other in a loop. (#2006)
- Fixed MSC4190 appservice device creation registering a random device ID instead of the requested one (and dropping the requested display name), which prevented encrypted mautrix bridges from
starting on OIDC-enabled servers and leaked an orphan device on the bridge bot per startup attempt. (#2015)
- Fixed MSC4190 appservice device creation registering a random device ID instead of the requested one (and dropping the requested display name), which prevented encrypted mautrix bridges from starting on OIDC-enabled servers and leaked an orphan device on the bridge bot per startup attempt. (#2015)
- Deactivated users and appservice puppets are no longer counted by `/_continuwuity/local_user_count`. Contributed by @ginger. (#2040)
- Re-introduced admin room registration alerts that were accidentally removed in the OAuth2 update. (#2057)
- Appservices are now properly able to create devices for E2EE.
@@ -40,22 +30,22 @@ ## Bugfixes
- Fixed freshly left room failing to sync.
- Fixed newly created rooms failing to sync properly in clients using legacy sync.
- Fixed newly joined rooms failing to sync their full state (including the room name) to clients using legacy sync.
- Fixed requests returning `500 Internal Server Error` when the header selected by `request_ip_source` is absent, duplicated, or malformed (for example Envoy omitting `X-Envoy-External-Address` on
internal requests). The client IP now falls back to the connection peer address instead of failing the request. Contributed by @eleboucher
- Fixed requests returning `500 Internal Server Error` when the header selected by `request_ip_source` is absent, duplicated, or malformed (for example Envoy omitting `X-Envoy-External-Address` on internal requests). The client IP now falls back to the connection peer address instead of failing the request. Contributed by @eleboucher
- Resolve alias service by correct name for auto-join. Contributed by @eleboucher
## Improved Documentation
- Updated an out-of-date statement about Oracle Linux release cadences. (#1999)
# Continuwuity 26.6.2 (2026-07-12)
## Bugfixes
- Fixed the server returning 500 errors if `admin_console_automatic` is enabled and no TTY is available. Contributed by @s1lv3r. (#1975)
- Fixed `global.oauth.compatibility_mode` being required, despite being ignored, when the `[global.oauth.oidc]` config section is provided.
- Fixed an issue with a migration that could cause user accounts imported from an identity provider to be marked as deactivated when the server started. If you have accounts affected by this issue,
use `!admin users reset-password --convert-to-local-account` to reactivate them.
- Fixed an issue with a migration that could cause user accounts imported from an identity provider to be marked as deactivated when the server started. If you have accounts affected by this issue, use `!admin users reset-password --convert-to-local-account` to reactivate them.
# Continuwuity 26.6.1 (2026-07-12)
@@ -71,6 +61,7 @@ ## Bugfixes
- Stopped appservice users from being erroneously marked as deactivated during a 26.6 database migration.
- Whitespace will now automatically be trimmed from the start and end of the `global.oauth.oidc.client_secret_file`.
# Continuwuity 26.6.0 (2026-07-10)
## Features
@@ -80,15 +71,19 @@ ## Features
- Added config option for default room ACLs. Contributed by @eve. (#1691)
- Added support for fallback encryption keys. (#1710)
- Add `!admin users reject-all-invites` to clean invite spam (#1741)
- Implemented event rejection, which should resolve and prevent future netsplits of the kinds observed within some Continuwuity rooms. Also resolved several bugs related to both soft-failing events,
and event backfilling, which should improve state resolution stability. The `!admin debug get-pdu` command was updated to disambiguate event acceptance status, and
`!admin debug show-auth-chain` was added to visually display event auth chains, which may assist developers in debugging strangely complex events.
- Implemented event rejection, which should resolve and prevent future netsplits of the kinds observed
within some Continuwuity rooms.
Also resolved several bugs related to both soft-failing events, and event backfilling, which should
improve state resolution stability.
The `!admin debug get-pdu` command was updated to disambiguate event acceptance status, and
`!admin debug show-auth-chain` was added to visually display event auth chains, which may assist
developers in debugging strangely complex events.
Contributed by @nex. (#1747)
- Added full support for [MSC4168: Update `m.space.*` state on room upgrade](https://github.com/matrix-org/matrix-spec-proposals/pull/4168). Contributed by @nex. (#1807)
- Improved the performance and reliability of fetching missing events, improving network partition recovery. Contributed by @nex. (#1818)
- Added static builds using Nix, allowing for Continuwuity on musl. During this, we also introduced a `max-perf-haswell` package, separating it from `max-perf`, so you may want to swap to this if you
are on NixOS. Contributed by @Henry-Hiles (QuadRadical). (#1853)
- Improved the performance and reliability of fetching missing events, improving network partition recovery. Contributed
by @nex. (#1818)
- Added static builds using Nix, allowing for Continuwuity on musl. During this, we also introduced a `max-perf-haswell` package, separating it from `max-perf`, so you may want to swap to this if you are on NixOS. Contributed by @Henry-Hiles (QuadRadical). (#1853)
- Added support for MSC4380 invite blocking, which has become part of the Matrix specification in v1.18. Contributed by @nex. (#1875)
- Added `!admin debug get-state-at` command (#1877)
- Added a configuration option to allow choosing a client IP source that is not the TCP connecting IP. Contributed by @nex. (#1931)
@@ -100,12 +95,10 @@ ## Features
## Bugfixes
- Adjusted legacy sync logic to allow the `roomsynctoken_shortstatehash` database column to be dropped, massively reducing database sizes, especially for old deployments. Contributed by @ginger.
(#917)
- Adjusted legacy sync logic to allow the `roomsynctoken_shortstatehash` database column to be dropped, massively reducing database sizes, especially for old deployments. Contributed by @ginger. (#917)
- Fixed a bug that caused the server to drop events during processing if several events for the same room were sent in a singular transaction. Contributed by @nex. (#1711)
- fix `!admin query account-data account-data-get` not returning the content (#1742)
- Fixed an issue where Continuwuity would only advertise support for the unstable endpoint for Mutual Rooms (MSC2666), despite only supporting the stable endpoint. Contributed by @Henry-Hiles
(QuadRadical) (#1752)
- Fixed an issue where Continuwuity would only advertise support for the unstable endpoint for Mutual Rooms (MSC2666), despite only supporting the stable endpoint. Contributed by @Henry-Hiles (QuadRadical) (#1752)
- Fixed admin commands being ignored when they had leading whitespace before admin commands. Contributed by @kitvonsnookerz. (#1804)
- Fixed several bugs in the `POST /_matrix/client/v3/rooms/{roomId}/upgrade` endpoint. Contributed by @nex. (#1807)
- Devices which set their presence as "offline" will no longer be considered for presence updates. Contributed by @timedout.
@@ -132,8 +125,8 @@ ## Misc
- #1505, #1829, #1927, #1933, #1934
- Switched from Continuwuity's fork of Ruma back to upstream Ruma. Contributed by @ginger.
- The version of Debian that the Docker-based build process uses has been upgraded from Bookworm to Trixie, meaning that standalone binaries now have a minimum glibc of 2.41, and can no longer be used
on distro versions from before 2025-01-30
- The version of Debian that the Docker-based build process uses has been upgraded from Bookworm to Trixie, meaning that standalone binaries now have a minimum glibc of 2.41, and can no longer be used on distro versions from before 2025-01-30
# Continuwuity 0.5.8 (2026-04-24)
@@ -151,6 +144,7 @@ ## Improved Documentation
- Updated config docs to state we support room version 12, and set it as default. Contributed by @ezera. (#1622)
- Improve instructions for generic deployments, removing unnecessary parts and documenting the new initial registration token flow. Contributed by @stratself (#1677)
# Continuwuity v0.5.7 (2026-04-17)
## Features
@@ -192,6 +186,7 @@ ## Misc
- Fixed compiler warning in cf_opts.rs when building in release. Contributed by @ezera. (#1620)
# Continuwuity 0.5.6 (2026-03-03)
## Security
@@ -201,33 +196,22 @@ ## Security
## Features
- Outgoing presence is now disabled by default, and the config option documentation has been adjusted to more accurately represent the weight of presence, typing indicators, and read receipts.
Contributed by @nex. ([#1399](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1399))
- Improved the concurrency handling of federation transactions, vastly improving performance and reliability by more accurately handling inbound transactions and reducing the amount of repeated wasted
work. Contributed by @nex and @Jade. ([#1428](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1428))
- Added [MSC3202](https://github.com/matrix-org/matrix-spec-proposals/pull/3202) Device masquerading (not all of MSC3202). This should fix issues with
enabling [MSC4190](https://github.com/matrix-org/matrix-spec-proposals/pull/4190) for some Mautrix bridges. Contributed by @Jade
([#1435](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1435))
- Added [MSC3814](https://github.com/matrix-org/matrix-spec-proposals/pull/3814) Dehydrated Devices - you can now decrypt messages sent while all devices were logged out.
([#1436](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1436))
- Implement [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143) MatrixRTC transport discovery endpoint. Move RTC foci configuration from `[global.well_known]` to a new
`[global.matrix_rtc]` section with a `foci` field. Contributed by @0xnim ([#1442](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1442))
- Outgoing presence is now disabled by default, and the config option documentation has been adjusted to more accurately represent the weight of presence, typing indicators, and read receipts. Contributed by @nex. ([#1399](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1399))
- Improved the concurrency handling of federation transactions, vastly improving performance and reliability by more accurately handling inbound transactions and reducing the amount of repeated wasted work. Contributed by @nex and @Jade. ([#1428](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1428))
- Added [MSC3202](https://github.com/matrix-org/matrix-spec-proposals/pull/3202) Device masquerading (not all of MSC3202). This should fix issues with enabling [MSC4190](https://github.com/matrix-org/matrix-spec-proposals/pull/4190) for some Mautrix bridges. Contributed by @Jade ([#1435](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1435))
- Added [MSC3814](https://github.com/matrix-org/matrix-spec-proposals/pull/3814) Dehydrated Devices - you can now decrypt messages sent while all devices were logged out. ([#1436](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1436))
- Implement [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143) MatrixRTC transport discovery endpoint. Move RTC foci configuration from `[global.well_known]` to a new `[global.matrix_rtc]` section with a `foci` field. Contributed by @0xnim ([#1442](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1442))
- Updated `list-backups` admin command to output one backup per line. ([#1394](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1394))
- Improved URL preview fetching with a more compatible user agent for sites like YouTube Music. Added `!admin media delete-url-preview <url>` command to clear cached URL previews that were stuck and
broken. ([#1434](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1434))
- Improved URL preview fetching with a more compatible user agent for sites like YouTube Music. Added `!admin media delete-url-preview <url>` command to clear cached URL previews that were stuck and broken. ([#1434](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1434))
## Bugfixes
- Removed non-compliant nor functional room alias lookups over federation. Contributed by @nex ([#1393](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1393))
- Removed ability to set rocksdb as read only. Doing so would cause unintentional and buggy behaviour. Contributed by @Terryiscool160.
([#1418](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1418))
- Fixed a startup crash in the sender service if we can't detect the number of CPU cores, even if the `sender_workers` config option is set correctly. Contributed by @katie.
([#1421](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1421))
- Removed ability to set rocksdb as read only. Doing so would cause unintentional and buggy behaviour. Contributed by @Terryiscool160. ([#1418](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1418))
- Fixed a startup crash in the sender service if we can't detect the number of CPU cores, even if the `sender_workers` config option is set correctly. Contributed by @katie. ([#1421](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1421))
- Removed the `allow_public_room_directory_without_auth` config option. Contributed by @0xnim. ([#1441](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1441))
- Fixed sliding sync v5 list ranges always starting from 0, causing extra rooms to be unnecessarily processed and returned. Contributed by @0xnim
([#1445](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1445))
- Fixed a bug that (repairably) caused a room split between continuwuity and non-continuwuity servers when the room had both `m.room.policy` and `org.matrix.msc4284.policy` in its room state.
Contributed by @nex ([#1481](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1481))
- Fixed sliding sync v5 list ranges always starting from 0, causing extra rooms to be unnecessarily processed and returned. Contributed by @0xnim ([#1445](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1445))
- Fixed a bug that (repairably) caused a room split between continuwuity and non-continuwuity servers when the room had both `m.room.policy` and `org.matrix.msc4284.policy` in its room state. Contributed by @nex ([#1481](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1481))
- Fixed `!admin media delete --mxc <url>` responding with an error message when the media was deleted successfully. Contributed by @lynxize
- Fixed spurious 404 media errors in the logs. Contributed by @benbot.
- Fixed spurious warn about needed backfill via federation for non-federated rooms. Contributed by @kraem.
@@ -237,15 +221,19 @@ # 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))
`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.
- 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.
@@ -253,10 +241,10 @@ ## Features
## 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 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
@@ -267,78 +255,100 @@ # 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))
- 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).
- 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))
- 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](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/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](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/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](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/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 ([#1278](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1278))
- 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](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))
- 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.
- 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](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))
- 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)
@@ -347,9 +357,12 @@ # Continuwuity 0.5.0 (2025-12-30)
## Features
- Enabled the OTLP exporter in default builds, and allow configuring the exporter protocol. (@Jade). ([#1251](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/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](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))
- 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))
Generated
+25 -25
View File
@@ -816,7 +816,7 @@ dependencies = [
[[package]]
name = "conduwuit"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"aws-lc-rs",
"clap",
@@ -854,7 +854,7 @@ dependencies = [
[[package]]
name = "conduwuit_admin"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"assign",
"clap",
@@ -880,7 +880,7 @@ dependencies = [
[[package]]
name = "conduwuit_api"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"assign",
"async-trait",
@@ -918,7 +918,7 @@ dependencies = [
[[package]]
name = "conduwuit_build_metadata"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"built",
"cargo_metadata",
@@ -926,7 +926,7 @@ dependencies = [
[[package]]
name = "conduwuit_core"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"argon2",
"arrayvec",
@@ -994,7 +994,7 @@ dependencies = [
[[package]]
name = "conduwuit_database"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"async-channel",
"conduwuit_core",
@@ -1015,7 +1015,7 @@ dependencies = [
[[package]]
name = "conduwuit_macros"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"cargo_toml",
"itertools 0.15.0",
@@ -1026,7 +1026,7 @@ dependencies = [
[[package]]
name = "conduwuit_router"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"assign",
"axum",
@@ -1063,7 +1063,7 @@ dependencies = [
[[package]]
name = "conduwuit_service"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"askama",
"assign",
@@ -1115,7 +1115,7 @@ dependencies = [
[[package]]
name = "conduwuit_web"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"askama",
"assign",
@@ -4505,9 +4505,9 @@ checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
[[package]]
name = "resolvematrix"
version = "1.3.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f072176a8b0cbe7c3df2281991ef00a09889d8ec789f2878e7fee43a3b978a6f"
checksum = "a68b9735d8c1096d8152d4dfb5a705c0135dd14d181542fd87301f8d3cb4ed03"
dependencies = [
"futures",
"hickory-resolver",
@@ -4567,7 +4567,7 @@ dependencies = [
[[package]]
name = "ruma"
version = "0.16.0"
source = "git+https://github.com/ruma/ruma.git?rev=c1100793d2fd464be34052773360f3f3773d0fc4#c1100793d2fd464be34052773360f3f3773d0fc4"
source = "git+https://github.com/ruma/ruma.git?rev=e7284c31da289f0a3b885191f5b2b6b307fa059f#e7284c31da289f0a3b885191f5b2b6b307fa059f"
dependencies = [
"assign",
"js_int",
@@ -4586,7 +4586,7 @@ dependencies = [
[[package]]
name = "ruma-appservice-api"
version = "0.16.0"
source = "git+https://github.com/ruma/ruma.git?rev=c1100793d2fd464be34052773360f3f3773d0fc4#c1100793d2fd464be34052773360f3f3773d0fc4"
source = "git+https://github.com/ruma/ruma.git?rev=e7284c31da289f0a3b885191f5b2b6b307fa059f#e7284c31da289f0a3b885191f5b2b6b307fa059f"
dependencies = [
"js_int",
"ruma-common",
@@ -4598,7 +4598,7 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.24.0"
source = "git+https://github.com/ruma/ruma.git?rev=c1100793d2fd464be34052773360f3f3773d0fc4#c1100793d2fd464be34052773360f3f3773d0fc4"
source = "git+https://github.com/ruma/ruma.git?rev=e7284c31da289f0a3b885191f5b2b6b307fa059f#e7284c31da289f0a3b885191f5b2b6b307fa059f"
dependencies = [
"as_variant",
"assign",
@@ -4620,7 +4620,7 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.19.0"
source = "git+https://github.com/ruma/ruma.git?rev=c1100793d2fd464be34052773360f3f3773d0fc4#c1100793d2fd464be34052773360f3f3773d0fc4"
source = "git+https://github.com/ruma/ruma.git?rev=e7284c31da289f0a3b885191f5b2b6b307fa059f#e7284c31da289f0a3b885191f5b2b6b307fa059f"
dependencies = [
"as_variant",
"base64 0.22.1",
@@ -4653,7 +4653,7 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.34.0"
source = "git+https://github.com/ruma/ruma.git?rev=c1100793d2fd464be34052773360f3f3773d0fc4#c1100793d2fd464be34052773360f3f3773d0fc4"
source = "git+https://github.com/ruma/ruma.git?rev=e7284c31da289f0a3b885191f5b2b6b307fa059f#e7284c31da289f0a3b885191f5b2b6b307fa059f"
dependencies = [
"as_variant",
"indexmap 2.14.0",
@@ -4674,7 +4674,7 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
version = "0.15.0"
source = "git+https://github.com/ruma/ruma.git?rev=c1100793d2fd464be34052773360f3f3773d0fc4#c1100793d2fd464be34052773360f3f3773d0fc4"
source = "git+https://github.com/ruma/ruma.git?rev=e7284c31da289f0a3b885191f5b2b6b307fa059f#e7284c31da289f0a3b885191f5b2b6b307fa059f"
dependencies = [
"bytes",
"headers",
@@ -4697,7 +4697,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.12.1"
source = "git+https://github.com/ruma/ruma.git?rev=c1100793d2fd464be34052773360f3f3773d0fc4#c1100793d2fd464be34052773360f3f3773d0fc4"
source = "git+https://github.com/ruma/ruma.git?rev=e7284c31da289f0a3b885191f5b2b6b307fa059f#e7284c31da289f0a3b885191f5b2b6b307fa059f"
dependencies = [
"js_int",
"thiserror 2.0.19",
@@ -4706,7 +4706,7 @@ dependencies = [
[[package]]
name = "ruma-macros"
version = "0.19.0"
source = "git+https://github.com/ruma/ruma.git?rev=c1100793d2fd464be34052773360f3f3773d0fc4#c1100793d2fd464be34052773360f3f3773d0fc4"
source = "git+https://github.com/ruma/ruma.git?rev=e7284c31da289f0a3b885191f5b2b6b307fa059f#e7284c31da289f0a3b885191f5b2b6b307fa059f"
dependencies = [
"as_variant",
"cfg-if",
@@ -4722,7 +4722,7 @@ dependencies = [
[[package]]
name = "ruma-push-gateway-api"
version = "0.15.0"
source = "git+https://github.com/ruma/ruma.git?rev=c1100793d2fd464be34052773360f3f3773d0fc4#c1100793d2fd464be34052773360f3f3773d0fc4"
source = "git+https://github.com/ruma/ruma.git?rev=e7284c31da289f0a3b885191f5b2b6b307fa059f#e7284c31da289f0a3b885191f5b2b6b307fa059f"
dependencies = [
"js_int",
"ruma-common",
@@ -4734,7 +4734,7 @@ dependencies = [
[[package]]
name = "ruma-signatures"
version = "0.21.0"
source = "git+https://github.com/ruma/ruma.git?rev=c1100793d2fd464be34052773360f3f3773d0fc4#c1100793d2fd464be34052773360f3f3773d0fc4"
source = "git+https://github.com/ruma/ruma.git?rev=e7284c31da289f0a3b885191f5b2b6b307fa059f#e7284c31da289f0a3b885191f5b2b6b307fa059f"
dependencies = [
"base64 0.22.1",
"ed25519-dalek",
@@ -4751,7 +4751,7 @@ dependencies = [
[[package]]
name = "ruma-state-res"
version = "0.17.0"
source = "git+https://github.com/ruma/ruma.git?rev=c1100793d2fd464be34052773360f3f3773d0fc4#c1100793d2fd464be34052773360f3f3773d0fc4"
source = "git+https://github.com/ruma/ruma.git?rev=e7284c31da289f0a3b885191f5b2b6b307fa059f#e7284c31da289f0a3b885191f5b2b6b307fa059f"
dependencies = [
"js_int",
"ruma-common",
@@ -4765,7 +4765,7 @@ dependencies = [
[[package]]
name = "ruminuwuity"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"assign",
"ruma",
@@ -6874,7 +6874,7 @@ dependencies = [
[[package]]
name = "xtask"
version = "26.7.2"
version = "26.7.1"
dependencies = [
"askama",
"cargo_metadata",
+3 -5
View File
@@ -12,7 +12,7 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "26.7.2"
version = "26.7.1"
[workspace.metadata.crane]
name = "conduwuit"
@@ -343,7 +343,7 @@ version = "1.1.1"
# Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma]
git = "https://github.com/ruma/ruma.git"
rev = "c1100793d2fd464be34052773360f3f3773d0fc4"
rev = "e7284c31da289f0a3b885191f5b2b6b307fa059f"
features = [
"appservice-api-c",
"client-api",
@@ -377,8 +377,6 @@ features = [
"unstable-msc4293",
"unstable-msc4406",
"unstable-msc4439",
"unstable-msc4354",
"unstable-msc4480",
"unstable-msc4466",
"unstable-extensible-events",
]
@@ -564,7 +562,7 @@ features = ["std"]
version = "0.3.0"
[workspace.dependencies.resolvematrix]
version = "1.3.0"
version = "1.2.0"
[workspace.dependencies.serde_urlencoded]
version = "0.7.1"
-1
View File
@@ -1 +0,0 @@
Update backup instructions: the right paths for `.sst` files are now in `$DATABASE_BACKUP_PATH/private/<number>` path instead of `$DATABASE_BACKUP_PATH/<number>`. Contributed by @stratself
-1
View File
@@ -1 +0,0 @@
Added MSC4480 sticky events to simplified sliding sync. Contributed by @eleboucher.
-1
View File
@@ -1 +0,0 @@
Replace deprecated `docker-compose` commands with `docker compose` to reflect latest Docker changes.
-1
View File
@@ -1 +0,0 @@
Fixed typing notifications not interrupting a sync long poll. Contributed by @eleboucher.
-1
View File
@@ -1 +0,0 @@
Updated Ruma dependencies. Contributed by @eleboucher.
-1
View File
@@ -1 +0,0 @@
Fixed missed wake-ups and stale responses in legacy and sliding sync long polls. Contributed by @eleboucher.
-1
View File
@@ -1 +0,0 @@
Livekit docs: `room.auto_create: false` is now included in default `livekit.yaml`, following `lk-jwt-service` recommendation.
-1
View File
@@ -1 +0,0 @@
Add caveat and workaround for container resolution (e.g. for dockerized appservices) when using mounted `/etc/resolv.conf`. Contributed by @stratself
-1
View File
@@ -1 +0,0 @@
Added MSC4354 sticky events, behind the `allow_sticky_events` config option. Contributed by @eleboucher.
-1
View File
@@ -1 +0,0 @@
Added MSC4186 direct, encrypted, and room-type list filters, resolving invited rooms from their stripped invite state. Contributed by @eleboucher.
-1
View File
@@ -1 +0,0 @@
Refactor TURN docs and remove unsafe setups. Polish LiveKit docs. Add guidance for TURNS-over-443 multiplexing for both LiveKit and legacy calls. Contributed by @stratself
-1
View File
@@ -1 +0,0 @@
Updated the docs to reflect the current default User-Agent for URL previews
-1
View File
@@ -1 +0,0 @@
Resolve membership service by correct name for auto-join. Contributed by @Aranjedeath
-1
View File
@@ -1 +0,0 @@
Plugged a logic gap that may have been allowing Continuwuity to process events with incorrect room versions under certain conditions. Contributed by @nex.
+1
View File
@@ -0,0 +1 @@
**TODO - embargoed until next release** (maintainers see security issue 10 when writing rls notes). Contributed by @eleboucher.
+10 -7
View File
@@ -1306,12 +1306,6 @@
#
#allow_incoming_typing = true
# Allow the use of unstable persistent "sticky events" ([MSC4354])
#
# MSC4354: https://c10y.cc/MSC4354
#
#allow_sticky_events = false
# Maximum time federation user can indicate typing.
#
#typing_federation_timeout_s = 30
@@ -1492,6 +1486,15 @@
#
#send_messages_from_ignored_users_to_client = false
# Disables strict validation on incoming federated invites. This weakens
# your server's security - only enable it if you encounter issues
# receiving invites from outdated servers.
#
# Validation will still be performed, but it will log a warning instead of
# causing an error.
#
#enable_legacy_invite_support = false
# Send "org.matrix.dummy_event" events to the client. This is a debugging
# option.
#
@@ -1601,7 +1604,7 @@
# User agent that is used specifically when fetching url previews.
#
#url_preview_user_agent = "continuwuity/<version> (embedbot; facebookexternalhit/1.1; +https://continuwuity.org)"
#url_preview_user_agent = "continuwuity/<version> (bot; +https://continuwuity.org)"
# Determines whether audio and video files will be downloaded for URL
# previews.
+11 -19
View File
@@ -20,19 +20,17 @@ ## Set up the appservice - general instructions
registered on the homeserver automatically joins it. Then send a message into
the room like this:
~~~txt
!admin appservices register
```
paste
the
contents
of
the
yaml
registration
here
```
~~~
!admin appservices register
```
paste
the
contents
of
the
yaml
registration
here
```
You can confirm it worked by sending a message like this:
`!admin appservices list`
@@ -53,9 +51,3 @@ ### Remove an appservice
`!admin appservices unregister <name>`
where `<name>` one of the output of `appservices list`.
## Caveats
### Docker DNS
If you follow the [DNS tuning guide](./guides/dns.mdx#for-docker-users) and mount a custom `/etc/resolv.conf`, you may not be able to connect to appservices' containers using their hostnames. Follow the [workaround in the guide](./guides/dns.mdx#for-docker-users) to connect to your appservice using static IPs.
+3 -3
View File
@@ -3,11 +3,11 @@ # Calls
Matrix supports two types of calls:
- Element Call powered by [MatrixRTC](https://half-shot.github.io/msc-crafter/#msc/4143) and [LiveKit](https://github.com/livekit/livekit)
- Legacy calls, supported by a STUN/TURN server.
- Legacy calls, sometimes using Jitsi
Both types of calls are supported by different sets of clients, but most clients are moving towards MatrixRTC / Element Call.
For either one to work correctly, you have to do some additional setup:
For either one to work correctly, you have to do some additional setup.
- For legacy calls to work, you need to set up a TURN/STUN server. [Read the TURN guide for tips on how to set up coturn](./calls/turn.mdx)
- For MatrixRTC / Element Call to work, you have to set up the LiveKit backend. LiveKit also uses TURN/STUN to increase reliability - you can set up its built-in TURN server, or integrate with an existing one. [Read the LiveKit guide](./calls/livekit.mdx)
- For MatrixRTC / Element Call to work, you have to set up the LiveKit backend (foci). LiveKit also uses TURN/STUN to increase reliability - you can set up its built-in TURN server, or integrate with an existing one. [Read the LiveKit guide](./calls/livekit.mdx)
+87 -193
View File
@@ -1,6 +1,8 @@
# MatrixRTC/Element Call Setup
# Matrix RTC/Element Call Setup
This guide assumes that you are using docker compose for deployment.
:::info
This guide assumes that you are using docker compose for deployment. LiveKit only provides Docker images.
:::
:::tip
You can find help setting up MatrixRTC in our dedicated room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
@@ -8,15 +10,15 @@ # MatrixRTC/Element Call Setup
## Instructions
### 1. Set up your domain
### 1. Domain
LiveKit should live on its own domain or subdomain. In this guide we use `livekit.example.com` - this should be replaced with a domain you control.
Make sure the DNS record for the (sub)domain you plan to use is pointed to your server.
### 2. Set up the LiveKit services
### 2. Services
Using LiveKit with Matrix requires two services - LiveKit itself, and a brokering service (`lk-jwt-service`) that grants Matrix users permission to connect to it.
Using LiveKit with Matrix requires two services - LiveKit itself, and a service (`lk-jwt-service`) that grants Matrix users permission to connect to it.
You must generate a key and secret to allow the Matrix service to authenticate with LiveKit. `LK_MATRIX_KEY` should be around 20 random characters, and `LK_MATRIX_SECRET` should be around 64. Remember to replace these with the actual values!
@@ -79,23 +81,15 @@ # - "50100-50200:50100-50200/udp"
LK_MATRIX_KEY: LK_MATRIX_SECRET
# replace these with your key-secret pair. Example:
# APIUxUnMnSkuFWV: t93ZVjPeoEdyx7Wbet3kG4L3NGZIZVEFvqe0UuiVc22A
# do not create rooms by default
room:
auto_create: false
```
Consult [`config-sample.yaml`][livekit-config-sample-yaml] for all LiveKit options.
[livekit-config-sample-yaml]: https://github.com/livekit/livekit/blob/master/config-sample.yaml
#### Firewall hints
You will need to allow ports `7881/tcp` and `50100:50200/udp` through your firewall. If you use UFW, the commands are: `ufw allow 7881/tcp` and `ufw allow 50100:50200/udp`.
### 3. Telling clients where to find LiveKit
To tell clients where to find LiveKit, you need to add your `lk-jwt-service`'s address to the `[global.matrix_rtc] > foci` field of your Continuwuity config file.
To tell clients where to find LiveKit, you need to add the address of your `lk-jwt-service` to the `[global.matrix_rtc]` config section using the `foci` option.
The variable should be a list of servers serving as MatrixRTC endpoints. Replace the URL with the address you are deploying your instance of lk-jwt-service to:
@@ -106,12 +100,6 @@ ### 3. Telling clients where to find LiveKit
]
```
If you configure Continuwuity via environment variables, use the following:
```bash
CONTINUWUITY_MATRIX_RTC__FOCI=[{ type = "livekit", livekit_service_url = "https://livekit.example.com" }]
```
This will expose LiveKit information on the following endpoints for clients to discover:
- `/_matrix/client/unstable/org.matrix.msc4143/rtc/transports` (MSC4143 unstable, behind auth)
@@ -144,11 +132,6 @@ ### 4. Configure your Reverse Proxy
}
```
**Note**: if you run Caddy inside a container (e.g. by following the example [here](../deploying/docker.mdx#caddy-using-caddyfile)) instead of on the host, then:
- Put these containers on the same bridge network as caddy (by defining `networks: [ caddy ]` in each of the services), and
- Use appropriate container hostnames (`lk-jwt-service` and `livekit`) instead of `127.0.0.1` in the Caddyfile
</details>
<details>
@@ -204,7 +187,6 @@ ### 4. Configure your Reverse Proxy
```
</details>
<details>
<summary>Example docker compose file with caddy-docker-proxy labels</summary>
```yaml
@@ -280,9 +262,76 @@ ### 6. Start Everything
Start up the services using your usual method - for example `docker compose up -d`.
## Additional TURN configuration
### Using LiveKit's built-in TURN server
LiveKit includes a built-in TURN server which can be used in place of an external option. This TURN server will only work with LiveKit, so you can't use it for legacy Matrix calling or anything else.
If you don't want to set up a separate TURN server, you can enable this with the following changes:
```yaml
### add this to livekit.yaml ###
turn:
enabled: true
udp_port: 3478
relay_range_start: 50300
relay_range_end: 50400
domain: livekit.example.com
```
```yaml
### add these to livekit's docker-compose ###
ports:
- "3478:3478/udp"
- "50300-50400:50300-50400/udp"
### if you're using `network_mode: host`, you can skip this part
```
Recreate the LiveKit container (with `docker-compose up -d livekit`) to apply these changes. Remember to allow the new `3478/udp` and `50300:50400/udp` ports through your firewall.
### Integration with an external TURN server
If you've already [set up coturn](./turn), you can configure Livekit to use it.
:::tip Avoid port clashes between the two services
Before continuing, make sure coturn's `min-port` and `max-port` do not overlap with LiveKit's port range:
```ini
# in your coturn.conf
min-port=50201
max-port=65535
```
:::
Generate a long random secret for LiveKit, and add it to your coturn config under the `static-auth-secret` option. You can add as many secrets as you want, so set a different one for LiveKit to use.
Then configure LiveKit, making sure to replace `COTURN_SECRET` with the one you generated:
```yaml
# livekit.yaml
rtc:
turn_servers:
- host: coturn.example.com
port: 3478
protocol: udp
secret: "COTURN_SECRET"
- host: coturn.example.com
port: 3478
protocol: tcp
secret: "COTURN_SECRET"
- host: coturn.example.com
port: 5349
protocol: tls # Only if you have already set up TLS in your coturn
secret: "COTURN_SECRET"
```
Restart LiveKit and coturn to apply these changes.
## Testing
To test that LiveKit is successfully integrated with Continuwuity, you will need to replicate its [Token Exchange Flow](https://github.com/element-hq/lk-jwt-service#%EF%B8%8F-how-it-works--token-exchange-flow). Follow the steps below while checking Docker logs (`docker compose logs --follow`), in order to help [troubleshooting](#troubleshooting) any issues.
To test that LiveKit is successfully integrated with Continuwuity, you will need to replicate its [Token Exchange Flow](https://github.com/element-hq/lk-jwt-service#%EF%B8%8F-how-it-works--token-exchange-flow). Follow the steps below while checking Docker logs (`docker-compose logs --follow`), in order to help [troubleshooting](#troubleshooting) any issues.
First, you will need an access token for your current login session. These can be found in your client's settings or obtained via [this website](https://timedout.uk/mxtoken.html).
@@ -347,19 +396,16 @@ ## Testing
{"url":"wss://livekit.example.com","jwt":"a_really_really_long_string"}
```
Use this token to test at the [LiveKit Connection Tester][livekit-connection-test]. If everything works there, then you have set up LiveKit successfully!
[livekit-connection-test]: https://livekit.com/webrtc/connection-test
Use this token to test at the [LiveKit Connection Tester](https://livekit.io/connection-test). If everything works there, then you have set up LiveKit successfully!
## Troubleshooting
To debug any issues, you can place a call or redo the Testing instructions, and check the container logs for any specific errors. Use `docker compose logs --follow` to follow these logs in real-time.
To debug any issues, you can place a call or redo the Testing instructions, and check the container logs for any specific errors. Use `docker-compose logs --follow` to follow these logs in real-time.
### Common errors in Element Call UI
- `MISSING_MATRIX_RTC_FOCUS`/`MISSING_MATRIX_RTC_TRANSPORT`: LiveKit is missing from Continuwuity's config file
- `MISSING_MATRIX_RTC_FOCUS`: LiveKit is missing from Continuwuity's config file
- "Waiting for media" popup always showing: a LiveKit URL has been configured in Continuwuity, but your client cannot connect to it for some reason
- `OPEN_ID_ERROR`: Your client can reach out to `lk-jwt-service`, but has problems authenticating with it. In this case, check `lk-jwt-service` logs for more details
For browser-based clients, you can also inspect connections using DevTools' Networking tab, to see which requests are erroring out.
@@ -394,19 +440,6 @@ # --- some errors ---
After implementing the changes and restarting your compose, `lk-jwt-service` should now connect to your other services. The sidecar container test above should now return an `OK` from LiveKit.
### Incorrect IP address for LiveKit
By default, LiveKit auto-discovers its public IP address(es), which is reflected in the "Establishing WebRTC connection" section of the connection test page. If these IPs are incorrect, you may want to hardcode your own IP by doing the following:
```diff
### in your livekit.yaml ###
rtc:
# ... other configs here ...
- use_external_ip: true
+ use_external_ip: false
+ node_ip: "1.2.3.4"
```
### Workaround for non-federating servers
When deploying on servers with federation disabled (`allow_federation = false`), LiveKit will fail as it can't fetch the required [OpenID endpoint](https://spec.matrix.org/v1.17/server-server-api/#get_matrixfederationv1openiduserinfo) via federation paths.
@@ -425,156 +458,17 @@ ## Related Documentation
Guides:
- [Element Call self-hosting documentation from element-hq][element-call-selfhosting]
- [Community guide with overview of LiveKit's mechanisms][tom-livekit-guide]
- [Community guide using systemd][kimiblock-livekit-guide]
[element-call-selfhosting]: https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md
[tom-livekit-guide]: https://tomfos.tr/matrix/livekit/
[kimiblock-livekit-guide]: https://blog.kimiblock.top/2024/12/24/hosting-element-call/
Configurations:
- [Livekit's `config-sample.yaml`][livekit-config-sample-yaml] - LiveKit configuration file with full options
- [Element Call self-hosting documentation](https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md)
- [Community guide with overview of LiveKit's mechanisms](https://tomfos.tr/matrix/livekit/)
- [Community guide using systemd](https://blog.kimiblock.top/2024/12/24/hosting-element-call/)
Specifications:
- [MSC4143 - MatrixRTC proposal][MSC4143]
- [MSC4195 - LiveKit proposal][MSC4195]
[MSC4143]: https://github.com/matrix-org/matrix-spec-proposals/pull/4143
[MSC4195]: https://github.com/matrix-org/matrix-spec-proposals/pull/4195
- [MSC4143 - MatrixRTC proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
- [MSC4195 - LiveKit proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4195)
Source code:
- [Element Call][element-call-github]
- [lk-jwt-service][lk-jwt-service-github]
- [LiveKit server][livekit-server-github]
[element-call-github]: https://github.com/element-hq/element-call
[lk-jwt-service-github]: https://github.com/element-hq/lk-jwt-service
[livekit-server-github]: https://github.com/livekit/livekit
Other:
- [Matrix VOIP and LiveKit][sspaeth-matrix-voip] - Community member deep dive on current VOIP solutions on Matrix
[sspaeth-matrix-voip]: https://sspaeth.de/2026/04/matrix-voip-and-livekit/
## Appendix
### Additional TURNS-over-443 configuration
Most of the time, LiveKit [**does not need TURN**][sspaeth-matrix-voip-turn] to function. However, there are situations where clients are in very restrictive networks that disallows non-standard ports and UDP. In these cases, a TURN-over-TLS server on port :443 could be employed to relay traffic for them.
[sspaeth-matrix-voip-turn]: https://sspaeth.de/2026/04/matrix-voip-and-livekit/#turn
You can either use LiveKit's built-in TURN server, or integrate LiveKit with [coturn](./turn).
<details>
<summary>Using LiveKit's built-in TURN server</summary>
First, set up LiveKit's built-in TURN server with its own domain - we'll use `livekit-turn.example.com` in our example.
```yaml
## add this to `livekit.yaml` ##
turn:
enabled: true
# note: the TLS port will always be advertised as :443
tls_port: 5349
# optional: configure an extra UDP port on :3478
# udp_port: 3478
relay_range_start: 50300
relay_range_end: 50400
domain: livekit-turn.example.com
# replace these with your actual cert/key files
cert_file: /path/to/livekit-turn.example.com.crt
key_file: /path/to/livekit-turn.example.com.key
```
```yaml
### add these ports to livekit's docker-compose ###
### if you're using `network_mode: host`, you can skip this part
ports:
- "127.0.0.1:5349:5349/tcp"
- "50300-50400:50300-50400/udp"
# "3478:3478/udp" # (optional UDP port)
```
Recreate the LiveKit container (with `docker compose up -d livekit`) to apply these changes. Remember to allow the new `50300:50400/udp` ports through your firewall.
Then, we will configure a route from port 443 of the host back to our `livekit-turn.example.com` service on port 5349. To both **multiplex** this and LiveKit's websocket on the same port, we will use a layer-4 reverse proxy with **SNI routing** capabilities, such as [caddy-l4][caddy-l4] on the host system.
```
## in your Caddyfile ##
{
servers {
listener_wrappers {
# intercept packets meant for the TURN domain first
# before forwarding other packets to "normal" HTTP listeners
layer4 {
@turn tls sni livekit-turn.example.com
route @turn {
proxy 127.0.0.1:5349 # forward to normal TURNS port
}
}
}
tls
}
}
}
# livekit stuff
https://livekit.example.com {
@lk-jwt-service path /healthz /get_token /sfu/get
route @lk-jwt-service {
reverse_proxy 127.0.0.1:8081
}
reverse_proxy http://127.0.0.1:7880
}
```
[caddy-l4]: https://github.com/mholt/caddy-l4
</details>
<details>
<summary>Using an external TURN server (coturn)</summary>
Before continuing, make sure coturn's `min-port` and `max-port` do not overlap with LiveKit's port range:
```ini
# in your coturn.conf
min-port=50201
max-port=65535
```
Then, generate a long random secret for LiveKit, and add it to your coturn config under the `static-auth-secret` option. You can add as many secrets as you want, so set a different one for LiveKit to use.
After that, refer to the following [**TURN instructions**](./turn#turns-over-443) to set up coturn with TLS, as well as multiplexing with LiveKit's websocket on port 443.
Then configure LiveKit, making sure to replace `COTURN_SECRET` with the one you generated:
```yaml
### in your livekit.yaml ###
rtc:
# ... other configs here ...
turn_servers:
- host: coturn.example.com
port: 443
protocol: tls
secret: "COTURN_SECRET"
```
Restart LiveKit, coturn, and Caddy-l4 to apply these changes.
</details>
After finishing configuration, you can run the Testing steps again to check that TURN-over-TLS is working. In the LiveKit connection test page, there should be a green tick saying "Can connect to TURN".
- [Element Call](https://github.com/element-hq/element-call)
- [lk-jwt-service](https://github.com/element-hq/lk-jwt-service)
- [LiveKit server](https://github.com/livekit/livekit)
+120 -284
View File
@@ -1,95 +1,36 @@
# Setting up TURN/STUN
[TURN][turn] and [STUN][stun] are used as a component in many calling systems. Matrix uses them directly for legacy calls and indirectly for MatrixRTC via Livekit.
[TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT) and [STUN](https://en.wikipedia.org/wiki/STUN) are used as a component in many calling systems. Matrix uses them directly for legacy calls and indirectly for MatrixRTC via Livekit.
Continuwuity recommends using [Coturn][coturn] as your TURN/STUN server, which is available as a Docker image or a distro package. This guide assumes that you are using docker compose for deployment.
:::tip
You can find help setting up TURN/STUN in our MatrixRTC room - [#matrixrtc:continuwuity.org](https://matrix.to/#/%23matrixrtc%3Acontinuwuity.org)
:::
[turn]: https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT
[stun]: https://en.wikipedia.org/wiki/STUN
[coturn]: https://github.com/coturn/coturn
Continuwuity recommends using [Coturn](https://github.com/coturn/coturn) as your TURN/STUN server, which is available as a Docker image or a distro package.
## Installing Coturn
### 1. Domain
Coturn should live on its own domain or subdomain. In this guide we use `coturn.example.com` - this should be replaced with a domain you control.
### 2. Configuration
### Configuration
Create a configuration file called `coturn.conf` containing:
```ini
use-auth-secret
static-auth-secret=<a secret key>
realm=coturn.example.com
realm=<your server domain>
```
:::tip Tip: Generate a long, secure secret with the following command
:::tip Generating a secure secret
A common way to generate a suitable alphanumeric secret key is by using:
```bash
pwgen -s 64 1
```
:::
If you want to enable TURN-over-TLS (TURNS), add the appropriate cert/key paths to your `coturn.conf` by adding the following lines:
```ini
cert=/etc/letsencrypt/coturn.example.com.crt
pkey=/etc/letsencrypt/coturn.example.com.key
```
The cert and key must be encoded in PEM format and are readable by the coturn user.
Check out the [`turnserver.conf`][turnserver-conf] example for all coturn options.
[turnserver-conf]: https://github.com/coturn/coturn/blob/master/examples/etc/turnserver.conf
### 3. Running the coturn container
Next, we will start the Coturn container with the [official image][coturn-image]. **Host networking mode** will be used, as it is better for performance and reduces configuration complexity (see [Coturn Docker docs][coturn-docker-docs] for rationale).
[coturn-image]: https://hub.docker.com/r/coturn/coturn
[coturn-docker-docs]: https://github.com/coturn/coturn/blob/master/docker/coturn/README.md#why-so-many-ports-opened
Create a `docker-compose.yml` file as follows and run `docker compose up -d`:
```yaml
version: '3'
services:
coturn:
container_name: coturn
image: docker.io/coturn/coturn
restart: unless-stopped
network_mode: "host"
volumes:
- ./coturn.conf:/etc/coturn/turnserver.conf
# replace this with actual paths to your certificates
- /path/to/certs:/etc/letsencrypt
```
### 4. Opening ports
#### Port Configuration
By default, coturn uses the following ports:
- `3478` (UDP/TCP): Standard TURN/STUN port
- `5349` (UDP/TCP): TURN/STUN over TLS
- `49152-65535` (UDP): Media relay ports
You will need to allow them through your firewall. If you use UFW, the commands are:
```bash
ufw allow 3478/udp
ufw allow 3478/tcp
ufw allow 5349/tcp
ufw allow 5349/udp
ufw allow 49152-65535/udp
```
::: tip For LiveKit users
If you're also running LiveKit, you'll need configure non-overlapping port ranges to avoid port conflicts:
If you're also running LiveKit, you'll need to avoid port conflicts. Configure non-overlapping port ranges:
```ini
# In coturn.conf
@@ -97,33 +38,57 @@ # In coturn.conf
max-port=65535
```
```bash
# With ufw
ufw allow 50201:65535/udp
```
This leaves ports `50100-50200` available for LiveKit's default configuration.
### Running with Docker
Run the [Coturn](https://hub.docker.com/r/coturn/coturn) image using:
```bash
docker run -d --network=host \
-v $(pwd)/coturn.conf:/etc/coturn/turnserver.conf \
coturn/coturn
```
### Running with Docker Compose
Create a `docker-compose.yml` file and run `docker compose up -d`:
```yaml
version: '3'
services:
turn:
container_name: coturn-server
image: docker.io/coturn/coturn
restart: unless-stopped
network_mode: "host"
volumes:
- ./coturn.conf:/etc/coturn/turnserver.conf
```
:::info Why host networking?
Coturn uses host networking mode because it needs to bind to multiple ports and work with various network protocols. Using host networking is better for performance, and reduces configuration complexity. To understand alternative configuration options, visit [Coturn's Docker documentation](https://github.com/coturn/coturn/blob/master/docker/coturn/README.md).
:::
### 5. Security Recommendations
### Security Recommendations
For Coturn hardening and security best practices, see Synapse's [Coturn documentation][synapse-coturn-guide],
which includes important firewall and access control recommendations.
[synapse-coturn-guide]: https://element-hq.github.io/synapse/latest/setup/turn/coturn.html#configuration
For security best practices, see Synapse's [Coturn documentation](https://element-hq.github.io/synapse/latest/turn-howto.html), which includes important firewall and access control recommendations.
## Configuring Continuwuity
Once your TURN server is running, configure Continuwuity to provide credentials to clients. Add the following to your Continuwuity configuration file:
### Shared Secret Authentication (Recommended)
This is the most secure method and generates time-limited credentials automatically:
```toml
# TURN URIs that clients should connect to
turn_uris = [
"turn:coturn.example.com:3478?transport=udp",
"turn:coturn.example.com:3478?transport=tcp",
# Add this if you're using TURN-over-TLS (note the `turns:` prefix)
"turns:coturn.example.com:5349?transport=tcp"
"turn:coturn.example.com?transport=udp",
"turn:coturn.example.com?transport=tcp",
"turns:coturn.example.com?transport=udp",
"turns:coturn.example.com?transport=tcp"
]
# Shared secret for generating credentials (must match coturn's static-auth-secret)
@@ -133,18 +98,54 @@ # Optional: Read secret from a file instead (takes priority over turn_secret)
# turn_secret_file = "/etc/continuwuity/.turn_secret"
# TTL for generated credentials in seconds (default: 86400 = 24 hours)
turn_ttl = 10800
turn_ttl = 86400
```
Restart Continuwuity, and the new changes should now be applied.
:::tip Using TLS
The `turns:` URI prefix instructs clients to connect to TURN over TLS, which is highly recommended for security. Make sure you've configured TLS in your coturn server first.
:::
### Static Credentials (Alternative)
If you prefer static username/password credentials instead of shared secrets:
```toml
turn_uris = [
"turn:coturn.example.com?transport=udp",
"turn:coturn.example.com?transport=tcp"
]
turn_username = "your_username"
turn_password = "your_password"
```
:::warning
Static credentials are less secure than shared secrets because they don't expire and must be configured in coturn separately. It is strongly advised you use shared secret authentication.
:::
### Guest Access
By default, TURN credentials require client authentication. To allow unauthenticated access:
```toml
turn_allow_guests = true
```
:::caution
This is not recommended as it allows unauthenticated users to access your TURN server, potentially enabling abuse by bots. All major Matrix clients that support legacy calls *also* support authenticated TURN access.
:::
### Important Notes
- Replace `coturn.example.com` with your actual TURN server domain (the `realm` from coturn.conf)
- The `turn_secret` must match the `static-auth-secret` in your coturn configuration
- Restart or reload Continuwuity after making configuration changes
## Testing Your TURN Server
### Testing Credentials
Get an access token for your current login session. These can be found in your client's settings or obtained via [this website](https://timedout.uk/mxtoken.html).
Then, using that token, verify that Continuwuity is correctly serving TURN credentials to clients:
Verify that Continuwuity is correctly serving TURN credentials to clients:
```bash
curl "https://matrix.example.com/_matrix/client/r0/voip/turnServer" \
@@ -158,221 +159,56 @@ ### Testing Credentials
"username": "1752792167:@jade:example.com",
"password": "KjlDlawdPbU9mvP4bhdV/2c/h65=",
"uris": [
"turns:coturn.example.com:3478?transport=tcp",
"turn:coturn.example.com:3478?transport=udp",
"turn:coturn.example.com:5349?transport=tcp"
"turns:coturn.example.com?transport=udp",
"turns:coturn.example.com?transport=tcp",
"turn:coturn.example.com?transport=udp",
"turn:coturn.example.com?transport=tcp"
],
"ttl": 86400
}
```
:::note MSC4166 Compliance
If no TURN URIs are configured (`turn_uris` is empty), Continuwuity will return a 404 Not Found response, as specified in MSC4166.
:::
### Testing Connectivity
Open the [Trickle ICE][trickle-ice] testing page in a browser and then:
Use [Trickle ICE](https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/) to verify that the TURN credentials actually work:
1. Copy the URIs and credentials from the response above
2. Paste them into the Trickle ICE testing tool, and click on "Add server"
3. When finished adding all URIs and credentials, click "Gather candidates"
1. Copy the credentials from the response above
2. Paste them into the Trickle ICE testing tool
3. Click "Gather candidates"
4. Look for successful `relay` candidates in the results
If you see `relay` candidates in the results, your TURN/STUN server is working correctly! You should now be able to place and receive legacy calls.
[trickle-ice]: https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
If you see relay candidates, your TURN server is working correctly!
## Troubleshooting
To gather debug logs while troubleshooting Coturn, add `verbose` to your `coturn.conf`. You can then view these logs with `docker-compose logs --follow coturn`.
### Clients can't connect to TURN server
### Errors with Trickle ICE
- Verify firewall rules allow the necessary ports (3478, 5349, and your media port range)
- Check that DNS resolves correctly for your TURN domain
- Ensure your `turn_secret` matches coturn's `static-auth-secret`
- Test with Trickle ICE to isolate the issue
- `code=701` - the TURN server is not reachable
- Verify firewall rules allow the necessary ports (3478, 5349, and your media port range)
- Verify via logs that coturn is exposed on the correct addresses and interfaces
- Check that DNS resolves correctly for your TURN domain
- `code=401` - unauthorized credentials
- Ensure your `turn_secret` matches coturn's `static-auth-secret`
- Ensure the credentials you obtained from the Testing steps has not expired yet. You can adjust `turn_ttl` in your Continuwuity configuration to increase this, or simply re-request a new one
- Wrong IP address advertised
- This may be caused by coturn not recognizing its public-facing IP correctly, due to particular network setups. You can configure `external-ip=<desired-public-ip>` to fix this issue.
### Port conflicts with LiveKit
### 404 when calling the turnServer endpoint
- Make sure coturn's `min-port` starts above LiveKit's `port_range_end` (default: 50200)
- Or adjust LiveKit's port range to avoid coturn's default range
This is the correct response when no TURN servers are configured, as per [MSC4166][msc4166]. Verify that your `turn_uris` is not empty in your Continuwuity config and try again.
### 404 when calling turnServer endpoint
[msc4166]: https://github.com/matrix-org/matrix-spec-proposals/pull/4166
- Verify that `turn_uris` is not empty in your Continuwuity config
- This behavior is correct per MSC4166 if no TURN URIs are configured
## Related Documentation
### Credentials expire too quickly
- Adjust the `turn_ttl` value in your Continuwuity configuration
- Default is 86400 seconds (24 hours)
### Related Documentation
- [MatrixRTC/LiveKit Setup](./livekit.mdx) - Configure group calling with LiveKit
- [Coturn GitHub][coturn] - Official coturn repository
- [`turnserver.conf`][turnserver-conf] - Coturn TURN SERVER configuration file with full options
- [Synapse TURN Guide][synapse-turn-guide] - TURN server guide for Synapse
- [Synapse Coturn Guide][synapse-coturn-guide] - Coturn-specific guide for Synapse, with important security recommendations
[synapse-turn-guide]: https://element-hq.github.io/synapse/latest/turn-howto.html
## Appendix
### Using Eturnal
If you instead prefer to use [eturnal][eturnal], you can refer to the guides below:
- [Community guidance][eturnal-continuwuity] on using eturnal with Legacy Calls, LiveKit calls, and more
- Synapse's [eturnal documentation][synapse-eturnal-guide]
[eturnal]: https://eturnal.net
[eturnal-continuwuity]: https://muoi.me/~stratself/articles/an-eturnal-to-rule-them-all/
[synapse-eturnal-guide]: https://element-hq.github.io/synapse/latest/setup/turn/eturnal.html
### TURNS-over-443
Normally, TURN would work on their default setups. However, there are situations where clients are in very restrictive networks that disallows non-standard ports and UDP. In these cases, a TURN-over-TLS server on port :443 could be employed to relay traffic for them.
However, port 443 is usually utilized by other HTTPS services. Therefore, one would need to **multiplex** both TURN HTTPS on these ports, and filter packets to them via **SNI routing**.
Below are examples to multiplex Coturn and [LiveKit](./livekit.mdx) on port 443, using [caddy-l4][caddy-l4] on the host system.
<details>
<summary>Caddyfile with TLS passthrough</summary>
This Caddyfile:
- Route `turn.example.com` to the TURNS port for Coturn without TLS termination, and
- Route `livekit.example.com` to the [LiveKit services](./livekit.mdx) with TLS termination by Caddy
Please note that all traffic from Coturn's perspective will be coming from caddy-l4's IP now.
```
{
servers {
listener_wrappers {
# intercept packets meant for the TURN domain first
# before forwarding other packets to "normal" HTTP listeners
layer4 {
@turn tls sni turn.example.com
route @turn {
proxy 127.0.0.1:5349 # forward to normal TURNS port
}
}
tls
}
}
}
# livekit stuff
https://livekit.example.com {
@lk-jwt-service path /healthz /get_token /sfu/get
route @lk-jwt-service {
reverse_proxy 127.0.0.1:8081
}
reverse_proxy http://127.0.0.1:7880
}
```
</details>
<details>
<summary>Caddyfile with TLS termination and PROXY protocol forwarding</summary>
This setup:
- Terminates TLS for `turn.example.com`,
- Tag the decrypted packets with PROXY protocol, and route it to coturn's `tcp-proxy-port`
- Route `livekit.example.com` to the [LiveKit services](./livekit.mdx) with TLS termination by Caddy
It allows coturn to see real client IPs, but the TLS handling is done on Caddy's side.
First, enable coturn's PROXY-protocol accepting port by adding this:
```ini
# in coturn.conf
tcp-proxy-port=5555
```
Then, in the Caddyfile:
```
{
servers {
listener_wrappers {
# intercept packets meant for the TURN domain first
# before forwarding other packets to "normal" HTTP listeners
layer4 {
@turn tlssni turn.example.com
route @turn {
tls # terminate TLS for the turn.example.com packets
proxy {
# then, proxy them to tcp-proxy-port and enable PROXY protocol version 2
upstream 127.0.0.1:5555
proxy_protocol v2
}
}
}
tls
}
}
}
# livekit stuff
https://livekit.example.com {
@lk-jwt-service path /healthz /get_token /sfu/get
route @lk-jwt-service {
reverse_proxy 127.0.0.1:8081
}
reverse_proxy http://127.0.0.1:7880
}
# placeholder block to obtain certs for turn.example.com
https://turn.example.com {
respond "OK" 200
}
```
**Note**: the setup will disable TURN-over-TLS functionality on port 5349/tcp.
</details>
After configuration and spin-up, the destination `turns:turn.example.com:443?transport=tcp` should work with Trickle ICE tests. You can now advertise it as an address in your `turn_uris` as well as [LiveKit](./livekit#additional-turns-over-443-configuration).
[caddy-l4]: https://github.com/mholt/caddy-l4
### Unsafe TURN setups (not recommended)
<details>
<summary>Using static credentials</summary>
:::caution
Static credentials are less secure than shared secrets because they don't expire and must be configured in coturn separately. It is strongly advised you use [shared secret authentication](#2-configuration).
:::
If you prefer static username/password credentials instead of shared secrets:
```ini
# In coturn.conf
# Comment out options to use a secret
# use-auth-secret
# static-auth-secret=<a secret key>
# Define a username-password pair
user=your_username:your_password
```
```toml
# In continuwuity.toml
turn_uris = [
"turn:coturn.example.com?transport=udp",
"turn:coturn.example.com?transport=tcp"
]
turn_username = "your_username"
turn_password = "your_password"
```
</details>
- [Coturn GitHub](https://github.com/coturn/coturn) - Official coturn repository
- [Synapse TURN Guide](https://element-hq.github.io/synapse/latest/turn-howto.html) - Additional security recommendations
+6 -29
View File
@@ -9,7 +9,7 @@ ### Choose an image
| Image | Notes |
| ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| [https://forgejo.ellis.link/continuwuation/continuwuity:**latest**][latest] | Latest tagged release. (recommended) |
| [https://forgejo.ellis.link/continuwuation/continuwuity:**main**][main] | Latest `main` branch commit. Built with [CI][release-image] |
| [https://forgejo.ellis.link/continuwuation/continuwuity:**main**][main] | Latest `main` branch commit. |
| [https://forgejo.ellis.link/continuwuation/continuwuity:**latest-maxperf**][latest-maxperf] | Latest tagged release, [performance optimised version](./generic.mdx#performance-optimised-builds). |
| [https://forgejo.ellis.link/continuwuation/continuwuity:**main-maxperf**][main-maxperf] | Latest `main` branch commit, [performance optimised version](./generic.mdx#performance-optimised-builds). |
@@ -17,7 +17,6 @@ ### Choose an image
[main]: https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main
[latest-maxperf]: https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest-maxperf
[main-maxperf]: https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main-maxperf
[release-image]: https://forgejo.ellis.link/continuwuation/continuwuity/actions/?workflow=release-image.yml
If you want a specific version or commit hash, you can browse for them [here][oci-all-versions].
@@ -26,11 +25,13 @@ ### Choose an image
- `ghcr.io/continuwuity/continuwuity` ([Github Registry][ghcr-io])
- `docker.io/jadedblueeyes/continuwuity` ([Docker Hub][docker-hub])
- `registry.gitlab.com/continuwuity/continuwuity` ([Gitlab Registry][gitlab-registry])
- `git.nexy7574.co.uk/mirrored/continuwuity` ([Nexy's forge][nexy-forge]. Releases only, no `main` tags)
[oci-all-versions]: https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/versions
[ghcr-io]: https://github.com/continuwuity/continuwuity/pkgs/container/continuwuity/versions?filters%5Bversion_type%5D=tagged
[docker-hub]: https://hub.docker.com/r/jadedblueeyes/continuwuity/
[gitlab-registry]: https://gitlab.com/continuwuity/continuwuity/container_registry/8871720
[nexy-forge]: https://git.nexy7574.co.uk/mirrored/-/packages/container/continuwuity/versions
### Prerequisites
@@ -62,8 +63,8 @@ ### Choose Your Reverse Proxy
These examples include reverse proxy configurations for Matrix federation, which will route your Matrix domain (and optionally .well-known paths) to Continuwuity.
:::important Docker DNS Performance
Docker's default DNS resolver is known to [**cause timeout issues**](../troubleshooting#dns-issues) for Matrix federation. To bypass it and use a more performant resolver, mount a custom `/etc/resolv.conf` config file into the Continuwuity container:
:::note Docker DNS Performance
Docker's default DNS resolver are known to [cause timeout issues](../troubleshooting#dns-issues) for Matrix federation. To bypass it and use a more performant resolver, mount a custom `/etc/resolv.conf` config file into the Continuwuity container.
```yaml title='docker-compose.yml'
services:
@@ -84,19 +85,8 @@ ### Choose Your Reverse Proxy
#### Caddy (using Caddyfile)
<details>
<summary>docker-compose.with-caddy.yml ([view raw](/deploying/docker-compose.with-caddy.yml))</summary>
Create a Caddyfile with the following content:
```
https://example.com:443, https://example.com:8448 {
reverse_proxy http://homeserver:8008
}
```
Then, for the docker-compose:
```yaml file="../public/deploying/docker-compose.with-caddy.yml"
```
@@ -174,7 +164,7 @@ ### Starting Your Server
4. Check your server logs for a registration token:
```bash
docker compose logs continuwuity 2>&1
docker-compose logs continuwuity 2>&1
```
You'll see output as below.
@@ -197,19 +187,6 @@ ## Testing
Check your container logs using `docker-compose logs --follow` to debug any issues. See the [Troubleshooting](../troubleshooting.mdx) page for common errors and how to fix them.
## Updating
To update to a new version of Continuwuity, use the following commands:
```bash
# stop the services in docker-compose.yaml
docker compose down
# pull latest images for the services in docker-compose.yaml
docker compose pull
# spin up the containers again
docker compose up -d
```
## Other deployment methods
### Docker - Quick Run
+35 -32
View File
@@ -29,12 +29,19 @@ ### Prebuilt binary
#### Performance-optimised builds
Continuwuity provides `*-maxperf` tagged binaries, which uses the `release-max-perf` build profile with [link-time optimisation (LTO)][lto-rust-docs]. For the x86_64 architecture, these binaries specifically target the Haswell architecture (hence the `-haswell-` name extension), and enables hardware-accelerated CRC32 checksumming in
RocksDB which significantly improves database performance. If you're using an x86_64 system with CPUs from the last ~15 years, consider using these images for best performance.
For x86_64 systems with CPUs from the last ~15 years, use the
`-haswell-` optimised binaries for best performance. These
binaries enable hardware-accelerated CRC32 checksumming in
RocksDB, which significantly improves database performance.
The haswell instruction set provides an excellent balance of
compatibility and speed.
If you're using Docker instead, equivalent performance-optimised
images are available with the `-maxperf` suffix (e.g.
`forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf`).
These images use the `release-max-perf` build profile with
[link-time optimisation (LTO)][lto-rust-docs]
and, for amd64, target the haswell CPU architecture.
[lto-rust-docs]: https://doc.rust-lang.org/cargo/reference/profiles.html#lto
@@ -52,7 +59,28 @@ ### Nix
### Compiling
Alternatively, you may compile the binary yourself. See the [Appendix subsection](#compiling-continuwuity) for more details.
Alternatively, you may compile the binary yourself.
#### Using Docker
See the [Building Docker Images](../development/index.mdx#building-docker-images)
section in the development documentation.
#### Manual
##### Dependencies
- Run `nix develop` to get a devshell with everything you need
- Or, install the following:
- (On linux) `liburing-dev` on the compiling machine, and `liburing` on the target host
- (On linux) `pkg-config` on the compiling machine to allow finding `liburing`
- A C++ compiler and (on linux) `libclang` for RocksDB
##### Build
You can now build Continuwuity using `cargo build --release`.
Continuwuity supports various optional features that can be enabled during compilation. Please see the Cargo.toml file for a comprehensive list, or ask in our rooms.
## Adding a Continuwuity user
@@ -179,7 +207,7 @@ ### Other Reverse Proxies
- `/_matrix/client` - core Client-Server APIs. These should be available on port :443
- `/_continuwuity/` - Continuwuity's integrated account management interface and authentication path for OAuth-compatible clients
- `/_continuwuity/` - Continuwuity's integrated account management interface for OAuth-compatible clients
You can optionally reverse proxy the following individual routes:
@@ -187,7 +215,7 @@ ### Other Reverse Proxies
Continuwuity to perform delegation (see the `[global.well_known]` config section)
- `/.well-known/matrix/support` if using Continuwuity to send the homeserver admin
[contact and support page][well-known-support]
- `/` and `/_continuwuity/resources` if you would like to see the Continuwuity landing page
- `/` and `/_continuwuity/logo.svg` if you would like to see the Continuwuity landing page
Refer to the respective software's documentation and online guides on how to do so.
@@ -258,10 +286,10 @@ ## How do I know it works?
As a quick health check, you can also use these cURL commands:
```bash
curl https://example.com/_continuwuity/server_version
curl https://example.com/_conduwuit/server_version
# If using port 8448
curl https://example.com:8448/_continuwuity/server_version
curl https://example.com:8448/_conduwuit/server_version
# If federation is enabled
curl https://example.com:8448/_matrix/federation/v1/version
@@ -277,28 +305,3 @@ ## What's next?
- For Audio/Video call functionality see the [**Calls**](../calls.md) page.
- Consult the [Maintenance](../maintenance.mdx) page for guidance on maintaining your homeserver.
- If you want to set up an appservice, take a look at the [**Appservice Guide**](../appservices.md).
## Appendix
### Compiling Continuwuity
#### Using Docker
See the [Building Docker Images](../development/index.mdx#building-docker-images)
section in the development documentation.
#### Manual
##### DEPENDENCIES
- Run `nix develop` to get a devshell with everything you need
- Or, install the following:
- (On linux) `liburing-dev` on the compiling machine, and `liburing` on the target host
- (On linux) `pkg-config` on the compiling machine to allow finding `liburing`
- A C++ compiler and (on linux) `libclang` for RocksDB
##### BUILD
You can now build Continuwuity using `cargo build --release`.
Continuwuity supports various optional features that can be enabled during compilation. Please see the Cargo.toml file for a comprehensive list, or ask in our rooms.
+64 -82
View File
@@ -53,19 +53,12 @@ # Defaults to members of the admin room if unset
After doing the steps above, Continuwuity will serve these 3 JSON files:
- `/.well-known/matrix/client`: for Client-Server discovery
- `/.well-known/matrix/server`: for Server-Server (federation) discovery
- `/.well-known/matrix/support`: admin contact details (strongly recommended).
- By default, this include members of the admin room.
- `/.well-known/matrix/support`: admin contact details (strongly recommended to have)
To enable full discovery, you will need to reverse proxy these paths from the base domain back to Continuwuity.
## Serving well-known files
You can either reverse proxy the well-known paths to Continuwuity, or serve static JSON files yourself.
### Option 1: Reverse proxying well-known files to Continuwuity
## Reverse proxying well-known files to Continuwuity
<details>
@@ -103,7 +96,7 @@ ### Option 1: Reverse proxying well-known files to Continuwuity
For **Docker** users, consult the compose files in the [Appendix section](#docker-compose-examples).
After applying these changes, restart Continuwuity and your reverse proxy. Visit these routes and check that the responses match the examples below:
After applying these changes, restart Continuwuity and your reverse proxy.Visit these routes and check that the responses match the examples below:
<details open>
@@ -129,12 +122,12 @@ ### Option 1: Reverse proxying well-known files to Continuwuity
</details>
### Option 2: Manually serving static files for well-known
### Serving well-known files manually
Instead of reverse proxying well-known URIs, you can serve these files directly as static JSON that match the ones above. This is useful if your base domain points to a different physical server, and reverse proxying isn't feasible.
:::important Important: always configure [global.well_known]
Even if you choose to serve the well-known files manually, you **must** still set the `global.well_known.client` configuration option to the domain you're delegating to. Continuwuity needs to know the domain it runs on in order to redirect clients properly to email, OAuth, and account management endpoints.
:::warning
Even if you choose to serve the well-known files manually, if you are using delegation at all, you **must** still set the `global.well_known.client` configuration option to the domain you're delegating to. Continuwuity needs to know the domain it runs on for OAuth-compatible clients to work correctly.
:::
<details>
@@ -197,79 +190,16 @@ ### Issues with alternative setups
In these scenarios, further configurations would be needed. Refer to the [Related Documentation](#related-documentation) section for resolution steps and see how they could apply to your use case.
## Related Documentation
---
### Matrix Specification
See the following sections of the Matrix Specification for more details on client and server resolution mechanisms:
- [Server-to-Server resolution](https://spec.matrix.org/latest/server-server-api/#resolving-server-names)
- [Client-to-Server resolution](https://spec.matrix.org/latest/client-server-api/#server-discovery)
- [MSC1929: Homeserver Admin Contact and Support page](https://github.com/matrix-org/matrix-spec-proposals/pull/1929)
### Community guides
- [Federation Setup guide by MTRNord](https://connectivity-tester.mtrnord.blog/docs/getting-started/federation-setup)
## Appendix
### Docker Compose examples
The following Compose files are taken from [Docker deployment instructions](../deploying/docker.mdx) and reconfigured to support split-domain delegation. Note the updated `CONTINUWUITY_WELL_KNOWN` variable and relevant changes in reverse proxy rules.
<details>
<summary>Caddy (using Caddyfile) - delegated.docker-compose.with-caddy.yml ([view raw](/advanced/delegated.docker-compose.with-caddy.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.with-caddy.yml"
```
```txt title="Caddyfile"
https://matrix.example.com:443 {
reverse_proxy http://homeserver:8008
}
https://example.com:443 {
reverse_proxy /.well-known/matrix* http://homeserver:8008
}
```
</details>
<details>
<summary>Caddy (using labels) - delegated.docker-compose.with-caddy-labels.yml ([view raw](/advanced/delegated.docker-compose.with-caddy-labels.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.with-caddy-labels.yml"
```
</details>
<details>
<summary>Traefik (for existing setup) - delegated.docker-compose.for-traefik.yml ([view raw](/advanced/delegated.docker-compose.for-traefik.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.for-traefik.yml"
```
</details>
<details>
<summary>Traefik included - delegated.docker-compose.with-traefik.yml ([view raw](/advanced/delegated.docker-compose.with-traefik.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.with-traefik.yml"
```
</details>
### Using SRV records (not recommended)
## Using SRV records (not recommended)
:::warning
The following methods use DNS SRV records, and are **not recommended** due to increased complexity with few benefits. Additionally, they only work for federation traffic.
If you have already set up well-known delegation as above, you can safely skip this part.
The following methods are **not recommended** due to increased complexity with little benefits. If you have already set up `.well-known` delegation as above, you can safely skip this part.
:::
The following methods uses SRV DNS records and only work with federation traffic. They are only included for completeness.
<details>
<summary>Using only SRV records</summary>
@@ -311,7 +241,7 @@ ### Using SRV records (not recommended)
<details>
<summary>Using SRV records as a fallback for well-known delegation</summary>
<summary>Using SRV records as a fallback for .well-known delegation</summary>
Assume your delegation is as below:
@@ -333,3 +263,55 @@ ### Using SRV records (not recommended)
On the target's IP at port 443, you'll need to provide a valid route and cert for `example.com`.
</details>
---
## Related Documentation
See the following Matrix Specs for full details on client/server resolution mechanisms:
- [Server-to-Server resolution](https://spec.matrix.org/v1.17/server-server-api/#resolving-server-names) (see this for more information on SRV records)
- [Client-to-Server resolution](https://spec.matrix.org/v1.17/client-server-api/#server-discovery)
- [MSC1929: Homeserver Admin Contact and Support page](https://github.com/matrix-org/matrix-spec-proposals/pull/1929)
## Appendix
### Docker Compose examples
The following Compose files are taken from [Docker instructions](../deploying/docker.mdx) and reconfigured to support split-domain delegation. Note the updated `CONTINUWUITY_WELL_KNOWN` variable and relevant changes in reverse proxy rules.
<details>
<summary>Caddy (using Caddyfile) - delegated.docker-compose.with-caddy.yml ([view raw](/advanced/delegated.docker-compose.with-caddy.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.with-caddy.yml"
```
</details>
<details>
<summary>Caddy (using labels) - delegated.docker-compose.with-caddy-labels.yml ([view raw](/advanced/delegated.docker-compose.with-caddy-labels.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.with-caddy-labels.yml"
```
</details>
<details>
<summary>Traefik (for existing setup) - delegated.docker-compose.for-traefik.yml ([view raw](/advanced/delegated.docker-compose.for-traefik.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.for-traefik.yml"
```
</details>
<details>
<summary>Traefik included - delegated.docker-compose.with-traefik.yml ([view raw](/advanced/delegated.docker-compose.with-traefik.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.with-traefik.yml"
```
</details>
+18 -55
View File
@@ -8,7 +8,7 @@ # DNS Tuning (recommended)
## Overview
For generic deployments, install your resolver of choice and configure your host system's `/etc/resolv.conf` to point to it. The resolver should ideally reside on the same host as Continuwuity.
For generic deployments, install your resolver of choice and configure `/etc/resolv.conf` to point to it. The resolver should ideally reside on the same host as Continuwuity.
```txt title="/etc/resolv.conf"
nameserver 127.0.0.1
@@ -18,7 +18,7 @@ ## Overview
### For Docker users
By default, Docker bridge networks uses a poorly performing resolver to intercept and respond to container hostnames. **This resolver should also be avoided**. Instead, mount a custom `/etc/resolv.conf` file into the container, and hardcode a resolver address to bypass Docker's.
Docker bridge networks uses a non-performant resolver to intercept and respond to container hostnames, and **this should also be avoided**. Instead, mount a custom `/etc/resolv.conf` file into the container, and hardcode a resolver address to bypass Docker's.
It is recommended to run a dedicated resolver container for Continuwuity, as to separate from the host's resolver setup. To do this, create a custom bridge network and IP range, and explicitly define an IP address for the resolver container.
@@ -27,28 +27,23 @@ ### For Docker users
```yaml title="docker-compose.yml"
networks:
# re-use the `proxy` network, as shown in the Docker deployment docs
# (https://continuwuity.org/deploying/docker#choose-your-reverse-proxy)
# and configure it with a custom subnet
proxy:
matrix_net:
ipam:
driver: default
config:
- subnet: "10.10.10.0/24"
services:
homeserver:
# ...
volume:
- ./continuwuity-resolv.conf:/etc/resolv.conf:ro
networks:
- proxy
homeserver:
# ...
volume:
- ./continuwuity-resolv.conf:/etc/resolv.conf:ro
unbound:
# ...
networks:
proxy:
ipv4_address: 10.10.10.20
unbound:
# ...
networks:
matrix_net:
ipv4_address: 10.10.10.20
```
```txt title="continuwuity-resolv.conf"
@@ -57,41 +52,9 @@ # and configure it with a custom subnet
</details>
:::warning Breaking resolution with container hostnames
After you mount a custom `/etc/resolv.conf`, you won't be able to connect to other containers using their hostnames, such as for [appservices](../appservices.mdx) in the same Docker network. To work around this, force your appservice to use a static IP address, and refer to its static IP during registration.
<details>
<summary>Example Docker deployment with an appservice</summary>
```yaml title="docker-compose.yml"
# ... other parts of compose file ...
appservice:
# ...
networks:
proxy:
ipv4_address: 10.10.10.30
```
~~~txt
!admin appservices register
```
id: appservice-name
url: http://10.10.10.30:29318
# rest of the registration file
```
~~~
</details>
:::
### For IPv4-only users
If you don't have IPv6 connectivity, change `ip_lookup_strategy` to `1` in Continuwuity's configuration in order to onlyresolve IPv4 addresses and remove unnecessary IPv6 queries.
If you don't have IPv6 connectivity, changing `ip_lookup_strategy` to only resolve for IPv4 will reduce unnecessary AAAA queries.
```toml title="continuwuity.toml"
[global]
@@ -109,13 +72,13 @@ ## Unbound
- Increase `rrset-cache-size` and `msg-cache-size` to something much higher than the default `4M`, such as `64M`.
- Increase `discard-timeout` to something like `4800` to wait longer for upstream resolvers, as recursion can take a long time to respond to some domains. Continuwuity defaults to `dns_timeout = 10` seconds, so dropping requests early would lead to unnecessary retries and/or failures.
- Increase `discard-timeout` to something like `4800` to wait longer for upstream resolvers, as recursion can take a long time to respond to some domains. Continuwuity default to `dns_timeout = 10` seconds, so dropping requests early would lead to unnecessary retries and/or failures.
### Using a forwarder (optional)
### Recursion versus forwarding
Unbound employs **recursive resolution** by default, contacting many nameservers around the world in the process. While this is generally viable for most users, sometimes these recursive queries can be too slow to fully resolve. As an alternative, you may consider **forwarding** your queries to public resolvers, and benefit from faster responses from their caches.
Unbound by default employs **recursive resolution** and contacts many servers around the world. While this allows updated and authoritative answers and are generally viable for most users, sometimes these recursive queries can be too slow to fully resolve. As an alternative, you can consider **forwarding** your queries to public resolvers, and benefit from faster responses from their CDNs.
However, do note that most popular upstreams (such as Google DNS or Quad9) employ IP ratelimiting, so a generous Unbound cache is still needed to avoid making too many queries.
Do note that most popular upstreams (such as Google DNS or Quad9) employ IP ratelimiting, so a generous cache is still needed to avoid making too many queries.
If you want to use forwarders, configure it as follows:
@@ -181,7 +144,7 @@ ### Technitium DNS
## Testing
As a rough stress test, you can run `!admin query resolver flush-cache -a` or `!admin server clear-caches` to clear your destinations cache - this will then trigger a netburst of DNS queries. If your resolver can handle this load without issue, then it should be ready for regular Continuwuity activity.
As a rough stress test, you can run `!admin query resolver flush-cache -a` or `!admin server clear-caches` to trigger a netburst of DNS queries. If your resolver can handle these loads without problem, then it should be ready for regular Continuwuity activity.
To test connectivity against a specific server, use `!admin debug ping <SERVER_NAME>` and `!admin debug resolve-true-destination <SERVER_NAME>`.
+1 -3
View File
@@ -6,7 +6,7 @@ # Performance tuning
## DNS tuning (recommended)
DNS tuning is important to ensure a smooth experience for federated servers. Please see the dedicated [DNS tuning guide](./dns.mdx).
Please see the dedicated [DNS tuning guide](./dns.mdx).
## Cache capacities
@@ -115,8 +115,6 @@ ### Tuning your trusted servers
:::info Vet your trusted servers!
Trusted servers are your first point of contact when obtaining public keys from other servers, and they could theoretically impersonate other servers and cause significant harm to your deployment. Please thoroughly verify your trusted servers' credibility before adding them to your configuration.
If you need suggestions for trusted servers, ask in the Continuwuity main room.
:::
Trusted servers are queried sequentially in the order they are listed. If you have multiple trusted servers configured, put the faster ones first:
+35 -48
View File
@@ -42,16 +42,12 @@ ## Database (RocksDB)
creating unnecessary I/O amplification. RocksDB is built with io_uring support
via liburing for improved read performance.
RocksDB troubleshooting can be found [in the RocksDB section of troubleshooting](./troubleshooting.mdx#rocksdb--database-issues).
RocksDB troubleshooting can be found [in the RocksDB section of troubleshooting](troubleshooting.md).
### Compression
Some RocksDB settings can be adjusted, such as the chosen compression method and level.
See the RocksDB section in the [example config](./reference/config.mdx), and the
[database compression section](./guides/performance.mdx#tuning-database-compression)
in the performance tuning documentation for more.
#### Caveats for btrfs users
Some RocksDB settings can be adjusted such as the compression method chosen. See
the RocksDB section in the [example config](./reference/config.mdx).
btrfs users have reported that database compression does not need to be disabled
on Continuwuity as the filesystem already does not attempt to compress. This can be
@@ -61,12 +57,10 @@ #### Caveats for btrfs users
unbuffered Direct IO inoperable, significantly slowing down read and write
performance. See [the Btrfs docs](https://btrfs.readthedocs.io/en/latest/Compression.html#compatibility).
:::important
Compression is done using the COW mechanism so its incompatible with
`nodatacow`. Direct IO read works on compressed files but will fall back to
buffered writes and leads to no compression even if force compression is set.
Currently `nodatasum` and compression dont work together.
:::
> Compression is done using the COW mechanism so its incompatible with
> nodatacow. Direct IO read works on compressed files but will fall back to
> buffered writes and leads to no compression even if force compression is set.
> Currently nodatasum and compression dont work together.
### Files in database
@@ -84,47 +78,37 @@ ### Files in database
## Backups
### Database online backup
f you'd like to run an online backup of your database - that is, a backup with
no downtime - check the [`!admin server` command](./reference/admin/server.md)
for the required commands and the `database_backup_path` config options in
the example config.
Please note that the format of the database backup is not the exact same as the
format of offline backups. This is unfortunately a bad design choice by Facebook
as we are using the database backup engine API from RocksDB, however the data
is still there and can still be joined together.
Currently only RocksDB supports online backups. If you'd like to backup your
database online without any downtime, see the `!admin server` command for the
backup commands and the `database_backup_path` config options in the example
config. Please note that the format of the database backup is not the exact
same. This is unfortunately a bad design choice by Facebook as we are using the
database backup engine API from RocksDB, however the data is still there and can
still be joined together.
To restore a backup from an online RocksDB backup:
- Shutdown Continuwuity
- Create a new directory for merging together the data
- In the online backup created, copy all `.sst` files in
- shutdown Continuwuity
- create a new directory for merging together the data
- in the online backup created, copy all `.sst` files in
`$DATABASE_BACKUP_PATH/shared_checksum` to your new directory
- trim all the strings so instead of `######_sxxxxxxxxx.sst`, it reads
`######.sst`. A way of doing this with sed and bash is `for file in *.sst; do mv
"$file" "$(echo "$file" | sed 's/_s.*/.sst/')"; done`
- Copy all the files in `$DATABASE_BACKUP_PATH/private/1` (or the latest backup number
- copy all the files in `$DATABASE_BACKUP_PATH/1` (or the latest backup number
if you have multiple) to your new directory
- Set your `database_path` config option to your new directory, or replace your
- set your `database_path` config option to your new directory, or replace your
old one with the new one you crafted
- Start up Continuwuity again and it should open as normal
Note: You can verify the necessary files to copy by comparing them with contents in the `$DATABASE_BACKUP_PATH/meta/1` (or latest backup number) file.
### Database offline backup
- start up Continuwuity again and it should open as normal
If you'd like to do an offline backup, shutdown Continuwuity and copy your
`database_path` directory elsewhere. This can be restored with no modifications
needed.
### Media backup
Backing up media is also just copying the `media/` directory from your database
directory.
Media is stored in the `media/` subdirectory from your database directory.
Backing up media is also just copying that subdirectory.
## Media management
## Media
Media still needs various work, however Continuwuity implements media deletion via:
@@ -133,16 +117,19 @@ ## Media management
- Delete list of MXC URIs
- Delete remote media in the past `N` seconds/minutes via filesystem metadata on
the file created time (`btime`) or file modified time (`mtime`)
- Delete all media from a local user
- Delete all media from a remote server
- Delete cached URL previews
See the [`!admin media` commands](./reference/admin/media.md) for further information.
All media in Continuwuity is stored at `$DATABASE_DIR/media`.
See the `!admin media` command for further information. All media in Continuwuity
is stored at `$DATABASE_DIR/media`. This will be configurable soon.
While Continuwuity does not implement built-in S3 support, using an S3 filesystem
mount on the `media/` path will work. Continuwuity also sends a `Cache-Control`
header of 1 year with the `immutable` directive for all media requests
(download and thumbnail) to reduce unnecessary bandwidth and load.
If you are finding yourself needing extensive granular control over media, we
recommend looking into [Matrix Media
Repo](https://github.com/t2bot/matrix-media-repo). Continuwuity intends to
implement various utilities for media, but MMR is dedicated to extensive media
management.
Built-in S3 support is also planned, but for now using a "S3 filesystem" on
`media/` works. Continuwuity also sends a `Cache-Control` header of 1 year and
immutable for all media requests (download and thumbnail) to reduce unnecessary
media requests from browsers, reduce bandwidth usage, and reduce load.
[rocksdb-compaction]: https://github.com/facebook/rocksdb/wiki/Compaction
@@ -6,10 +6,10 @@
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
},
{
"id": 17,
"id": 16,
"mention_room": true,
"date": "2026-07-30",
"message": "[Continuwuity 26.7.2](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v26.7.2) (and [v26.7.1](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v26.7.1)) have been released! v26.7.1 includes several new bug fixes and features, such as the OAuth2 device authorization flow, and a fix for the \"empty room\" bug (check the release notes!). v26.7.2 is a hotfix that fixes a low severity vulnerability in simplified sliding sync (particularly relevant to multi-user homeservers) and two regressions. It is recommended every deployment upgrades as soon as possible. Join the [announcements room](https://matrix.to/#/#announcements:continuwuity.org) for more prompt announcements regarding updates!"
"date": "2026-07-13",
"message": "[Continuwuity 26.6.2](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v26.6.1) has just been released! This release fixes a severe bug with OIDC that could cause users' accounts to be flagged as deactivated. If you use OIDC, please update as soon as possible."
}
]
}
@@ -1,55 +1,54 @@
# Continuwuity - With Caddy Labels
services:
caddy:
# This compose file uses caddy-docker-proxy as the reverse proxy for Continuwuity!
# For more info, visit https://github.com/lucaslorentz/caddy-docker-proxy
image: "docker.io/lucaslorentz/caddy-docker-proxy:ci-alpine"
ports:
- 80:80
- 443:443
environment:
- CADDY_INGRESS_NETWORKS=caddy
networks:
- proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
restart: unless-stopped
labels:
caddy: example.com
caddy.reverse_proxy: /.well-known/matrix/* homeserver:8008
caddy:
# This compose file uses caddy-docker-proxy as the reverse proxy for Continuwuity!
# For more info, visit https://github.com/lucaslorentz/caddy-docker-proxy
image: "docker.io/lucaslorentz/caddy-docker-proxy:ci-alpine"
ports:
- 80:80
- 443:443
environment:
- CADDY_INGRESS_NETWORKS=caddy
networks:
- caddy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
restart: unless-stopped
labels:
caddy: example.com
caddy.reverse_proxy: /.well-known/matrix/* homeserver:8008
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
# - ./continuwuity.toml:/etc/continuwuity.toml
environment:
CONTINUWUITY_SERVER_NAME: example.com # EDIT THIS
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008
# CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
#- ./continuwuity.toml:/etc/continuwuity.toml
environment:
CONTINUWUITY_SERVER_NAME: example.com # EDIT THIS
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
# Serve .well-known files to tell others to reach Continuwuity on port :443
CONTINUWUITY_WELL_KNOWN: |
{
client=https://matrix.example.com,
server=matrix.example.com:443
}
networks:
- proxy
labels:
caddy: matrix.example.com
caddy.reverse_proxy: "{{upstreams 8008}}"
# Serve .well-known files to tell others to reach Continuwuity on port :443
CONTINUWUITY_WELL_KNOWN: |
{
client=https://matrix.example.com,
server=matrix.example.com:443
}
networks:
- caddy
labels:
caddy: matrix.example.com
caddy.reverse_proxy: "{{upstreams 8008}}"
volumes:
db:
networks:
proxy:
caddy:
@@ -1,46 +1,57 @@
# Continuwuity - Using Caddy Docker Image
services:
caddy:
image: "docker.io/caddy:latest"
ports:
- 80:80
- 443:443
networks:
- caddy
volumes:
- ./data:/data
# mount your created Caddyfile into the container
- ./Caddyfile:/etc/caddy/Caddyfile
restart: unless-stopped
caddy:
image: "docker.io/caddy:latest"
ports:
- 80:80
- 443:443
networks:
- caddy
volumes:
- ./data:/data
restart: unless-stopped
configs:
- source: Caddyfile
target: /etc/caddy/Caddyfile
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
# - ./continuwuity.toml:/etc/continuwuity.toml
environment:
CONTINUWUITY_SERVER_NAME: example.com
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008
# CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
#- ./continuwuity.toml:/etc/continuwuity.toml
environment:
CONTINUWUITY_SERVER_NAME: example.com
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
## Serve .well-known files to tell others to reach Continuwuity on port :443
CONTINUWUITY_WELL_KNOWN: |
{
client=https://example.com,
server=example.com:443
}
## Serve .well-known files to tell others to reach Continuwuity on port :443
CONTINUWUITY_WELL_KNOWN: |
{
client=https://matrix.example.com,
server=matrix.example.com:443
}
networks:
- proxy
networks:
- caddy
networks:
proxy:
caddy:
volumes:
db:
configs:
Caddyfile:
content: |
https://matrix.example.com:443 {
reverse_proxy http://homeserver:8008
}
https://example.com:443 {
reverse_proxy /.well-known/matrix* http://homeserver:8008
}
@@ -1,52 +1,51 @@
# Continuwuity - With Caddy Labels
services:
caddy:
# This compose file uses caddy-docker-proxy as the reverse proxy for Continuwuity!
# For more info, visit https://github.com/lucaslorentz/caddy-docker-proxy
image: "docker.io/lucaslorentz/caddy-docker-proxy:ci-alpine"
ports:
- 80:80
- 443:443
environment:
- CADDY_INGRESS_NETWORKS=caddy
networks:
- proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
restart: unless-stopped
caddy:
# This compose file uses caddy-docker-proxy as the reverse proxy for Continuwuity!
# For more info, visit https://github.com/lucaslorentz/caddy-docker-proxy
image: "docker.io/lucaslorentz/caddy-docker-proxy:ci-alpine"
ports:
- 80:80
- 443:443
environment:
- CADDY_INGRESS_NETWORKS=caddy
networks:
- caddy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
restart: unless-stopped
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
# - ./continuwuity.toml:/etc/continuwuity.toml
environment:
CONTINUWUITY_SERVER_NAME: example.com
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008
# CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
#- ./continuwuity.toml:/etc/continuwuity.toml
environment:
CONTINUWUITY_SERVER_NAME: example.com
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
# Serve .well-known files to tell others to reach Continuwuity on port :443
CONTINUWUITY_WELL_KNOWN: |
{
client=https://example.com,
server=example.com:443
}
networks:
- proxy
labels:
caddy: example.com
caddy.reverse_proxy: "{{upstreams 8008}}"
# Serve .well-known files to tell others to reach Continuwuity on port :443
CONTINUWUITY_WELL_KNOWN: |
{
client=https://example.com,
server=example.com:443
}
networks:
- caddy
labels:
caddy: example.com
caddy.reverse_proxy: "{{upstreams 8008}}"
volumes:
db:
db:
networks:
proxy:
caddy:
@@ -1,48 +1,56 @@
# Continuwuity - Using Caddy Docker Image
services:
caddy:
image: "docker.io/caddy:latest"
ports:
- 80:80
- 443:443
- 8448:8448
networks:
- proxy
volumes:
- ./data:/data
# mount your created Caddyfile into the container
- ./Caddyfile:/etc/caddy/Caddyfile
restart: unless-stopped
caddy:
image: "docker.io/caddy:latest"
ports:
- 80:80
- 443:443
- 8448:8448
networks:
- caddy
volumes:
- ./data:/data
restart: unless-stopped
configs:
- source: Caddyfile
target: /etc/caddy/Caddyfile
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
# - ./continuwuity.toml:/etc/continuwuity.toml
environment:
CONTINUWUITY_SERVER_NAME: example.com
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008
# CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
#- ./continuwuity.toml:/etc/continuwuity.toml
environment:
CONTINUWUITY_SERVER_NAME: example.com
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
## (Optional) Serve .well-known files to tell others to reach Continuwuity on port :443
## If you do this, remove all routes to port :8448 from the compose and Caddyfile
# CONTINUWUITY_WELL_KNOWN: |
# {
# client=https://example.com,
# server=example.com:443
# }
## (Optional) Serve .well-known files to tell others to reach Continuwuity on port :443
## If you do this, remove all routes to port :8448 from the compose and Caddyfile
# CONTINUWUITY_WELL_KNOWN: |
# {
# client=https://example.com,
# server=example.com:443
# }
networks:
- proxy
networks:
- caddy
networks:
proxy:
caddy:
volumes:
db:
db:
configs:
Caddyfile:
content: |
https://example.com:443, https://example.com:8448 {
reverse_proxy http://homeserver:8008
}
-5
View File
@@ -1,8 +1,3 @@
## Configuration file reference
This configuration file reflect the latest developments on the `main` branch. To check the configuration from a specific Continuwuity version, browse the [tags](https://forgejo.ellis.link/continuwuation/continuwuity/tags) and navigate to the `conduwuit-example.toml` file for that version.
---
```toml file="../../conduwuit-example.toml"
+3 -5
View File
@@ -13,7 +13,7 @@ ### Slow joins to rooms
Some slowness is to be expected if you're the first person on your homeserver to join a room (which will
always be the case for single-user homeservers). In this situation, your homeserver has to verify the signatures of
all of the state events sent by other servers before your join. To make this process as fast as possible, make sure you have
[multiple fast, trusted servers](./guides/performance.mdx#tuning-your-trusted-servers) listed in `trusted_servers` in your configuration, and ensure
multiple fast, trusted servers listed in `trusted_servers` in your configuration, and ensure
`query_trusted_key_servers_first_on_join` is set to true (the default).
If you need suggestions for trusted servers, ask in the Continuwuity main room.
@@ -23,8 +23,6 @@ ### Slow joins to rooms
the bug caused your homeserver to forget to tell your client. **To fix this, clear your client's cache.** Both Element and Cinny
have a button to clear their cache in the "About" section of their settings.
[Tuning your DNS](./guides/dns.mdx) can also help make initial joins faster.
### Configuration not working as expected
Sometimes you can make a mistake in your configuration that
@@ -66,7 +64,7 @@ ### DNS server overload
### Intermittent federation failures to a specific server
There may be circumstances where servers fail to connect to each other, probably due to a bad DNS cache. In such cases, issuing `!admin debug ping <SERVER_NAME>` would return some errors, and `!admin query resolver cache <SERVER_NAME>` would return a wrong destination endpoint.
There may be circumstances where servers fail to connect to each other, probably due to a bad DNS cache. In such cases, issuing `!admin debug ping <SERVER_NAME>` would return some errors.
To fix this, you can run `!admin query resolver flush-cache <SERVER_NAME>` to clear the bad cache for that domain, and outbound requests should work again.
@@ -123,7 +121,7 @@ ## Debugging
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`](./reference/admin/debug).
Various debug commands can be found in `!admin debug`.
### Debug/Trace log level
+126 -110
View File
@@ -16,21 +16,21 @@
}
},
"node_modules/@emnapi/core": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.3",
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -39,9 +39,9 @@
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -125,13 +125,13 @@
}
},
"node_modules/@rsbuild/core": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.1.10.tgz",
"integrity": "sha512-lwxC5w88U2AMv6aNwG3VH7+AV33N4JJQjD//egVWFsMbV+OE/bnfCsurSpX8s3lGyRYkJMwUVN+YXMbmmYZFfw==",
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.1.5.tgz",
"integrity": "sha512-7TW4U1SH7VxQZzSTIOzvwj5lo9uNTmGpsmTXFr4axQAE4giLDKP3kVUA1ZW4P3/Mz4QQJJyvZP29mVcb8kZCfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/core": "~2.1.8",
"@rspack/core": "~2.1.3",
"@swc/helpers": "^0.5.23"
},
"bin": {
@@ -169,30 +169,30 @@
}
},
"node_modules/@rspack/binding": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.1.8.tgz",
"integrity": "sha512-tmAyHzDbPiy8V7HvQqtuPsbs6dPgwV0YjzW5XrPRV9gzf+Hdm7pvsZJKE1QKO9WV5RuvGYav98xIX6O+abZxzQ==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.1.3.tgz",
"integrity": "sha512-4UGXJqUHmm36tWG1GgFZz3p8sQ5JuSgWI+gq1xPPoihS41uYJL3cXuJnirTeWsmVrusmYZTQhgU3tl3VYGcJYg==",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"@rspack/binding-darwin-arm64": "2.1.8",
"@rspack/binding-darwin-x64": "2.1.8",
"@rspack/binding-linux-arm64-gnu": "2.1.8",
"@rspack/binding-linux-arm64-musl": "2.1.8",
"@rspack/binding-linux-riscv64-gnu": "2.1.8",
"@rspack/binding-linux-riscv64-musl": "2.1.8",
"@rspack/binding-linux-x64-gnu": "2.1.8",
"@rspack/binding-linux-x64-musl": "2.1.8",
"@rspack/binding-wasm32-wasi": "2.1.8",
"@rspack/binding-win32-arm64-msvc": "2.1.8",
"@rspack/binding-win32-ia32-msvc": "2.1.8",
"@rspack/binding-win32-x64-msvc": "2.1.8"
"@rspack/binding-darwin-arm64": "2.1.3",
"@rspack/binding-darwin-x64": "2.1.3",
"@rspack/binding-linux-arm64-gnu": "2.1.3",
"@rspack/binding-linux-arm64-musl": "2.1.3",
"@rspack/binding-linux-riscv64-gnu": "2.1.3",
"@rspack/binding-linux-riscv64-musl": "2.1.3",
"@rspack/binding-linux-x64-gnu": "2.1.3",
"@rspack/binding-linux-x64-musl": "2.1.3",
"@rspack/binding-wasm32-wasi": "2.1.3",
"@rspack/binding-win32-arm64-msvc": "2.1.3",
"@rspack/binding-win32-ia32-msvc": "2.1.3",
"@rspack/binding-win32-x64-msvc": "2.1.3"
}
},
"node_modules/@rspack/binding-darwin-arm64": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.1.8.tgz",
"integrity": "sha512-kia+eWtyWPvR4ntg1bWYoVU8nLPbUg2fG3zgBEocsTcsh5ZENSiEPxEKymDgMyIMONUqj611E0775cdUBoNmqw==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.1.3.tgz",
"integrity": "sha512-oOGI0RSL89Ehu9T22rugmfUY9OC2eBqLMeWRYsu7bhlUrjoXeVfGBBSEXCse666BQ1sAiM8hD/k7nqVria/okQ==",
"cpu": [
"arm64"
],
@@ -204,9 +204,9 @@
]
},
"node_modules/@rspack/binding-darwin-x64": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.1.8.tgz",
"integrity": "sha512-08pBkFhlD3Y3Qzh94w/Fc3skaIE3e96kl2P14m8+tnYTcglpOfpA2OwS3iHt9fOqy0HjoAVe6/MW3cBgs5iabA==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.1.3.tgz",
"integrity": "sha512-sVqWXNiFTMXAyN362y6IA+eJc8LXZKfHdhEJ/zDuMmRp+u2IvhgaF8tk3vX/OmeB9jydVjySijuiqk8No/FoCA==",
"cpu": [
"x64"
],
@@ -218,9 +218,9 @@
]
},
"node_modules/@rspack/binding-linux-arm64-gnu": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.1.8.tgz",
"integrity": "sha512-KLniMc9GzhKpVqhPzaJo3KJwzdAllXVVqZIk/uL1QipXOxs57fgM4u7IexKPFVla0o/u1PQG/Ah2YLDmda24Ow==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.1.3.tgz",
"integrity": "sha512-aCy9Zli/2Qf+Ee5otXfFQ6mhv5fEyn0wIoBVmouqtJoqOO21et6UTtJ+LHLsMDolwGLyHERAljeSFSmYX3/O5A==",
"cpu": [
"arm64"
],
@@ -235,9 +235,9 @@
]
},
"node_modules/@rspack/binding-linux-arm64-musl": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.1.8.tgz",
"integrity": "sha512-yUKAxHNGnICtw5RnxFWu4dHtsz/tdt7rbeFcsINNVre9HcrRxf5XP+FbOGL/SMxd9oM9XCo10paU2WckTKwbEA==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.1.3.tgz",
"integrity": "sha512-flIE7eluz0d21Fn28EVm3vPwoJooOSqtmjLFVSuOMcoCbwV9clfor195oIrAppp/W7dL/3XquFuVfrsa01Jy8Q==",
"cpu": [
"arm64"
],
@@ -252,9 +252,9 @@
]
},
"node_modules/@rspack/binding-linux-riscv64-gnu": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-2.1.8.tgz",
"integrity": "sha512-gg4S1jaitwYPHR9HZ3zNGH1EK2GXINm66p4kEpOP1gbc+akyOouVF/dMcu9NGPlRg58FbEhVRZYKu7Z/zcpKHg==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-2.1.3.tgz",
"integrity": "sha512-yojg8elye1nhsNeGncw0NrZ4pMGF7NVebR80CLg72TXyzfYwFJlFTdU5yUYb1Gy+JXIvrSCwzQt2QkyiEvmkfg==",
"cpu": [
"riscv64"
],
@@ -269,9 +269,9 @@
]
},
"node_modules/@rspack/binding-linux-riscv64-musl": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-2.1.8.tgz",
"integrity": "sha512-b/aU5j1h368SLNyz5u+flqpZVhzSZ1UIslaj9sZJuAvqkGWv3xsjc/28/PTo/RYXCxd0FNVAxTxWHKvRiAAS8w==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-2.1.3.tgz",
"integrity": "sha512-6PvbXb3FOK4X3S5QGvoSW/sqExmsvAoPnQ/YSFrXvTphkXFezA7wnobmGBHT8JQP31hcFRzJHIZSIOKktzSzzg==",
"cpu": [
"riscv64"
],
@@ -286,9 +286,9 @@
]
},
"node_modules/@rspack/binding-linux-x64-gnu": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.1.8.tgz",
"integrity": "sha512-EyegohSx0BJRqieCg9f/caCqFARRWkqI5hwJt6k530MoOTLeq8I3vsbeg24/2MktwIC1dmJi8bl0+WhPKQs4eQ==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.1.3.tgz",
"integrity": "sha512-lMXjoGKf0SnviH596fmTszgtnXLHmWOoE90G8grG9MvKVa3pelRmfps5ewZL9s8ENf3NXRfOxIhIf/M9as6MqA==",
"cpu": [
"x64"
],
@@ -303,9 +303,9 @@
]
},
"node_modules/@rspack/binding-linux-x64-musl": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.1.8.tgz",
"integrity": "sha512-I6E+goN+UQ297q4r1qdbiAyNCI3t0+a5Y0xDIAPOZfRDRxDTnH/LF8/y65gjsJoKRKyn7zxRC0T/NURTkRNQ9A==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.1.3.tgz",
"integrity": "sha512-0esT35v7pW2ZsJTMc/zDUHwpNXlPqyUCyuiLJvrAAUcdENrgOVe4DmFrgVJ2hwqI4GjeN1VBnGRJ8c+edAHH5w==",
"cpu": [
"x64"
],
@@ -320,9 +320,9 @@
]
},
"node_modules/@rspack/binding-wasm32-wasi": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.1.8.tgz",
"integrity": "sha512-om7GAKWAU3lcSvbCon2m7mzw8v9OTrO2LW2MZ1lGe/uVJJmwGGkl9HVoXFyWFLrN6YVFyx8iP+AkN4owDWB9Cw==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.1.3.tgz",
"integrity": "sha512-UsrDjD59UEP0mhfN/Z+uTc3vLgiUvIr+mn92WC1sbQi9gtZohTYvaQYFhWuMkBqsACGcmZp704JAbbSrVrVYCA==",
"cpu": [
"wasm32"
],
@@ -330,15 +330,15 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.11.3",
"@emnapi/runtime": "1.11.3",
"@emnapi/core": "1.11.1",
"@emnapi/runtime": "1.11.1",
"@napi-rs/wasm-runtime": "1.1.6"
}
},
"node_modules/@rspack/binding-win32-arm64-msvc": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.1.8.tgz",
"integrity": "sha512-WDnsP/SUb9zbxyGX9XjPw5AXrX86u5oidn0MDdfJduOOqdCSpHwmRjlQ8NUJhbBq9WqVJMFlcab7NwZVWX/yyg==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.1.3.tgz",
"integrity": "sha512-42RS/SwKBTkNvXIPZXqTn0yEFN8zwGRfRe/ly0GDvPp/KxpLMFWxJmLxgtoLompU+0UCGvV4KpcBENt3oWs6BA==",
"cpu": [
"arm64"
],
@@ -350,9 +350,9 @@
]
},
"node_modules/@rspack/binding-win32-ia32-msvc": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.1.8.tgz",
"integrity": "sha512-QiMQMPNDiY3dhhaIdaFPzcPDC06cEYkNY89ea+EmDvNVgZq6V+2mFS/WnzZVMeEbGAYJCjsv/ABhhLT1hlYMvg==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.1.3.tgz",
"integrity": "sha512-thk43H1JHHbetNF3txcsGXeOwZi2m6Luf/uVUqNORXjxr5VV8woHAgaAx4QXVZRg70z1VDjd8mBWnHBnKI0FGA==",
"cpu": [
"ia32"
],
@@ -364,9 +364,9 @@
]
},
"node_modules/@rspack/binding-win32-x64-msvc": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.1.8.tgz",
"integrity": "sha512-b7sA5eB64vo2mbsuc//MOYzVLeCKHPn0dfP/GmNEoHdWbhRgZ/orZLWurYMQj04ELTLW6YCJEy59g5KRzNYHfw==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.1.3.tgz",
"integrity": "sha512-ObaUcj+BHo/aBL1weyM3orApaHyAR5Phr3YNEpQEifxjZbNKM1iO5X5prN4OqEv3H+7o5e/Wh9Fy3N7Vvd+psA==",
"cpu": [
"x64"
],
@@ -378,13 +378,13 @@
]
},
"node_modules/@rspack/core": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.1.8.tgz",
"integrity": "sha512-na1kyA6Mj8/LWw9O3A8NsrG9rNKN3Iq2WiXrEuIwsU5r/Nl/evm3hO7bWKHxgsRyydI6W7okwx3MXgf8rzel6g==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.1.3.tgz",
"integrity": "sha512-1iGnxLrP+iyY0ZSjLZeQTaxrISpQz4yLyqJPmaF/l4uw27/dBcrloszAeLOJQ9jiv7EJtU0T5zB+LOFPpivIxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/binding": "2.1.8"
"@rspack/binding": "2.1.3"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
@@ -419,21 +419,20 @@
}
},
"node_modules/@rspress/core": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.19.tgz",
"integrity": "sha512-m0KfQNNXxjP975652nIOwUTqrvbJapRp4NpnoQMc5heawGhkC4+F7zTHvxY+huvzGYnKtL2fMMTrgWhEjKkrug==",
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.18.tgz",
"integrity": "sha512-DBpsr/6XAItQkZPL5FSjthLGuzdG9ks/7EOxkqaaXp93uAT54LO0PD5Dkoy6ydup1PnvkpHyiHpUyDcnHHTVhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@rsbuild/core": "^2.1.8",
"@rsbuild/core": "^2.1.5",
"@rsbuild/plugin-react": "~2.1.0",
"@rspress/shared": "2.0.19",
"@rspress/shared": "2.0.18",
"@shikijs/rehype": "^4.2.0",
"@types/mdast": "^4.0.4",
"@types/react": "^19.2.17",
"@unhead/react": "^2.1.16",
"@types/unist": "^3.0.3",
"@unhead/react": "^2.1.15",
"body-scroll-lock": "4.0.0-beta.0",
"clsx": "2.1.1",
"copy-to-clipboard": "^3.3.3",
@@ -444,9 +443,10 @@
"mdast-util-mdxjs-esm": "^2.0.1",
"medium-zoom": "1.1.0",
"nprogress": "^0.2.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-lazy-with-preload": "^2.2.1",
"react-reconciler": "0.33.0",
"react-render-to-markdown": "19.1.0",
"react-router-dom": "^7.18.1",
"rehype-external-links": "^3.0.0",
@@ -460,9 +460,9 @@
"scroll-into-view-if-needed": "^3.1.0",
"shiki": "^4.2.0",
"unified": "^11.0.5",
"unist-util-remove": "^4.0.0",
"unist-util-visit": "^5.1.0",
"unist-util-visit-children": "^3.0.0",
"vfile": "^6.0.3"
"unist-util-visit-children": "^3.0.0"
},
"bin": {
"rspress": "bin/rspress.js"
@@ -472,9 +472,9 @@
}
},
"node_modules/@rspress/plugin-client-redirects": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.19.tgz",
"integrity": "sha512-BUwgEyj8vCkU58cV3Gp97aTSnfGJm7aXh40K0Dot1BqsCQ8ekdsMuaLXrICROdP9/ctd4oALtLBkOE+PKTa4Sg==",
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.18.tgz",
"integrity": "sha512-q70ufN0S125kl0QlucO7JuiZ0WJaxFOtPPoGu7eWwam9fb+i4YsWnOWtAKSKGH4JP5+897mgpDPP+dmfl9DPCw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -485,9 +485,9 @@
}
},
"node_modules/@rspress/plugin-sitemap": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.19.tgz",
"integrity": "sha512-9ydrpj9hmmXWKWKIXUqJ1UrfxSvfxhHZRBZLb5OkTlOuQBNAg6BpsrW1WHoJqJr7p+3zqlz7S/9HTzSA3+EYcA==",
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.18.tgz",
"integrity": "sha512-XfaXzZx+ASN9OMTc0fOvW8QmbmyCyDWkO8xJj4XF4E0YJtsu7j/65+v8/KXXdwpywOfUIQ6M94/meRTy2urNmQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -498,16 +498,14 @@
}
},
"node_modules/@rspress/shared": {
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.19.tgz",
"integrity": "sha512-INrETllWuR49lksqCz+xeLSO6rKtHA+5Ix2YQWcP9nerCuTSyGYwH8eBC0HrIacJkGHxB8wkkbV99tyzGtY8Wg==",
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.18.tgz",
"integrity": "sha512-GJswqJQCPSxvBt5r+gJzz8Em8EEK/sOmCQjTpemTvldvxr1Lva85BGVnSQZOgofnM0nrjt18kn4mmnuuSstbpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rsbuild/core": "^2.1.8",
"@rsbuild/core": "^2.1.5",
"@shikijs/rehype": "^4.2.0",
"@types/react": "^19.2.17",
"mdast-util-mdx-jsx": "^3.2.0",
"unified": "^11.0.5"
}
},
@@ -725,6 +723,7 @@
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1084,13 +1083,13 @@
"license": "ISC"
},
"node_modules/@unhead/react": {
"version": "2.1.17",
"resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.17.tgz",
"integrity": "sha512-KmcYksDjlLozL0fxUjIDwH/0k6+Lg2HWHLUPlkRW8Gl08hdB1AbLB5iU9QxFvkaT8p0Q4dFEqZWnz3g6RMeiRA==",
"version": "2.1.15",
"resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.15.tgz",
"integrity": "sha512-5hfAaZ3XJq9JkspRzZdSPsMrXXA8v/SKiEOxZcN9L40o44byF/50bcQuOLgSSCAx8802mI5VG32KZXWTtsLu9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"unhead": "2.1.17"
"unhead": "2.1.15"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
@@ -1273,7 +1272,8 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/debug": {
"version": "4.4.3",
@@ -3176,9 +3176,9 @@
}
},
"node_modules/react": {
"version": "19.2.8",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3186,16 +3186,16 @@
}
},
"node_modules/react-dom": {
"version": "19.2.8",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.8"
"react": "^19.2.7"
}
},
"node_modules/react-lazy-with-preload": {
@@ -3736,9 +3736,9 @@
}
},
"node_modules/unhead": {
"version": "2.1.17",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.17.tgz",
"integrity": "sha512-HLMKXOszRhAPBrr6VlqCeVeJq2kbC4kXwzGLEZvvojPLWNYTJw22xG7Bfwhsvs31+IBet3Wl8ADg9dwYdyphfQ==",
"version": "2.1.15",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.15.tgz",
"integrity": "sha512-MCt5T90mCWyr3Z6pUCdM9lVRXoMoVBlL7z7U4CYVIiaDiuzad/UCfLuMqz5MeNmpZUgoBCQnrucJimU7EZR+XA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3810,6 +3810,22 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-remove": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz",
"integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/unist": "^3.0.0",
"unist-util-is": "^6.0.0",
"unist-util-visit-parents": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/unist-util-stringify-position": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
-1
View File
@@ -541,7 +541,6 @@ pub(crate) async fn create_room_route(
&content,
&state_key,
None,
None,
)
.boxed()
.await
+1 -11
View File
@@ -1,11 +1,7 @@
use std::collections::BTreeMap;
use axum::extract::State;
use conduwuit::{
Err, Result, err,
matrix::pdu::{PartialPdu, sticky},
utils,
};
use conduwuit::{Err, Result, err, matrix::pdu::PartialPdu, utils};
use ruma::{api::client::message::send_message_event, events::MessageLikeEventType};
use serde_json::from_str;
@@ -85,12 +81,6 @@ pub(crate) async fn send_message_event_route(
PartialPdu {
event_type: body.event_type.clone().into(),
content,
sticky: services
.config
.allow_sticky_events
.then_some(body.sticky_duration_ms)
.flatten()
.map(sticky::object),
unsigned: Some(unsigned),
timestamp: if body.identity.is_appservice() {
body.timestamp
-1
View File
@@ -49,7 +49,6 @@ pub(crate) async fn send_state_event_for_key_route(
&body.event_type,
&body.body.body,
&body.state_key,
body.sticky_duration_ms,
if body.identity.is_appservice() {
body.timestamp
} else {
+4 -81
View File
@@ -4,7 +4,7 @@
Result, at, debug_warn, err, extract_variant,
matrix::{
Event,
pdu::{PduCount, PduEvent, sticky},
pdu::{PduCount, PduEvent},
},
trace,
utils::{
@@ -20,12 +20,12 @@
future::{OptionFuture, join, join3, join4, try_join, try_join3},
};
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedUserId, RoomId, UserId,
OwnedRoomId, OwnedUserId, RoomId, UserId,
api::client::sync::sync_events::{
UnreadNotificationsCount,
v3::{
Ephemeral, JoinedRoom, RoomAccountData, RoomSummary, State as RoomState, StateEvents,
Sticky, Timeline,
Timeline,
},
},
assign,
@@ -42,7 +42,7 @@
use super::{load_timeline, share_encrypted_room};
use crate::client::{
TimelinePdus, ignored_filter, is_ignored_pdu,
TimelinePdus, ignored_filter,
sync::v3::{
DEFAULT_TIMELINE_LIMIT, DeviceListUpdates, SyncContext, prepare_lazily_loaded_members,
state::{build_state_incremental, build_state_initial},
@@ -76,7 +76,6 @@ pub(super) async fn load_joined_room(
StateAndTimeline {
state_events,
timeline,
sticky,
summary,
notification_counts,
device_list_updates,
@@ -112,7 +111,6 @@ pub(super) async fn load_joined_room(
RoomState::Before(state_events)
},
ephemeral,
sticky,
unread_thread_notifications: BTreeMap::new(),
});
@@ -253,82 +251,11 @@ async fn build_ephemeral(
struct StateAndTimeline {
state_events: Vec<PduEvent>,
timeline: Timeline,
sticky: Sticky,
summary: Option<RoomSummary>,
notification_counts: Option<UnreadNotificationsCount>,
device_list_updates: DeviceListUpdates,
}
async fn build_sticky_events(
services: &Services,
sync_context: SyncContext<'_>,
room_id: &RoomId,
timeline: &TimelinePdus,
joined_since_last_sync: bool,
) -> Sticky {
if !services.config.allow_sticky_events {
return Sticky::new();
}
let SyncContext {
syncing_user,
last_sync_end_count,
current_count,
..
} = sync_context;
let timeline_event_ids: HashSet<_> =
timeline.pdus.iter().map(|(_, pdu)| &pdu.event_id).collect();
let now = u64::from(MilliSecondsSinceUnixEpoch::now().get());
// on join the client needs every unexpired sticky event, not just the ones
// since its last sync
let since = (!joined_since_last_sync)
.then_some(last_sync_end_count)
.flatten()
.map(PduCount::Normal);
// nothing older than this can still be sticky, so the walk back never goes far
let oldest_sticky_ts = now.saturating_sub(sticky::MAX_DURATION_MS);
let mut events = Vec::new();
let pdus = services
.rooms
.timeline
.pdus_rev(room_id, Some(PduCount::Normal(current_count).saturating_add(1)))
.ignore_err();
pin!(pdus);
while let Some((count, mut pdu)) = pdus.next().await {
if since.is_some_and(|since| count <= since)
|| u64::from(pdu.origin_server_ts) < oldest_sticky_ts
{
break;
}
let is_sticky = pdu
.sticky
.as_deref()
.is_some_and(|sticky| sticky::is_sticky(pdu.origin_server_ts, sticky, now));
if !is_sticky
|| timeline_event_ids.contains(&pdu.event_id)
|| is_ignored_pdu(services, &pdu, syncing_user)
.await
.unwrap_or(true)
{
continue;
}
pdu.set_unsigned(Some(syncing_user));
events.push(Event::into_format(pdu));
}
events.reverse();
assign!(Sticky::new(), { events })
}
/// Compute changes to the room's state and timeline.
#[tracing::instrument(level = "debug", skip_all)]
async fn build_state_and_timeline(
@@ -347,9 +274,6 @@ async fn build_state_and_timeline(
check_joined_since_last_sync(services, shortstatehashes, sync_context),
)
.await?;
let sticky =
build_sticky_events(services, sync_context, room_id, &timeline, joined_since_last_sync)
.await;
let state_events = build_state_events(
services,
@@ -418,7 +342,6 @@ async fn build_state_and_timeline(
prev_batch: prev_batch.as_ref().map(ToString::to_string),
events: filtered_timeline,
}),
sticky,
summary,
notification_counts,
device_list_updates,
-1
View File
@@ -328,7 +328,6 @@ fn create_dummy_leave_event(
.expect("Timestamp is valid js_int value"),
kind: TimelineEventType::RoomMember,
content: RawValue::from_string(r#"{"membership": "leave"}"#.to_owned()).unwrap(),
sticky: None,
state_key: Some(syncing_user.as_str().into()),
unsigned: None,
// The following keys are dropped on conversion
+1 -2
View File
@@ -201,7 +201,6 @@ pub(crate) async fn sync_events_route(
.update_device_last_seen(sender_user, Some(sender_device), client_ip)
.await;
let mut wake_receiver = services.sync.subscribe_to_wake(sender_user).await;
let response = build_sync_events(&services, &body).await?;
if body.body.full_state
|| !(response.rooms.is_empty()
@@ -217,7 +216,7 @@ pub(crate) async fn sync_events_route(
// Stop hanging if new info arrives
let default = Duration::from_secs(30);
let duration = cmp::min(body.body.timeout.unwrap_or(default), default);
_ = tokio::time::timeout(duration, wake_receiver.changed()).await;
_ = tokio::time::timeout(duration, services.sync.wait_for_wake(sender_user)).await;
// Retry returning data
build_sync_events(&services, &body).await
+106 -569
View File
@@ -1,16 +1,14 @@
use std::{
cmp::{self, Ordering},
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
ops::Deref,
time::Duration,
};
use axum::extract::State;
use conduwuit::{
Err, Error, Result, at, error, extract_variant, is_equal_to,
matrix::{
Event, TypeStateKey,
pdu::{PduCount, PduEvent, sticky},
},
matrix::{Event, TypeStateKey, pdu::PduCount},
trace,
utils::{
BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt,
@@ -20,31 +18,22 @@
},
warn,
};
use conduwuit_service::{
Services,
rooms::read_receipt::pack_receipts,
sync::{SnakeConnectionsKey, into_snake_key},
};
use conduwuit_service::{Services, rooms::read_receipt::pack_receipts, sync::into_snake_key};
use futures::{
FutureExt, StreamExt, TryFutureExt,
FutureExt, Stream, StreamExt, TryFutureExt,
future::{OptionFuture, join3, try_join4},
pin_mut,
};
use ruma::{
DeviceId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, RoomId, UInt, UserId,
DeviceId, OwnedEventId, OwnedRoomId, RoomId, UInt, UserId,
api::client::sync::sync_events::{
self, DeviceLists, UnreadNotificationsCount, v5::request::ExtensionRoomConfig,
},
assign,
directory::RoomTypeFilter,
events::{
AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, AnySyncStateEvent,
GlobalAccountDataEventType, StateEventType, TimelineEventType,
direct::DirectEvent,
room::{
create::RoomCreateEventContent,
member::{MembershipState, RoomMemberEventContent},
},
AnySyncEphemeralRoomEvent, AnySyncStateEvent, StateEventType, TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent},
typing::{SyncTypingEvent, TypingEventContent},
},
serde::Raw,
@@ -65,15 +54,6 @@
type SyncInfo<'a> = (&'a UserId, &'a DeviceId, u64, &'a sync_events::v5::Request);
type TodoRooms = BTreeMap<OwnedRoomId, (BTreeSet<TypeStateKey>, usize, u64)>;
type KnownRooms = BTreeMap<String, BTreeMap<OwnedRoomId, u64>>;
type KnownRoomUpdates = BTreeMap<String, BTreeSet<OwnedRoomId>>;
/// Default and maximum number of sticky events returned per response.
const DEFAULT_STICKY_LIMIT: usize = 100;
struct SyncCollection {
response: sync_events::v5::Response,
known_room_updates: KnownRoomUpdates,
}
/// `POST /_matrix/client/unstable/org.matrix.simplified_msc3575/sync`
/// ([MSC4186])
@@ -100,6 +80,8 @@ pub(crate) async fn sync_events_v5_route(
let mut body = body.body;
let next_batch = services.globals.next_count()?;
let conn_id = body.conn_id.clone();
let globalsince = body
@@ -126,69 +108,6 @@ pub(crate) async fn sync_events_v5_route(
.sync
.update_snake_sync_request_with_cache(&snake_key, &mut body);
let mut wake_receiver = services.sync.subscribe_to_wake(sender_user).await;
let mut collection = collect_sync_response(
services,
sender_user,
sender_device,
&body,
globalsince,
&known_rooms,
)
.await?;
if response_is_empty(&collection.response) {
let default = Duration::from_secs(30);
let duration = cmp::min(body.timeout.unwrap_or(default), default);
let woke = tokio::time::timeout(duration, wake_receiver.changed())
.await
.is_ok();
if woke {
collection = collect_sync_response(
services,
sender_user,
sender_device,
&body,
globalsince,
&known_rooms,
)
.await?;
}
}
commit_sync_collection(
services,
sender_user,
sender_device,
&body,
globalsince,
&snake_key,
&collection.known_room_updates,
)
.await;
trace!(
rooms = ?collection.response.rooms.len(),
account_data = ?collection.response.extensions.account_data.rooms.len(),
receipts = ?collection.response.extensions.receipts.rooms.len(),
"responding to request with"
);
Ok(collection.response)
}
async fn collect_sync_response(
services: &Services,
sender_user: &UserId,
sender_device: &DeviceId,
body: &sync_events::v5::Request,
globalsince: u64,
known_rooms: &KnownRooms,
) -> Result<SyncCollection> {
// Bounds every read below and becomes the `pos` returned to the client. Zero
// would be echoed back as a request for an initial sync.
let next_batch = services.globals.current_count()?.max(1);
let all_joined_rooms = services
.rooms
.state_cache
@@ -227,10 +146,6 @@ async fn collect_sync_response(
.collect();
let all_joined_rooms = all_joined_rooms.iter().map(AsRef::as_ref);
let joined_room_ids = all_joined_rooms
.clone()
.map(ToOwned::to_owned)
.collect::<BTreeSet<_>>();
let all_invited_rooms = all_invited_rooms.iter().map(AsRef::as_ref);
let all_knocked_rooms = all_knocked_rooms.iter().map(AsRef::as_ref);
let all_rooms = all_joined_rooms
@@ -238,12 +153,11 @@ async fn collect_sync_response(
.chain(all_invited_rooms.clone())
.chain(all_knocked_rooms.clone());
let pos = next_batch.to_string();
let pos = next_batch.clone().to_string();
let mut todo_rooms: TodoRooms = BTreeMap::new();
let mut sticky_rooms = BTreeSet::new();
let sync_info: SyncInfo<'_> = (sender_user, sender_device, globalsince, body);
let sync_info: SyncInfo<'_> = (sender_user, sender_device, globalsince, &body);
let account_data = collect_account_data(services, sync_info).map(Ok);
@@ -262,7 +176,6 @@ async fn collect_sync_response(
to_device,
receipts,
typing: sync_events::v5::response::Typing::default(),
sticky_events: sync_events::v5::response::StickyEvents::default(),
});
let mut response = assign!(sync_events::v5::Response::new(pos), {
@@ -271,46 +184,21 @@ async fn collect_sync_response(
rooms: BTreeMap::new(),
extensions,
});
let direct_rooms = if body.lists.values().any(|list| {
list.filters
.as_ref()
.is_some_and(|filters| filters.is_dm.is_some())
}) {
match services
.account_data
.get_global::<DirectEvent>(sender_user, GlobalAccountDataEventType::Direct)
.await
{
| Ok(event) => event.content.0.into_values().flatten().collect(),
| Err(error) if error.is_not_found() => HashSet::new(),
| Err(error) => return Err(error),
}
} else {
HashSet::new()
};
let mut known_room_updates = handle_lists(
handle_lists(
services,
sender_user,
body,
sync_info,
all_invited_rooms.clone(),
all_joined_rooms.clone(),
all_rooms,
&direct_rooms,
&mut todo_rooms,
&mut sticky_rooms,
known_rooms,
&known_rooms,
&mut response,
)
.await;
known_room_updates.insert(
"subscriptions".to_owned(),
fetch_subscriptions(services, body, known_rooms, &allowed_rooms, &mut todo_rooms).await,
);
sticky_rooms.extend(body.room_subscriptions.keys().cloned());
fetch_subscriptions(services, sync_info, &known_rooms, &allowed_rooms, &mut todo_rooms).await;
let mut timeline_event_ids = BTreeSet::new();
response.rooms = process_rooms(
services,
sender_user,
@@ -319,29 +207,10 @@ async fn collect_sync_response(
all_knocked_rooms.clone(),
&todo_rooms,
&mut response,
body,
&mut timeline_event_ids,
&body,
)
.await?;
response.extensions.sticky_events = collect_sticky_events(
services,
sender_user,
body,
&sticky_rooms,
&joined_room_ids,
&todo_rooms,
&timeline_event_ids,
)
.await?;
response.extensions.typing =
collect_typing_events(services, sender_user, body, &todo_rooms).await?;
Ok(SyncCollection { response, known_room_updates })
}
fn response_is_empty(response: &sync_events::v5::Response) -> bool {
let no_account_data = response.extensions.account_data.global.is_empty()
&& response
.extensions
@@ -349,61 +218,47 @@ fn response_is_empty(response: &sync_events::v5::Response) -> bool {
.rooms
.values()
.all(Vec::is_empty);
let no_room_data = response.rooms.iter().all(|(id, room)| {
room.timeline.is_empty()
&& room.required_state.is_empty()
&& room.invite_state.is_none()
let no_room_data = response.rooms.iter().all(|(id, r)| {
r.timeline.is_empty()
&& r.required_state.is_empty()
&& r.invite_state.is_none()
&& !response.extensions.receipts.rooms.contains_key(id)
});
let no_to_device_messages = response
.extensions
.to_device
.as_ref()
.clone()
.is_none_or(|to| to.events.is_empty());
no_account_data
&& no_room_data
&& no_to_device_messages
&& response.extensions.sticky_events.is_empty()
}
async fn commit_sync_collection(
services: &Services,
sender_user: &UserId,
sender_device: &DeviceId,
body: &sync_events::v5::Request,
globalsince: u64,
snake_key: &SnakeConnectionsKey,
known_room_updates: &KnownRoomUpdates,
) {
let (.., conn_id) = snake_key;
if conn_id.is_some() {
for (list_id, rooms) in known_room_updates {
services.sync.update_snake_sync_known_rooms(
snake_key,
list_id.clone(),
rooms.clone(),
globalsince,
);
}
if no_account_data && no_room_data && no_to_device_messages {
// Hang a few seconds so requests are not spammed
// Stop hanging if new info arrives
let default = Duration::from_secs(30);
let duration = cmp::min(body.timeout.unwrap_or(default), default);
_ = tokio::time::timeout(duration, services.sync.wait_for_wake(sender_user)).await;
}
if body.extensions.to_device.enabled.unwrap_or(false) {
services
.users
.remove_to_device_events(sender_user, sender_device, globalsince)
.await;
}
let typing = collect_typing_events(services, sender_user, &body, &todo_rooms).await?;
response.extensions.typing = typing;
trace!(
rooms = ?response.rooms.len(),
account_data = ?response.extensions.account_data.rooms.len(),
receipts = ?response.extensions.receipts.rooms.len(),
"responding to request with"
);
Ok(response)
}
async fn fetch_subscriptions(
services: &Services,
body: &sync_events::v5::Request,
(sender_user, sender_device, globalsince, body): SyncInfo<'_>,
known_rooms: &KnownRooms,
allowed_rooms: &BTreeSet<OwnedRoomId>,
todo_rooms: &mut TodoRooms,
) -> BTreeSet<OwnedRoomId> {
) {
let mut known_subscription_rooms = BTreeSet::new();
for (room_id, room) in &body.room_subscriptions {
// Silently ignore subscriptions to rooms the user is not a member of
@@ -452,97 +307,54 @@ async fn fetch_subscriptions(
// body.room_subscriptions.remove(&r);
//}
known_subscription_rooms
if let Some(conn_id) = body.conn_id.clone() {
let snake_key = into_snake_key(sender_user, sender_device, conn_id);
services.sync.update_snake_sync_known_rooms(
&snake_key,
"subscriptions".to_owned(),
known_subscription_rooms,
globalsince,
);
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_lists<'a, Rooms, AllRooms>(
services: &Services,
sender_user: &UserId,
body: &sync_events::v5::Request,
(sender_user, sender_device, globalsince, body): SyncInfo<'_>,
all_invited_rooms: Rooms,
all_joined_rooms: Rooms,
all_rooms: AllRooms,
direct_rooms: &HashSet<OwnedRoomId>,
todo_rooms: &'a mut TodoRooms,
sticky_rooms: &mut BTreeSet<OwnedRoomId>,
known_rooms: &'a KnownRooms,
response: &'_ mut sync_events::v5::Response,
) -> KnownRoomUpdates
) -> KnownRooms
where
Rooms: Iterator<Item = &'a RoomId> + Clone + Send + 'a,
AllRooms: Iterator<Item = &'a RoomId> + Clone + Send + 'a,
{
// TODO MSC4186: ruma's `ListFilters` does not model `spaces`, `tags` or
// `not_tags`, nor the rename of `is_invite` to `is_invited`.
let invited_rooms: HashSet<&RoomId> = all_invited_rooms.clone().collect();
// Memoised for the whole request; lists commonly overlap.
let mut encrypted_rooms: HashMap<&RoomId, bool> = HashMap::new();
let mut room_types: HashMap<&RoomId, Option<RoomTypeFilter>> = HashMap::new();
let mut known_room_updates = KnownRoomUpdates::new();
// TODO MSC4186: Implement remaining list filters: is_dm, is_encrypted,
// room_types.
for (list_id, list) in &body.lists {
let filters = list.filters.as_ref();
let is_dm = filters.and_then(|filters| filters.is_dm);
let is_encrypted = filters.and_then(|filters| filters.is_encrypted);
let room_types_filter = filters.map_or(&[][..], |filters| filters.room_types.as_slice());
let not_room_types = filters.map_or(&[][..], |filters| filters.not_room_types.as_slice());
let filter_room_types = !room_types_filter.is_empty() || !not_room_types.is_empty();
let candidate_rooms: Vec<&RoomId> = match filters.and_then(|filters| filters.is_invite) {
let active_rooms: Vec<_> = match list.filters.as_ref().and_then(|f| f.is_invite) {
| None => all_rooms.clone().collect(),
| Some(true) => all_invited_rooms.clone().collect(),
| Some(false) => all_joined_rooms.clone().collect(),
};
let mut active_rooms: Vec<&RoomId> = Vec::with_capacity(candidate_rooms.len());
for room_id in candidate_rooms {
if !matches_bool_filter(direct_rooms.contains(room_id), is_dm) {
continue;
}
let invited = invited_rooms.contains(room_id);
if is_encrypted.is_some() {
let encrypted = match encrypted_rooms.get(room_id) {
| Some(encrypted) => *encrypted,
| None => {
let encrypted =
room_is_encrypted(services, sender_user, room_id, invited).await;
encrypted_rooms.insert(room_id, encrypted);
encrypted
},
};
if !matches_bool_filter(encrypted, is_encrypted) {
continue;
}
}
if filter_room_types {
let room_type = match room_types.get(room_id) {
| Some(room_type) => room_type.clone(),
| None => {
let room_type =
room_type_filter(services, sender_user, room_id, invited).await;
room_types.insert(room_id, room_type.clone());
room_type
},
};
let Some(room_type) = room_type else {
continue;
};
if !matches_room_type(&room_type, room_types_filter, not_room_types) {
continue;
}
}
active_rooms.push(room_id);
}
sticky_rooms.extend(active_rooms.iter().map(|room_id| (*room_id).to_owned()));
let active_rooms = match list.filters.as_ref().map(|f| &f.not_room_types) {
| None => active_rooms,
| Some(filter) if filter.is_empty() => active_rooms,
| Some(value) =>
filter_rooms(
services,
value,
&true,
active_rooms.iter().stream().map(Deref::deref),
)
.collect()
.await,
};
let mut new_known_rooms: BTreeSet<OwnedRoomId> = BTreeSet::new();
@@ -599,96 +411,18 @@ async fn handle_lists<'a, Rooms, AllRooms>(
}),
);
known_room_updates.insert(list_id.clone(), new_known_rooms);
if let Some(conn_id) = body.conn_id.clone() {
let snake_key = into_snake_key(sender_user, sender_device, conn_id);
services.sync.update_snake_sync_known_rooms(
&snake_key,
list_id.clone(),
new_known_rooms,
globalsince,
);
}
}
known_room_updates
}
fn matches_bool_filter(value: bool, filter: Option<bool>) -> bool {
filter.is_none_or(|expected| value == expected)
}
/// Rooms we are only invited to have no state locally, so their stripped
/// invite state answers for them instead.
async fn room_is_encrypted(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
invited: bool,
) -> bool {
if !invited {
return services
.rooms
.state_accessor
.is_encrypted_room(room_id)
.await;
}
stripped_state_event(services, sender_user, room_id, &StateEventType::RoomEncryption)
.await
.is_some()
}
/// Returns `None` if the type could not be determined, in which case the room
/// is excluded from the list.
///
/// Stripped invite state is only recommended to carry `m.room.create`, so an
/// invite without one is treated as untyped rather than hidden from every list.
async fn room_type_filter(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
invited: bool,
) -> Option<RoomTypeFilter> {
if invited {
let content: Option<RoomCreateEventContent> =
stripped_state_event(services, sender_user, room_id, &StateEventType::RoomCreate)
.await
.and_then(|event| event.get_field("content").ok().flatten());
return Some(RoomTypeFilter::from(content.and_then(|content| content.room_type)));
}
match services.rooms.state_accessor.get_room_type(room_id).await {
| Ok(room_type) => Some(RoomTypeFilter::from(Some(room_type))),
| Err(error) if error.is_not_found() => Some(RoomTypeFilter::Default),
| Err(error) => {
warn!(%room_id, %error, "Failed to fetch room type for a sliding sync list filter");
None
},
}
}
async fn stripped_state_event(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
event_type: &StateEventType,
) -> Option<Raw<AnyStrippedStateEvent>> {
services
.rooms
.state_cache
.invite_state(sender_user, room_id)
.await
.ok()?
.into_iter()
.find(|event| {
event
.get_field::<StateEventType>("type")
.ok()
.flatten()
.as_ref() == Some(event_type)
})
}
fn matches_room_type(
room_type: &RoomTypeFilter,
room_types: &[RoomTypeFilter],
not_room_types: &[RoomTypeFilter],
) -> bool {
!not_room_types.contains(room_type)
&& (room_types.is_empty() || room_types.contains(room_type))
BTreeMap::default()
}
#[allow(clippy::too_many_arguments)]
@@ -701,7 +435,6 @@ async fn process_rooms<'a, Rooms>(
todo_rooms: &TodoRooms,
response: &mut sync_events::v5::Response,
body: &sync_events::v5::Request,
timeline_event_ids: &mut BTreeSet<OwnedEventId>,
) -> Result<BTreeMap<OwnedRoomId, sync_events::v5::response::Room>>
where
Rooms: Iterator<Item = &'a RoomId> + Clone + Send + 'a,
@@ -905,7 +638,6 @@ async fn process_rooms<'a, Rooms>(
.map(Event::into_format)
.collect()
.await;
timeline_event_ids.extend(timeline_pdus.iter().map(|(_, pdu)| pdu.event_id.clone()));
for (_, pdu) in timeline_pdus {
let ts = pdu.origin_server_ts;
@@ -1043,124 +775,6 @@ async fn process_rooms<'a, Rooms>(
Ok(rooms)
}
async fn collect_sticky_events(
services: &Services,
sender_user: &UserId,
body: &sync_events::v5::Request,
sticky_rooms: &BTreeSet<OwnedRoomId>,
joined_room_ids: &BTreeSet<OwnedRoomId>,
todo_rooms: &TodoRooms,
timeline_event_ids: &BTreeSet<OwnedEventId>,
) -> Result<sync_events::v5::response::StickyEvents> {
if !services.config.allow_sticky_events
|| !body.extensions.sticky_events.enabled.unwrap_or(false)
{
return Ok(sync_events::v5::response::StickyEvents::default());
}
let since = body
.extensions
.sticky_events
.since
.as_deref()
.and_then(|token| token.parse::<u64>().ok())
.unwrap_or(0);
let limit = body
.extensions
.sticky_events
.limit
.map_or(DEFAULT_STICKY_LIMIT, usize_from_ruma)
.clamp(1, DEFAULT_STICKY_LIMIT);
let now = u64::from(MilliSecondsSinceUnixEpoch::now().get());
let oldest_sticky_ts = now.saturating_sub(sticky::MAX_DURATION_MS);
let mut backlog = Vec::new();
let mut stream = Vec::new();
for room_id in sticky_rooms
.iter()
.filter(|room_id| joined_room_ids.contains(*room_id))
{
// a room the client has not seen before needs its whole backlog, which a
// single stream position cannot express
let initial = todo_rooms
.get(room_id)
.is_some_and(|&(_, _, roomsince)| roomsince == 0);
let pdus = services.rooms.timeline.pdus_rev(room_id, None).ignore_err();
pin_mut!(pdus);
while let Some((count, pdu)) = pdus.next().await {
// nothing older than this can still be sticky, and on the stream we
// have already delivered everything up to `since`
if u64::from(pdu.origin_server_ts) < oldest_sticky_ts
|| (!initial && count <= PduCount::Normal(since))
{
break;
}
let PduCount::Normal(count) = count else {
continue;
};
let is_sticky = pdu
.sticky
.as_deref()
.is_some_and(|sticky| sticky::is_sticky(pdu.origin_server_ts, sticky, now));
if !is_sticky || timeline_event_ids.contains(&pdu.event_id) {
continue;
}
if services
.users
.user_is_ignored(pdu.sender(), sender_user)
.await
{
continue;
}
if count > since {
stream.push((count, room_id.clone(), pdu));
} else {
backlog.push((room_id.clone(), pdu));
}
}
}
stream.sort_by_key(|(count, ..)| *count);
let mut response = sync_events::v5::response::StickyEvents::default();
// MSC4480 lets us ignore the limit for a newly visible room's backlog
for (room_id, pdu) in backlog {
push_sticky_event(&mut response, sender_user, room_id, pdu);
}
let mut next_batch = None;
for (count, room_id, pdu) in stream.into_iter().take(limit) {
push_sticky_event(&mut response, sender_user, room_id, pdu);
next_batch = Some(count);
}
if !response.rooms.is_empty() {
response.next_batch = Some(next_batch.unwrap_or(since).to_string());
}
Ok(response)
}
fn push_sticky_event(
response: &mut sync_events::v5::response::StickyEvents,
sender_user: &UserId,
room_id: OwnedRoomId,
mut pdu: PduEvent,
) {
pdu.set_unsigned(Some(sender_user));
response
.rooms
.entry(room_id)
.or_default()
.events
.push(Event::into_format(pdu));
}
/// Collect the required state events for a room
async fn collect_required_state(
services: &Services,
@@ -1525,11 +1139,16 @@ async fn collect_to_device(
return None;
}
services
.users
.remove_to_device_events(sender_user, sender_device, globalsince)
.await;
Some(assign!(sync_events::v5::response::ToDevice::default(), {
next_batch: next_batch.to_string(),
events: services
.users
.get_to_device_events(sender_user, sender_device, Some(globalsince), Some(next_batch))
.get_to_device_events(sender_user, sender_device, None, Some(next_batch))
.map(at!(1))
.collect()
.await,
@@ -1541,112 +1160,30 @@ async fn collect_receipts(_services: &Services) -> sync_events::v5::response::Re
sync_events::v5::response::Receipts::default()
}
#[cfg(test)]
mod tests {
use std::slice;
fn filter_rooms<'a, Rooms>(
services: &'a Services,
filter: &'a [RoomTypeFilter],
negate: &'a bool,
rooms: Rooms,
) -> impl Stream<Item = &'a RoomId> + Send + 'a
where
Rooms: Stream<Item = &'a RoomId> + Send + 'a,
{
rooms.filter_map(async |room_id| {
let room_type = services.rooms.state_accessor.get_room_type(room_id).await;
use ruma::{owned_room_id, room_id};
if room_type.as_ref().is_err_and(|e| !e.is_not_found()) {
return None;
}
use super::*;
let room_type_filter = RoomTypeFilter::from(room_type.ok());
fn custom_room_type() -> RoomTypeFilter { RoomTypeFilter::from(Some("com.example")) }
let include = if *negate {
!filter.contains(&room_type_filter)
} else {
filter.is_empty() || filter.contains(&room_type_filter)
};
fn response() -> sync_events::v5::Response { sync_events::v5::Response::new("1".to_owned()) }
#[test]
fn absent_bool_filter_matches_either_value() {
assert!(matches_bool_filter(true, None));
assert!(matches_bool_filter(false, None));
}
#[test]
fn bool_filter_matches_only_its_own_value() {
assert!(matches_bool_filter(true, Some(true)));
assert!(!matches_bool_filter(false, Some(true)));
assert!(matches_bool_filter(false, Some(false)));
assert!(!matches_bool_filter(true, Some(false)));
}
#[test]
fn absent_room_type_filters_match_every_type() {
assert!(matches_room_type(&custom_room_type(), &[], &[]));
assert!(matches_room_type(&RoomTypeFilter::Default, &[], &[]));
}
#[test]
fn positive_room_type_matches_and_mismatches() {
let custom = custom_room_type();
assert!(matches_room_type(&custom, slice::from_ref(&custom), &[]));
assert!(!matches_room_type(&custom, &[RoomTypeFilter::Default], &[]));
}
#[test]
fn default_room_type_matches_untyped_rooms() {
assert!(matches_room_type(&RoomTypeFilter::Default, &[RoomTypeFilter::Default], &[]));
}
#[test]
fn negative_room_type_excludes_without_a_positive_filter() {
assert!(!matches_room_type(&RoomTypeFilter::Space, &[], &[RoomTypeFilter::Space]));
assert!(matches_room_type(&RoomTypeFilter::Default, &[], &[RoomTypeFilter::Space]));
}
#[test]
fn negative_room_type_overrides_positive() {
let custom = custom_room_type();
assert!(!matches_room_type(&custom, slice::from_ref(&custom), slice::from_ref(&custom)));
}
#[test]
fn fresh_response_is_empty() {
assert!(response_is_empty(&response()));
}
#[test]
fn account_data_is_not_empty() {
let mut response = response();
response
.extensions
.account_data
.rooms
.insert(owned_room_id!("!a:example.com"), vec![
Raw::from_json_string("{}".to_owned()).unwrap(),
]);
assert!(!response_is_empty(&response));
}
#[test]
fn a_room_with_no_events_is_still_empty() {
let mut response = response();
response
.rooms
.insert(owned_room_id!("!a:example.com"), sync_events::v5::response::Room::default());
assert!(response_is_empty(&response));
}
#[test]
fn a_receipt_makes_its_room_non_empty() {
let room_id = room_id!("!a:example.com");
let mut response = response();
response
.rooms
.insert(room_id.to_owned(), sync_events::v5::response::Room::default());
response
.extensions
.receipts
.rooms
.insert(room_id.to_owned(), Raw::from_json_string("{}".to_owned()).unwrap());
assert!(!response_is_empty(&response));
}
#[test]
fn to_device_without_events_is_empty() {
let mut response = response();
response.extensions.to_device = Some(sync_events::v5::response::ToDevice::default());
assert!(response_is_empty(&response));
}
include.then_some(room_id)
})
}
+4 -11
View File
@@ -21,19 +21,12 @@
/// Note: Unstable features are used while developing new features. Clients
/// should avoid using unstable features in their stable releases
pub(crate) async fn get_supported_versions_route(
State(services): State<crate::State>,
_body: Ruma<get_supported_versions::Request>,
) -> Result<get_supported_versions::Response> {
let mut unstable_features = unstable_features();
if services.config.allow_sticky_events {
// sticky events (https://github.com/matrix-org/matrix-spec-proposals/pull/4354)
unstable_features.insert("org.matrix.msc4354".to_owned(), true);
}
Ok(assign!(get_supported_versions::Response::new(versions()), {
unstable_features
}))
Ok(assign!(
get_supported_versions::Response::new(versions()),
{ unstable_features: unstable_features() }
))
}
/// # `GET /_continuwuity/server_version`
+154 -68
View File
@@ -3,7 +3,7 @@
use axum::extract::State;
use base64::{Engine as _, engine::general_purpose};
use conduwuit::{
Err, Error, EventTypeExt, PduEvent, Result, debug, err, error,
Err, Error, EventTypeExt, PduEvent, Result, debug, debug_warn, err, error,
matrix::{Event, StateKey},
result::FlatOk,
state_res, trace,
@@ -11,8 +11,8 @@
warn,
};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedUserId, ServerName,
UserId,
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, OwnedUserId,
ServerName, UserId,
api::{
error::{ErrorKind, IncompatibleRoomVersionErrorData},
federation::membership::{RawStrippedState, create_invite},
@@ -21,6 +21,7 @@
room_version_rules::RoomVersionRules,
};
use serde::Deserialize;
use serde_json::value::RawValue;
use crate::{Ruma, server::utils::validate_any_membership_event};
@@ -168,10 +169,14 @@ pub(crate) async fn create_invite_route(
// request, but we will get that during send_join anyway.
// This is safe to just add directly as an outlier as we already auth checked it
// during validation.
services
.rooms
.outlier
.add_pdu_outlier(&create_event_id, create_event_json);
if let Some(create_event_id) = create_event_id {
let mut create_event_json = create_event_json.clone();
create_event_json.insert("event_id".to_owned(), create_event_id.as_str().into());
services
.rooms
.outlier
.add_pdu_outlier(&create_event_id, &create_event_json);
}
services
.rooms
@@ -241,10 +246,10 @@ pub(crate) async fn create_invite_route(
/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#put_matrixfederationv2inviteroomideventid
async fn validate_invite_membership_event(
services: &crate::State,
body: &serde_json::value::RawValue,
body: &RawValue,
room_version_rules: &RoomVersionRules,
origin: &ServerName,
create_event_id: OwnedEventId,
create_event_id: Option<OwnedEventId>,
room_id: OwnedRoomId,
event_id: OwnedEventId,
) -> Result<(CanonicalJsonObject, OwnedUserId, OwnedUserId)> {
@@ -279,6 +284,9 @@ async fn validate_invite_membership_event(
Ok((pdu, sender_user, recipient_user))
}
type ValidatedInviteState =
(Option<OwnedEventId>, HashMap<(StateEventType, StateKey), CanonicalJsonObject>);
/// Validates the *invite state* of an invite request, per the steps listed
/// under the endpoint's [spec].
///
@@ -290,7 +298,8 @@ async fn validate_invite_state(
invite_state: &[RawStrippedState],
room_version_rules: &RoomVersionRules,
room_id: OwnedRoomId,
) -> Result<(OwnedEventId, HashMap<(StateEventType, StateKey), CanonicalJsonObject>)> {
) -> Result<ValidatedInviteState> {
let allow_stripped = services.config.enable_legacy_invite_support;
trace!(?invite_state, "Raw invite state");
let mut invite_state_map: HashMap<(StateEventType, StateKey), _> =
HashMap::with_capacity(invite_state.len());
@@ -299,49 +308,69 @@ async fn validate_invite_state(
for (idx, invite_state_event) in invite_state.iter().cloned().enumerate() {
trace!(%idx, ?invite_state_event, "Invite state event");
// Stripped state hasn't been sent over federation since v1.16.
let RawStrippedState::Pdu(raw_pdu) = invite_state_event else {
debug!(%idx, "Invite state event is not a PDU");
return Err!(Request(InvalidParam(
"PDU in invite state (index {idx}) violates the room event format"
)));
// we allow stripped state for compatibility with outdated servers if enabled.
#[allow(deprecated)]
let (raw_pdu, full) = match invite_state_event {
| RawStrippedState::Pdu(pdu) => (pdu, true),
| RawStrippedState::Stripped(event) if allow_stripped => {
warn!(index=%idx, "Event in incoming invite state is not a PDU and cannot be verified");
(
serde_json::value::to_raw_value(&event)
.expect("must be able to convert raw stripped state event to JSON value"),
false,
)
},
| RawStrippedState::Stripped(_) => {
debug_warn!(%idx, "Invite state event is not a PDU");
return Err!(Request(InvalidParam(
"PDU in invite state (index {idx}) violates the room event format"
)));
},
| _ =>
return Err!(Request(BadJson(
"PDU in invite state (index {idx}) is completely malformed"
))),
};
let (state_event_id, state_event_json) = if !allow_stripped || full {
validate_invite_state_pdu(services, &raw_pdu, &room_id, room_version_rules).await?
} else {
validate_legacy_invite_state_event(&raw_pdu, idx)?
};
let (state_event_room_id, state_event_id, state_event_json) = services
.rooms
.event_handler
.parse_incoming_pdu(&raw_pdu, Some(room_version_rules))
.await
.map_err(|e| {
err!(Request(InvalidParam(debug_warn!("Invalid PDU in invite state: {e}"))))
})?;
if state_event_room_id != room_id {
return Err!(Request(InvalidParam(debug_warn!(
%state_event_room_id,
%room_id,
"PDU in invite state ({state_event_id}) belongs to the wrong room"
))));
}
services
.server_keys
.verify_event(&state_event_json, room_version_rules)
.await
.map_err(|e| {
err!(Request(InvalidParam("Signature verification failed on invite event: {e}")))
})?;
let Some(state_key) = state_event_json.get("state_key").and_then(|k| k.as_str()) else {
return Err!(Request(InvalidParam(debug_info!(
"PDU in invite state ({state_event_id}) is not a state event"
"Event in invite state ({state_event_id}) is not a state event"
))));
};
let Some(event_type) = state_event_json.get("type").and_then(|k| k.as_str()) else {
return Err!(Request(InvalidParam(debug_warn!(
"PDU in invite state ({state_event_id}) is not an event?"
"Event in invite state ({state_event_id}) is not an event?"
))));
};
let key = StateEventType::from(event_type).with_state_key(state_key);
if key.0 == StateEventType::RoomCreate && key.1.is_empty() {
// Ensure this is a legal create event.
match PduEvent::from_id_val(&state_event_id, state_event_json.clone()) {
| Ok(pdu_event) => {
debug!("Validating discovered create event in invite room state");
create_event_id = Some(
validate_invite_create_event(&pdu_event, room_version_rules)
.await
.map(|_| state_event_id.clone())?,
)
},
| Err(e) => {
if !allow_stripped {
return Err!(Request(InvalidParam(
"Invalid create event in invite room state: {e:?}"
)));
}
warn!(error=?e, "Invalid create event in invite room state");
},
};
}
match invite_state_map.entry(key) {
| Entry::Occupied(entry) =>
return Err!(Request(InvalidParam(
@@ -349,41 +378,98 @@ async fn validate_invite_state(
entry.key(),
))),
| Entry::Vacant(entry) => {
if entry.key().0 == StateEventType::RoomCreate {
// Ensure this is a legal create event.
let pdu_event =
PduEvent::from_id_val(&state_event_id, state_event_json.clone())
.expect("must be able to create pdu event from event json");
debug!("Validating discovered create event in invite room state");
validate_invite_create_event(&pdu_event, room_version_rules).await?;
create_event_id = Some(state_event_id);
}
entry.insert(state_event_json);
},
}
}
let Some(create_event_id) = create_event_id else {
return Err!(Request(InvalidParam(debug_warn!(
parsed_state=?invite_state_map,
"Invite state does not contain the m.room.create event"
))));
};
invite_state_map.iter().try_for_each(|(key, event_json)| {
service::rooms::event_handler::Service::pdu_format_check_1(
event_json,
room_version_rules,
&create_event_id,
)
.map_err(|e| {
err!(Request(InvalidParam(
"PDU in invite state for {key:?} violates the room event format: {e}"
)))
})
})?;
format_check_state_map(
create_event_id.as_deref(),
&invite_state_map,
room_version_rules,
allow_stripped,
)?;
Ok((create_event_id, invite_state_map))
}
async fn validate_invite_state_pdu(
services: &crate::State,
raw_pdu: &RawValue,
room_id: &ruma::RoomId,
rules: &RoomVersionRules,
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
let (state_event_room_id, state_event_id, state_event_json) = services
.rooms
.event_handler
.parse_incoming_pdu(raw_pdu, Some(rules))
.await
.map_err(|e| {
err!(Request(InvalidParam(debug_warn!("Invalid PDU in invite state: {e}"))))
})?;
if state_event_room_id != room_id {
return Err!(Request(InvalidParam(debug_warn!(
%state_event_room_id,
%room_id,
"PDU in invite state ({state_event_id}) belongs to the wrong room"
))));
}
services
.server_keys
.verify_event(&state_event_json, rules)
.await
.map_err(|e| {
err!(Request(InvalidParam("Signature verification failed on invite event: {e}")))
})?;
Ok((state_event_id, state_event_json))
}
fn validate_legacy_invite_state_event(
event: &RawValue,
idx: usize,
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
let pdu = serde_json::from_str::<CanonicalJsonObject>(event.get()).map_err(|e| {
err!(BadServerResponse(debug_warn!("Error parsing incoming event {e:?}")))
})?;
Ok((EventId::parse(format!("$stripped_invite_state_{idx}"))?, pdu))
}
fn format_check_state_map(
create_event_id: Option<&EventId>,
invite_state_map: &HashMap<(StateEventType, StateKey), CanonicalJsonObject>,
room_version_rules: &RoomVersionRules,
allow_stripped: bool,
) -> Result<()> {
let Some(create_event_id) = create_event_id else {
if !allow_stripped {
return Err!(Request(InvalidParam(debug_warn!(
parsed_state=?invite_state_map,
"Invite state does not contain a valid m.room.create event"
))));
}
warn!("Not validating final invite state map, no valid create event was included.");
return Ok(());
};
invite_state_map.iter().try_for_each(|(key, event_json)| {
if event_json.get("signatures").is_none() && allow_stripped {
warn!(state_key=?key, "Skipping validation of nonconformant invite state event");
Ok(())
} else {
service::rooms::event_handler::Service::pdu_format_check_1(
event_json,
room_version_rules,
create_event_id,
)
.map_err(|e| {
err!(Request(InvalidParam(
"PDU in invite state for {key:?} violates the room event format: {e}"
)))
})
}
})
}
#[derive(Deserialize)]
struct MFederate {
#[serde(rename = "m.federate")]
+1 -1
View File
@@ -252,7 +252,7 @@ pub(crate) async fn create_join_event_v2_route(
&services,
&body.pdu,
&room_version_rules,
create_event.event_id.clone(),
Some(create_event.event_id.clone()),
body.room_id.clone(),
body.event_id.clone(),
)
+1 -1
View File
@@ -67,7 +67,7 @@ pub(crate) async fn create_knock_event_v1_route(
&services,
&body.pdu,
&room_version_rules,
create_event.event_id().to_owned(),
Some(create_event.event_id().to_owned()),
body.room_id.clone(),
body.event_id.clone(),
)
+1 -1
View File
@@ -53,7 +53,7 @@ pub(crate) async fn create_leave_event_v2_route(
&services,
&body.pdu,
&room_version_rules,
create_event.event_id.clone(),
Some(create_event.event_id.clone()),
body.room_id.clone(),
body.event_id.clone(),
)
+24 -11
View File
@@ -80,6 +80,12 @@ pub(super) async fn assert(&self) -> Result {
/// 6. `state_key` field presence (and parsing)
/// 7. PDU room format check (PDU check 1)
///
/// ## Compatibility
///
/// If `create_event_id` is `None`, step 7 is not performed. This should only be
/// the case for the invite route where servers still send stripped events on
/// occasion, and as such the create event may not be included in the state.
///
/// ## Returns
///
/// A resulting tuple of (PDU JSON, target membership state, sender, recipient).
@@ -87,7 +93,7 @@ pub(crate) async fn validate_any_membership_event(
services: &crate::State,
body: &serde_json::value::RawValue,
room_version_rules: &RoomVersionRules,
create_event_id: OwnedEventId,
create_event_id: Option<OwnedEventId>,
expected_room_id: OwnedRoomId,
expected_event_id: OwnedEventId,
) -> Result<(CanonicalJsonObject, MembershipState, OwnedUserId, OwnedUserId)> {
@@ -171,6 +177,11 @@ pub(crate) async fn validate_any_membership_event(
.ok_or_else(|| err!(Request(BadJson("Event is not a string"))))?
.to_owned();
assert!(
create_event_id.is_some() || membership == "invite",
"non-invite membership check had no associated create event"
);
let sender_user = pdu
.get("sender")
.and_then(|v| v.as_str())
@@ -184,16 +195,18 @@ pub(crate) async fn validate_any_membership_event(
.and_then(Result::ok)
.ok_or_else(|| err!(Request(InvalidParam("Invalid state_key property"))))?;
// Do a quick format check. The spec doesn't suggest this, but it's probably
// a good idea nonetheless.
service::rooms::event_handler::Service::pdu_format_check_1(
&pdu,
room_version_rules,
&create_event_id,
)
.map_err(|e| {
err!(Request(InvalidParam("Membership event violates the room event format: {e}")))
})?;
if let Some(create_event_id) = create_event_id {
// Do a quick format check. The spec doesn't suggest this, but it's probably
// a good idea nonetheless.
service::rooms::event_handler::Service::pdu_format_check_1(
&pdu,
room_version_rules,
&create_event_id,
)
.map_err(|e| {
err!(Request(InvalidParam("Membership event violates the room event format: {e}")))
})?;
}
Ok((pdu, membership.into(), sender_user, recipient_user))
}
+10 -9
View File
@@ -1526,14 +1526,6 @@ pub struct Config {
#[serde(default = "true_fn")]
pub allow_incoming_typing: bool,
/// Allow the use of unstable persistent "sticky events" ([MSC4354])
///
/// MSC4354: https://c10y.cc/MSC4354
///
/// default: false
#[serde(default)]
pub allow_sticky_events: bool,
/// Maximum time federation user can indicate typing.
///
/// default: 30
@@ -1731,6 +1723,15 @@ pub struct Config {
#[serde(default)]
pub send_messages_from_ignored_users_to_client: bool,
/// Disables strict validation on incoming federated invites. This weakens
/// your server's security - only enable it if you encounter issues
/// receiving invites from outdated servers.
///
/// Validation will still be performed, but it will log a warning instead of
/// causing an error.
#[serde(default)]
pub enable_legacy_invite_support: bool,
/// Send "org.matrix.dummy_event" events to the client. This is a debugging
/// option.
#[serde(default)]
@@ -1854,7 +1855,7 @@ pub struct Config {
/// User agent that is used specifically when fetching url previews.
///
/// default: "continuwuity/<version> (embedbot; facebookexternalhit/1.1; +https://continuwuity.org)"
/// default: "continuwuity/<version> (bot; +https://continuwuity.org)"
pub url_preview_user_agent: Option<String>,
/// Determines whether audio and video files will be downloaded for URL
+1 -3
View File
@@ -88,9 +88,7 @@ pub enum Error {
#[error(transparent)]
YamlSer(#[from] serde_saphyr::ser_error::Error),
#[error(transparent)]
ResolveServer(#[from] resolvematrix::error::ServerResolverBuilderError),
#[error(transparent)]
ServerResolutionError(#[from] resolvematrix::error::ServerResolutionError),
ResolveServer(#[from] resolvematrix::error::ResolveServerError),
// ruma/conduwuit
#[error("Arithmetic operation failed: {0}")]
-4
View File
@@ -181,10 +181,6 @@ fn as_mut_pdu(&mut self) -> &mut Pdu { unimplemented!("not a mutable Pdu") }
/// The state key for this event.
fn state_key(&self) -> Option<&str>;
/// The MSC4354 sticky object, verbatim, if the event has one.
#[inline]
fn sticky(&self) -> Option<&RawJsonValue> { None }
/// The event type.
fn kind(&self) -> &TimelineEventType;
-10
View File
@@ -9,7 +9,6 @@
use serde_json::json;
use super::{Event, redact};
use crate::matrix::pdu::sticky;
pub struct Owned<E: Event>(pub(super) E);
@@ -37,9 +36,6 @@ fn from(event: Ref<'a, E>) -> Self {
if let Some(state_key) = event.state_key() {
json["state_key"] = json!(state_key);
}
if let Some(sticky_object) = event.sticky() {
json[sticky::PDU_KEY] = json!(sticky_object);
}
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
@@ -71,9 +67,6 @@ fn from(event: Ref<'a, E>) -> Self {
if let Some(state_key) = event.state_key() {
json["state_key"] = json!(state_key);
}
if let Some(sticky_object) = event.sticky() {
json[sticky::PDU_KEY] = json!(sticky_object);
}
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
@@ -105,9 +98,6 @@ fn from(event: Ref<'a, E>) -> Self {
if let Some(state_key) = event.state_key() {
json["state_key"] = json!(state_key);
}
if let Some(sticky_object) = event.sticky() {
json[sticky::PDU_KEY] = json!(sticky_object);
}
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
+4 -10
View File
@@ -1,7 +1,4 @@
use ruma::{
CanonicalJsonObject, EventId, OwnedEventId,
room_version_rules::{EventIdFormatVersion, RoomVersionRules},
};
use ruma::{CanonicalJsonObject, OwnedEventId, room_version_rules::RoomVersionRules};
use serde_json::value::RawValue as RawJsonValue;
use crate::{Result, err};
@@ -27,11 +24,8 @@ pub fn gen_event_id(
value: &CanonicalJsonObject,
room_version_rules: &RoomVersionRules,
) -> Result<OwnedEventId> {
assert_ne!(
room_version_rules.event_id_format,
EventIdFormatVersion::V1,
"Continuwuity does not support PDU v1"
);
let reference_hash = ruma::signatures::reference_hash(value, room_version_rules)?;
Ok(EventId::new_v2_or_v3(&reference_hash)?)
let event_id: OwnedEventId = format!("${reference_hash}").try_into()?;
Ok(event_id)
}
-12
View File
@@ -3,7 +3,6 @@
mod partial;
mod raw_id;
mod redact;
pub mod sticky;
#[cfg(test)]
mod tests;
mod unsigned;
@@ -47,11 +46,6 @@ pub struct Pdu {
pub content: Box<RawJsonValue>,
/// MSC4354 sticky object, kept verbatim because it is signed. Interpret it
/// with [`sticky`], never by rewriting it.
#[serde(rename = "msc4354_sticky", default, skip_serializing_if = "Option::is_none")]
pub sticky: Option<Box<RawJsonValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state_key: Option<StateKey>,
@@ -143,9 +137,6 @@ fn sender(&self) -> &UserId { &self.sender }
#[inline]
fn state_key(&self) -> Option<&str> { self.state_key.as_deref() }
#[inline]
fn sticky(&self) -> Option<&RawJsonValue> { self.sticky.as_deref() }
#[inline]
fn kind(&self) -> &TimelineEventType { &self.kind }
@@ -217,9 +208,6 @@ fn sender(&self) -> &UserId { &self.sender }
#[inline]
fn state_key(&self) -> Option<&str> { self.state_key.as_deref() }
#[inline]
fn sticky(&self) -> Option<&RawJsonValue> { self.sticky.as_deref() }
#[inline]
fn kind(&self) -> &TimelineEventType { &self.kind }
-3
View File
@@ -20,8 +20,6 @@ pub struct PartialPdu {
pub content: Box<RawJsonValue>,
pub sticky: Option<Box<RawJsonValue>>,
pub unsigned: Option<Unsigned>,
pub state_key: Option<StateKey>,
@@ -68,7 +66,6 @@ fn default() -> Self {
Self {
event_type: "m.room.message".into(),
content: to_raw_value(&json!({})).unwrap(),
sticky: None,
unsigned: None,
state_key: None,
redacts: None,
-3
View File
@@ -11,9 +11,6 @@ pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) ->
self.unsigned = None;
// a redacted sticky event is just a normal event (MSC4354)
self.sticky = None;
let mut content = serde_json::from_str(self.content.get()).map_err(|e| {
err!(Request(BadJson("Failed to deserialize content into type: {e}")))
})?;
-155
View File
@@ -1,155 +0,0 @@
//! Sticky event helpers, as defined by [MSC4354].
//!
//! [MSC4354]: https://github.com/matrix-org/matrix-spec-proposals/pull/4354
use ruma::{UInt, events::sticky::StickyDurationMs};
use serde::Deserialize;
use serde_json::{
json,
value::{RawValue as RawJsonValue, to_raw_value},
};
/// Name of the sticky object at the top level of a PDU. Unstable prefix.
pub const PDU_KEY: &str = "msc4354_sticky";
/// Name of the expiry hint injected into `unsigned`. Unstable prefix.
pub const TTL_UNSIGNED_KEY: &str = "msc4354_sticky_duration_ttl_ms";
/// The longest an event may remain sticky (one hour).
#[allow(clippy::as_conversions)]
pub const MAX_DURATION_MS: u64 = StickyDurationMs::MAX as u64;
#[derive(Deserialize)]
struct Object {
duration_ms: u64,
}
/// Build the sticky object for an event we are creating locally.
#[must_use]
pub fn object(duration_ms: StickyDurationMs) -> Box<RawJsonValue> {
to_raw_value(&json!({ "duration_ms": duration_ms }))
.expect("a StickyDurationMs always produces a valid sticky object")
}
/// The sticky duration of `sticky`, clamped to [`MAX_DURATION_MS`], or `None`
/// if this is not a valid sticky object.
///
/// A malformed object leaves the event unsticky rather than invalidating it, so
/// a peer more lenient than us cannot split the room DAG.
#[must_use]
pub fn duration_ms(sticky: &RawJsonValue) -> Option<u64> {
serde_json::from_str::<Object>(sticky.get())
.ok()
.map(|sticky| sticky.duration_ms.min(MAX_DURATION_MS))
}
/// The time at which an event stops being sticky, in milliseconds since the
/// unix epoch, or `None` if the event was never sticky.
///
/// MSC4354 starts the clock at `min(received_ts, origin_server_ts)` so a server
/// cannot extend stickiness by dating its events forward. We keep no receive
/// time per PDU, so instead we deny stickiness to events dated further ahead
/// than any event could still be sticky for.
#[must_use]
pub fn expires_at(origin_server_ts: UInt, sticky: &RawJsonValue, now: u64) -> Option<u64> {
let origin_server_ts = u64::from(origin_server_ts);
if origin_server_ts.saturating_sub(now) > MAX_DURATION_MS {
return None;
}
let start = origin_server_ts.min(now);
duration_ms(sticky).map(|duration_ms| start.saturating_add(duration_ms))
}
#[must_use]
pub fn is_sticky(origin_server_ts: UInt, sticky: &RawJsonValue, now: u64) -> bool {
expires_at(origin_server_ts, sticky, now).is_some_and(|expires_at| expires_at > now)
}
#[cfg(test)]
mod tests {
use ruma::{UInt, events::sticky::StickyDurationMs};
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
use super::{MAX_DURATION_MS, duration_ms, expires_at, is_sticky, object};
fn raw(json: &serde_json::Value) -> Box<RawJsonValue> { to_raw_value(json).unwrap() }
fn ts(millis: u64) -> UInt { UInt::try_from(millis).unwrap() }
#[test]
fn duration_of_valid_object() {
let sticky = raw(&serde_json::json!({ "duration_ms": 300_000 }));
assert_eq!(duration_ms(&sticky), Some(300_000));
}
#[test]
fn duration_is_clamped_to_one_hour() {
let sticky = raw(&serde_json::json!({ "duration_ms": 9_999_999_999_u64 }));
assert_eq!(duration_ms(&sticky), Some(MAX_DURATION_MS));
}
#[test]
fn malformed_objects_are_not_sticky() {
for malformed in [
serde_json::json!({}),
serde_json::json!({ "duration_ms": -1 }),
serde_json::json!({ "duration_ms": "300000" }),
serde_json::json!({ "duration_ms": 3000.5 }),
serde_json::json!("nonsense"),
serde_json::json!(null),
] {
let sticky = raw(&malformed);
assert_eq!(duration_ms(&sticky), None, "{malformed} should not be sticky");
}
}
#[test]
fn object_round_trips_through_duration_ms() {
let sticky = object(StickyDurationMs::new_clamped(600_000_u32));
assert_eq!(duration_ms(&sticky), Some(600_000));
}
#[test]
fn expiry_is_origin_server_ts_plus_duration() {
let sticky = raw(&serde_json::json!({ "duration_ms": 300_000 }));
assert_eq!(expires_at(ts(1_000_000), &sticky, 1_100_000), Some(1_300_000));
}
#[test]
fn expiry_uses_the_clamped_duration() {
let sticky = raw(&serde_json::json!({ "duration_ms": 9_999_999_999_u64 }));
assert_eq!(
expires_at(ts(1_000_000), &sticky, 1_000_000),
Some(1_000_000 + MAX_DURATION_MS)
);
}
#[test]
fn timestamps_far_in_the_future_are_not_sticky() {
let sticky = raw(&serde_json::json!({ "duration_ms": 300_000 }));
let now = 1_000_000;
// a little ahead of us is tolerated, and starts the clock at `now`
let slightly_ahead = ts(now + MAX_DURATION_MS);
assert_eq!(expires_at(slightly_ahead, &sticky, now), Some(now + 300_000));
// beyond the point any honest event could still be sticky, it is not
let far_ahead = ts(now + MAX_DURATION_MS + 1);
assert_eq!(expires_at(far_ahead, &sticky, now), None);
}
#[test]
fn stickiness_ends_at_the_expiry() {
let sticky = raw(&serde_json::json!({ "duration_ms": 300_000 }));
assert!(is_sticky(ts(1_000_000), &sticky, 1_299_999));
assert!(!is_sticky(ts(1_000_000), &sticky, 1_300_000));
assert!(!is_sticky(ts(1_000_000), &sticky, 1_300_001));
}
#[test]
fn zero_duration_is_never_sticky() {
let sticky = raw(&serde_json::json!({ "duration_ms": 0 }));
assert!(!is_sticky(ts(1_000_000), &sticky, 1_000_000));
}
}
-93
View File
@@ -15,96 +15,3 @@ fn normal_parse() {
assert!(!backfilled, "backfilled variant");
}
fn sticky_pdu(sticky: &serde_json::Value) -> super::Pdu {
serde_json::from_value(serde_json::json!({
"event_id": "$test:example.com",
"room_id": "!test:example.com",
"sender": "@test:example.com",
"origin_server_ts": 1_000_000,
"type": "m.room.message",
"content": { "msgtype": "m.text", "body": "test" },
"msc4354_sticky": sticky,
"prev_events": [],
"depth": 1,
"auth_events": [],
"hashes": { "sha256": "test_hash" },
}))
.expect("PDU is valid")
}
#[test]
fn sticky_object_round_trips_verbatim() {
// the object is signed, so serialization must not touch it
let pdu = sticky_pdu(&serde_json::json!({ "duration_ms": 300_000, "unknown": "key" }));
let value = serde_json::to_value(&pdu).expect("PDU serializes");
assert_eq!(
value["msc4354_sticky"],
serde_json::json!({ "duration_ms": 300_000, "unknown": "key" })
);
}
#[test]
fn out_of_range_sticky_object_is_clamped_not_rejected() {
let pdu = sticky_pdu(&serde_json::json!({ "duration_ms": 9_999_999_999_u64 }));
let sticky = pdu.sticky.as_deref().expect("sticky object is kept");
assert_eq!(super::sticky::duration_ms(sticky), Some(super::sticky::MAX_DURATION_MS));
}
#[test]
fn nonsense_sticky_object_does_not_reject_the_pdu() {
let pdu = sticky_pdu(&serde_json::json!("nonsense"));
let sticky = pdu.sticky.as_deref().expect("sticky object is kept");
assert_eq!(super::sticky::duration_ms(sticky), None);
}
#[test]
fn redaction_removes_stickiness() {
let mut pdu = sticky_pdu(&serde_json::json!({ "duration_ms": 300_000 }));
pdu.redact(&ruma::RoomVersionId::V11, serde_json::json!({}))
.expect("redaction succeeds");
assert!(pdu.sticky.is_none());
}
#[test]
fn sticky_ttl_is_added_to_unsigned() {
let mut pdu = sticky_pdu(&serde_json::json!({ "duration_ms": 300_000 }));
pdu.origin_server_ts = ruma::MilliSecondsSinceUnixEpoch::now().get();
pdu.add_sticky_duration_ttl().expect("ttl is added");
let unsigned: serde_json::Value =
serde_json::from_str(pdu.unsigned.as_deref().expect("unsigned is set").get())
.expect("unsigned is valid");
let ttl = unsigned["msc4354_sticky_duration_ttl_ms"]
.as_u64()
.expect("ttl is a number");
assert!(ttl > 0 && ttl <= 300_000, "ttl {ttl} out of range");
}
#[test]
fn no_ttl_for_events_that_are_not_sticky() {
let mut pdu = sticky_pdu(&serde_json::json!({ "duration_ms": "nonsense" }));
pdu.add_sticky_duration_ttl().expect("ttl is skipped");
assert!(pdu.unsigned.is_none());
}
#[test]
fn set_unsigned_adds_the_sticky_ttl() {
// sticky events in the timeline get the hint through here, not the sticky
// section
let mut pdu = sticky_pdu(&serde_json::json!({ "duration_ms": 300_000 }));
pdu.origin_server_ts = ruma::MilliSecondsSinceUnixEpoch::now().get();
pdu.set_unsigned(None);
let unsigned: serde_json::Value =
serde_json::from_str(pdu.unsigned.as_deref().expect("unsigned is set").get())
.expect("unsigned is valid");
assert!(unsigned["msc4354_sticky_duration_ttl_ms"].is_number());
}
+1 -27
View File
@@ -3,7 +3,7 @@
use ruma::MilliSecondsSinceUnixEpoch;
use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue, to_raw_value};
use super::{Pdu, sticky};
use super::Pdu;
use crate::{Result, err, result::LogErr};
impl Pdu {
@@ -17,7 +17,6 @@ pub fn set_unsigned(&mut self, user_id: Option<&ruma::UserId>) {
self.remove_transaction_id().log_err().ok();
}
self.add_age().log_err().ok();
self.add_sticky_duration_ttl().log_err().ok();
}
pub fn remove_transaction_id(&mut self) -> Result {
@@ -59,31 +58,6 @@ pub fn add_age(&mut self) -> Result {
Ok(())
}
pub fn add_sticky_duration_ttl(&mut self) -> Result {
use BTreeMap as Map;
let now = u64::from(MilliSecondsSinceUnixEpoch::now().get());
let Some(expires_at) = self
.sticky
.as_deref()
.and_then(|sticky| sticky::expires_at(self.origin_server_ts, sticky, now))
else {
return Ok(());
};
let mut unsigned: Map<&str, Box<RawJsonValue>> = self
.unsigned
.as_deref()
.map(RawJsonValue::get)
.map_or_else(|| Ok(Map::new()), serde_json::from_str)
.map_err(|e| err!(Database("Invalid unsigned in pdu event: {e}")))?;
unsigned.insert(sticky::TTL_UNSIGNED_KEY, to_raw_value(&expires_at.saturating_sub(now))?);
self.unsigned = Some(to_raw_value(&unsigned)?);
Ok(())
}
pub fn add_relation(&mut self, name: &str, pdu: Option<&Self>) -> Result {
use serde_json::Map;
-1
View File
@@ -414,7 +414,6 @@ fn to_pdu_event<S>(
content,
origin: None,
redacts: None,
sticky: None,
unsigned: None,
auth_events,
prev_events,
-2
View File
@@ -414,7 +414,6 @@ pub(crate) fn to_init_pdu_event(
content,
origin: None,
redacts: None,
sticky: None,
unsigned: None,
auth_events: vec![],
prev_events: vec![],
@@ -463,7 +462,6 @@ pub(crate) fn to_pdu_event<S>(
content,
origin: None,
redacts: None,
sticky: None,
unsigned: None,
auth_events,
prev_events,
+13 -12
View File
@@ -1,5 +1,6 @@
use std::{borrow::Cow, fmt::Debug, mem, time::Instant};
use bytes::Bytes;
use conduwuit::{
Err, Error, Result, debug, debug_error, debug_info, debug_warn, err, trace,
utils::response::LimitReadExt,
@@ -10,8 +11,7 @@
use ruma::{
ServerName,
api::{
EndpointError, IncomingResponseExt, OutgoingRequest, OutgoingRequestExt,
SupportedVersions,
EndpointError, IncomingResponse, OutgoingRequest, OutgoingRequestExt, SupportedVersions,
auth_scheme::{AuthScheme, NoAuthentication},
error::Error as RumaError,
federation::authentication::{ServerSignatures, ServerSignaturesInput},
@@ -232,12 +232,8 @@ async fn handle_response<T>(
let response =
into_http_response(dest, actual, method, url, response, size_limit).await?;
let (parts, body) = response.into_parts();
T::IncomingResponse::try_from_http_response(http::Response::from_parts(
parts,
body.as_ref(),
))
.map_err(|e| err!(BadServerResponse("Server returned bad 200 response: {e:?}")))
T::IncomingResponse::try_from_http_response(response)
.map_err(|e| err!(BadServerResponse("Server returned bad 200 response: {e:?}")))
}
}
@@ -248,7 +244,7 @@ async fn into_http_response(
url: &Url,
mut response: Response,
max_size: u64,
) -> Result<http::Response<Vec<u8>>> {
) -> Result<http::Response<Bytes>> {
let status = response.status();
trace!(
%status, %method,
@@ -271,15 +267,20 @@ async fn into_http_response(
trace!("Waiting for response body...");
let http_response = http_response_builder
.body(response.limit_read(max_size).await.unwrap_or_default())
.body(
response
.limit_read(max_size)
.await
.unwrap_or_default()
.into(),
)
.expect("reqwest body is valid http body");
debug!("Got {status:?} for {method} {url}");
if !status.is_success() {
let (parts, body) = http_response.into_parts();
return Err(Error::Federation(
dest.to_owned(),
RumaError::from_http_response(http::Response::from_parts(parts, body.as_ref())),
RumaError::from_http_response(http_response),
));
}
+4 -6
View File
@@ -13,7 +13,7 @@
use ruma::{
DeviceId, OwnedDeviceId, RoomId, UInt, UserId,
api::{
IncomingResponseExt, OutgoingRequest, OutgoingRequestExt,
IncomingResponse, OutgoingRequest, OutgoingRequestExt,
auth_scheme::NoAuthentication,
client::push::{Pusher, PusherKind, set_pusher},
path_builder::SinglePath,
@@ -266,12 +266,10 @@ pub async fn send_request<T>(&self, dest: &str, request: T) -> Result<T::Incomin
)));
}
let (parts, body) = http_response_builder
.body(body)
.expect("reqwest body is valid http body")
.into_parts();
let response = T::IncomingResponse::try_from_http_response(
http::Response::from_parts(parts, body.as_ref()),
http_response_builder
.body(body)
.expect("reqwest body is valid http body"),
);
response.map_err(|e| {
err!(BadServerResponse(warn!(
@@ -7,7 +7,7 @@
Err, Event, Result, debug, debug_error, debug_warn, defer, matrix::PartialPdu, trace,
utils::time::jitter,
};
use futures::{FutureExt, StreamExt, future::try_join4};
use futures::{FutureExt, StreamExt, future::try_join3};
use ruma::{CanonicalJsonValue, EventId, RoomId, ServerName, UserId};
use tokio::sync::mpsc;
@@ -69,13 +69,9 @@ pub async fn handle_incoming_pdu<'a>(
.and_then(|s| UserId::parse(s).ok())
.is_some_and(|u| self.services.globals.user_is_local(&u));
let (room_exists, is_disabled, is_resident, ()) = try_join4(
let (room_exists, is_disabled, ()) = try_join3(
self.services.metadata.exists(room_id).map(Ok),
self.services.metadata.is_disabled(room_id).map(Ok),
self.services
.state_cache
.server_in_room(self.services.globals.server_name(), room_id)
.map(Ok),
self.acl_check(origin, room_id),
)
.await
@@ -89,20 +85,12 @@ pub async fn handle_incoming_pdu<'a>(
)));
}
// If the room doesn't exist (we don't have the create event), there's nothing
// we can do.
if !room_exists {
return Err!(Request(NotFound("Room is unknown to this server")));
}
// If the room does exist, but we aren't a resident of it, we might be
// interested in an out-of-band membership (for example, an inviter rescinding
// their invite).
if !is_resident {
if is_interesting_member_event {
// TODO: handle interesting membership events where we aren't in
// the room
}
return Err!(Request(NotFound("This server does not have any members this room")));
return Err!(Request(NotFound("Room is unknown to this server")));
}
// Fetch create event
@@ -1,15 +1,13 @@
use std::str::FromStr;
use conduwuit::{
Err, Error, Result, err,
Err, Result, err,
matrix::event::{gen_event_id, gen_event_id_canonical_json},
};
use itertools::Itertools;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, RoomId,
RoomVersionId,
api::error::{ErrorKind, IncompatibleRoomVersionErrorData},
room_version_rules::RoomVersionRules,
RoomVersionId, room_version_rules::RoomVersionRules,
};
use serde_json::value::RawValue as RawJsonValue;
@@ -104,25 +102,14 @@ pub async fn parse_incoming_pdu(
let room_version_rules = match room_version_rules {
| Some(r) => r,
| None => {
let room_version = self
.services
.state
.get_room_version(&room_id)
.await
.map_err(|_| err!(Request(NotFound("no version associated with room ID"))))?;
if matches!(room_version, RoomVersionId::V1 | RoomVersionId::V2) {
return Err(Error::BadRequest(
ErrorKind::IncompatibleRoomVersion(
IncompatibleRoomVersionErrorData::new(room_version),
),
"Room version is too old.",
));
}
&room_version
.rules()
.expect("room version must be supported")
},
| None => &self
.services
.state
.get_room_version(&room_id)
.await
.unwrap_or(RoomVersionId::V1)
.rules()
.expect("room version must be supported"),
};
let (event_id, value) =
+2 -127
View File
@@ -1,6 +1,5 @@
use std::collections::HashMap;
use base64::Engine;
use conduwuit::{
Err, Event, EventTypeExt, PduEvent, Result, debug, debug::DebugInspect, debug_error,
debug_info, err, info, matrix::StateKey, state_res, trace,
@@ -8,43 +7,14 @@
use futures::future::ready;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, ServerName,
api::error::ErrorKind,
canonical_json::redact,
events::StateEventType,
room_version_rules::{EventIdFormatVersion, RoomVersionRules},
api::error::ErrorKind, canonical_json::redact, events::StateEventType,
room_version_rules::RoomVersionRules,
};
use crate::rooms::{
event_handler::parse_incoming_pdu::expect_event_id_array, timeline::pdu_fits,
};
/// Checks that the given event ID matches the expected format by attempting to
/// decode the base64 string.
fn check_event_id_format(
event_id: &EventId,
room_version_rules: &RoomVersionRules,
) -> Result<Vec<u8>> {
let event_id_without_sigil = event_id
.as_str()
.strip_prefix("$")
.expect("event ID must start with a $ sigil");
let b64_alphabet = match room_version_rules.event_id_format {
| EventIdFormatVersion::V2 => base64::alphabet::STANDARD,
| EventIdFormatVersion::V3 => base64::alphabet::URL_SAFE,
| _ => return Err!("Unsupported event ID Format"),
};
let b64_engine = base64::engine::GeneralPurpose::new(
&b64_alphabet,
base64::engine::general_purpose::NO_PAD,
);
b64_engine.decode(event_id_without_sigil).map_err(|e| {
err!(Request(InvalidParam(debug_error!(
error=?e,
"PDU references an invalid event ID: {event_id}"
))))
})
}
impl super::Service {
/// Checks that the PDU conforms to the PDU format (check 1). This is
/// already mostly done during deserialisation, so this function just checks
@@ -72,9 +42,6 @@ pub fn pdu_format_check_1(
if auth_events.len() > 10 {
return Err!(Request(BadJson("PDU has too many auth events")));
}
for auth_event_id in &auth_events {
check_event_id_format(auth_event_id, room_version_rules)?;
}
// The m.room.create event is the genesis event and has empty auth_events
// by definition, so it is exempt from the checks below requiring or
@@ -103,9 +70,6 @@ pub fn pdu_format_check_1(
if prev_events.len() > 20 {
return Err!(Request(BadJson("PDU has too many prev events")));
}
for prev_event_id in &prev_events {
check_event_id_format(prev_event_id, room_version_rules)?;
}
Ok(())
}
@@ -342,92 +306,3 @@ pub(super) async fn policy_server_check_7(
Ok(true)
}
}
#[cfg(test)]
mod tests {
use ruma::server_name;
use super::*;
#[test]
fn v1_event_id_always_errors() {
let v1_event_id = EventId::new_v1(server_name!("example.com"));
assert!(
check_event_id_format(&v1_event_id, &RoomVersionRules::V3).is_err(),
"V1 event ID should not be valid in room V3"
);
assert!(
check_event_id_format(&v1_event_id, &RoomVersionRules::V4).is_err(),
"V1 event ID should not be valid in room V4"
);
}
#[test]
fn v2_event_id_ok_in_room_v3_only() {
let v2_event_id = EventId::new_v2_or_v3("KtY/RFXNXYxprwSOypvTlZsbohReRw19qcPATZDda4E")
.expect("fixture event hash must be valid");
let decoded_bytes = check_event_id_format(&v2_event_id, &RoomVersionRules::V3)
.expect("V2 event ID should be valid in room V3");
assert_eq!(
decoded_bytes,
vec![
42, 214, 63, 68, 85, 205, 93, 140, 105, 175, 4, 142, 202, 155, 211, 149, 155, 27,
162, 20, 94, 71, 13, 125, 169, 195, 192, 77, 144, 221, 107, 129
],
"V2 event reference hash did not decode to expected bytes"
);
assert!(
check_event_id_format(&v2_event_id, &RoomVersionRules::V4).is_err(),
"V2 event ID should not be valid in room V4"
);
}
#[test]
fn v3_event_id_errors_in_room_v3() {
let v3_event_id = EventId::new_v2_or_v3("zsj67_pqjr5qqh5GMTXqxLM0FqjP5OLrvXO0PjwWe88")
.expect("fixture event hash must be valid");
// Since the urlsafe replacements aren't in the standard base64 alphabet, this
// simply errors instead of decoding to potentially incorrect bytes.
assert!(
check_event_id_format(&v3_event_id, &RoomVersionRules::V3).is_err(),
"V3 event ID should not be valid in room V3"
);
}
#[test]
fn v3_event_id_ok_in_room_v4_onward() {
let v3_event_id = EventId::new_v2_or_v3("zsj67_pqjr5qqh5GMTXqxLM0FqjP5OLrvXO0PjwWe88")
.expect("fixture event hash must be valid");
// Since the urlsafe replacements aren't in the standard base64 alphabet, this
// simply errors instead of decoding to potentially incorrect bytes.
let expected_bytes = vec![
206, 200, 250, 239, 250, 106, 142, 190, 106, 170, 30, 70, 49, 53, 234, 196, 179, 52,
22, 168, 207, 228, 226, 235, 189, 115, 180, 62, 60, 22, 123, 207,
];
assert_eq!(
check_event_id_format(&v3_event_id, &RoomVersionRules::V4)
.expect("V3 event should be valid room V4"),
expected_bytes,
"V3 event ID in room V4 did not decode to expected bytes"
);
// These versions didn't change the algorithm, but might as well test them
// anyway
assert_eq!(
check_event_id_format(&v3_event_id, &RoomVersionRules::V6)
.expect("V3 event should be valid room V6"),
expected_bytes,
"V3 event ID in room V6 did not decode to expected bytes"
);
assert_eq!(
check_event_id_format(&v3_event_id, &RoomVersionRules::V10)
.expect("V3 event should be valid room V10"),
expected_bytes,
"V3 event ID in room V10 did not decode to expected bytes"
);
assert_eq!(
check_event_id_format(&v3_event_id, &RoomVersionRules::V12)
.expect("V3 event should be valid room V12"),
expected_bytes,
"V3 event ID in room V12 did not decode to expected bytes"
);
}
}
@@ -104,24 +104,24 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu(
// Determine whether this PDU should be soft-failed.
// If the auth check failed, invariably yes. Otherwise, only if the user isn't
// allowed to redact the target event (if any).
let redaction_permitted =
if let Some(redacted_id) = incoming_pdu.redacts_id(&room_version_rules) {
self.services
let mut should_soft_fail =
match (passes_current_state, incoming_pdu.redacts_id(&room_version_rules)) {
| (false, _) => true,
| (true, None) => false,
| (true, Some(redact_id)) => self
.services
.state_accessor
.user_can_redact(&redacted_id, incoming_pdu.sender(), room_id, true)
.user_can_redact(&redact_id, incoming_pdu.sender(), room_id, true)
.await
.is_ok_and(is_true!())
} else {
true
.is_ok_and(is_true!()),
};
let mut should_soft_fail = !redaction_permitted || !passes_current_state;
if !should_soft_fail {
// Now we can perform check 7, which is ensuring the event passes policy server
// checks.
// We explicitly only do this if we aren't already going to soft-fail the event,
// since the policy server refusing this event also soft-fails it.
debug!("Checking policy server for event");
debug!(event_id = %incoming_pdu.event_id, "Checking policy server for event");
should_soft_fail = !self
.policy_server_check_7(&incoming_pdu, &mut val, &room_version_rules)
.await
@@ -153,12 +153,6 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu(
should_soft_fail = true;
}
}
} else {
debug!(
%redaction_permitted,
%passes_current_state,
"Intending to soft-fail event (skipping further PDU checks)"
);
}
// The PDU has now passed all checks! We can now promote it (or soft-fail it if
@@ -281,7 +281,6 @@ fn create_test_pdu(unsigned_content: Option<JsonValue>) -> PduEvent {
origin_server_ts: UInt::try_from(1_234_567_890_u64).unwrap(),
kind: TimelineEventType::RoomMessage,
content: to_raw_value(&json!({"msgtype": "m.text", "body": "test"})).unwrap(),
sticky: None,
state_key: None,
prev_events: vec![],
depth: UInt::from(1_u32),
@@ -446,7 +445,6 @@ fn create_test_event(
origin_server_ts: UInt::try_from(1_234_567_890_u64).unwrap(),
kind: event_type,
content: to_raw_value(&content).unwrap(),
sticky: None,
state_key: state_key.map(Into::into),
prev_events: vec![],
depth: UInt::from(1_u32),
+1 -4
View File
@@ -1,4 +1,4 @@
use conduwuit::{Err, Result, matrix::Event, pdu::PartialPdu, trace};
use conduwuit::{Err, Result, matrix::Event, pdu::PartialPdu};
use ruma::{
EventId, RoomId, UserId,
events::{
@@ -46,7 +46,6 @@ pub async fn user_can_redact(
let power_levels = self.get_room_power_levels(room_id).await;
if power_levels.user_can_redact_event_of_other(sender) {
trace!(%sender, "Sender is allowed to redact other users' events");
return Ok(true);
}
@@ -60,12 +59,10 @@ pub async fn user_can_redact(
},
| _ => false,
};
trace!(%is_own_event, "User can redact own event");
return Ok(is_own_event);
}
trace!("User is not permitted to redact their own event nor others' events");
Ok(false)
}
+6 -95
View File
@@ -1,7 +1,6 @@
use std::{
borrow::Borrow,
collections::{BTreeMap, HashMap, HashSet},
iter::once,
sync::Arc,
};
@@ -19,22 +18,17 @@
};
use conduwuit_core::{
err, error,
matrix::pdu::{PduCount, PduId, RawPduId, sticky},
result::LogErr,
utils::{self, stream::TryIgnore},
matrix::pdu::{PduCount, PduId, RawPduId},
utils::{self},
};
use futures::{FutureExt, StreamExt, TryFutureExt, pin_mut};
use futures::{FutureExt, StreamExt, TryFutureExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, OwnedServerName,
RoomId, RoomVersionId, ServerName, UserId,
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, RoomId,
RoomVersionId, UserId,
events::{
AnySyncTimelineEvent, GlobalAccountDataEventType, TimelineEventType,
push_rules::PushRulesEvent,
room::{
encrypted::Relation,
member::{MembershipState, RoomMemberEventContent},
redaction::RoomRedactionEventContent,
},
room::{encrypted::Relation, redaction::RoomRedactionEventContent},
},
push::{Action, Ruleset, Tweak},
room_version_rules::RoomVersionRules,
@@ -548,10 +542,6 @@ async fn handle_pdu_effects(
let target_user_id = UserId::parse(state_key)
.expect("This state_key was previously validated");
// must be checked before the membership update, which is what
// marks the server as being in the room
let joining_server = self.joining_server(pdu, &target_user_id, room_id).await;
// Update our membership info, we do this here incase a user is invited or
// knocked and immediately leaves we need the DB to record the invite or
// knock event for auth
@@ -559,11 +549,6 @@ async fn handle_pdu_effects(
.state_cache
.update_membership(room_id, &target_user_id, pdu, true)
.await?;
if let Some(server) = joining_server {
self.send_sticky_events_to_server(room_id, short_room_id, &server)
.await;
}
}
},
| TimelineEventType::RoomMessage => {
@@ -578,80 +563,6 @@ async fn handle_pdu_effects(
Ok(())
}
/// The server of `target_user_id`, if this membership event brings it into
/// the room for the first time.
async fn joining_server(
&self,
pdu: &PduEvent,
target_user_id: &UserId,
room_id: &RoomId,
) -> Option<OwnedServerName> {
let server = target_user_id.server_name();
if !self.services.config.allow_sticky_events
|| self.services.globals.server_is_ours(server)
{
return None;
}
let content: RoomMemberEventContent = pdu.get_content().ok()?;
if content.membership != MembershipState::Join {
return None;
}
let already_joined = self
.services
.state_cache
.server_in_room(server, room_id)
.await;
(!already_joined).then(|| server.to_owned())
}
/// MSC4354: a server which has just joined will not backfill far enough to
/// see our unexpired sticky events, so push them to it.
async fn send_sticky_events_to_server(
&self,
room_id: &RoomId,
short_room_id: ShortRoomId,
server: &ServerName,
) {
let now = utils::millis_since_unix_epoch();
let oldest_sticky_ts = now.saturating_sub(sticky::MAX_DURATION_MS);
let mut pdu_ids = Vec::new();
let pdus = self.pdus_rev(room_id, None).ignore_err();
pin_mut!(pdus);
while let Some((count, pdu)) = pdus.next().await {
if u64::from(pdu.origin_server_ts) < oldest_sticky_ts {
break;
}
let is_ours = self.services.globals.user_is_local(pdu.sender());
let is_sticky = pdu
.sticky
.as_deref()
.is_some_and(|sticky| sticky::is_sticky(pdu.origin_server_ts, sticky, now));
if is_ours && is_sticky {
pdu_ids.push(RawPduId::from(PduId {
shortroomid: short_room_id,
shorteventid: count,
}));
}
}
// oldest first, as MSC4354 asks for creation order
for pdu_id in pdu_ids.iter().rev() {
self.services
.sending
.send_pdu_servers(once(server.to_owned()).stream(), pdu_id)
.await
.log_err()
.ok();
}
}
/// Adds relation data to the incoming event and events it relates to.
async fn aggregate_relations(&self, pdu: &PduEvent, count2: Count) {
// CONCERN: If we receive events with a relation out-of-order, we never write
-2
View File
@@ -88,7 +88,6 @@ pub async fn create_event(
let PartialPdu {
event_type,
content,
sticky,
unsigned,
state_key,
redacts,
@@ -210,7 +209,6 @@ pub async fn create_event(
),
kind: event_type,
content,
sticky,
state_key,
prev_events,
depth,
+1 -13
View File
@@ -1,9 +1,6 @@
//! Helpers for submitting events with the right checks performed
use conduwuit::{
Err, Result, err,
matrix::pdu::{PartialPdu, sticky},
};
use conduwuit::{Err, Result, err, matrix::pdu::PartialPdu};
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId,
events::{
@@ -15,7 +12,6 @@
member::{MembershipState, RoomMemberEventContent},
server_acl::RoomServerAclEventContent,
},
sticky::StickyDurationMs,
},
serde::Raw,
};
@@ -32,7 +28,6 @@ pub async fn send_state_event_for_key(
event_type: &StateEventType,
content: &Raw<AnyStateEventContent>,
state_key: &str,
sticky_duration_ms: Option<StickyDurationMs>,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
) -> Result<OwnedEventId> {
let mut content: Raw<AnyStateEventContent> = content.clone();
@@ -47,13 +42,6 @@ pub async fn send_state_event_for_key(
PartialPdu {
event_type: event_type.to_string().into(),
content,
sticky: self
.services
.config
.allow_sticky_events
.then_some(sticky_duration_ms)
.flatten()
.map(sticky::object),
state_key: Some(state_key.into()),
timestamp,
..Default::default()
+23 -8
View File
@@ -1,7 +1,7 @@
use std::{collections::BTreeMap, sync::Arc};
use conduwuit::{
Result, Server, debug_info,
Result, Server, debug_info, trace,
utils::{self, IterStream},
};
use futures::StreamExt;
@@ -10,9 +10,9 @@
api::federation::transactions::edu::{Edu, TypingContent},
events::{SyncEphemeralRoomEvent, typing::TypingEventContent},
};
use tokio::sync::RwLock;
use tokio::sync::{RwLock, broadcast};
use crate::{Dep, globals, sending, sending::EduBuf, sync, users};
use crate::{Dep, globals, sending, sending::EduBuf, users};
pub struct Service {
server: Arc<Server>,
@@ -21,12 +21,12 @@ pub struct Service {
pub typing: RwLock<BTreeMap<OwnedRoomId, BTreeMap<OwnedUserId, u64>>>,
/// timestamp of the last change to typing users
pub last_typing_update: RwLock<BTreeMap<OwnedRoomId, u64>>,
pub typing_update_sender: broadcast::Sender<OwnedRoomId>,
}
struct Services {
globals: Dep<globals::Service>,
sending: Dep<sending::Service>,
sync: Dep<sync::Service>,
users: Dep<users::Service>,
}
@@ -37,11 +37,11 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
services: Services {
globals: args.depend::<globals::Service>("globals"),
sending: args.depend::<sending::Service>("sending"),
sync: args.depend::<sync::Service>("sync"),
users: args.depend::<users::Service>("users"),
},
typing: RwLock::new(BTreeMap::new()),
last_typing_update: RwLock::new(BTreeMap::new()),
typing_update_sender: broadcast::channel(100).0,
}))
}
@@ -71,7 +71,9 @@ pub async fn typing_add(
.await
.insert(room_id.to_owned(), self.services.globals.next_count()?);
self.services.sync.wake_all_joined(room_id).await;
if self.typing_update_sender.send(room_id.to_owned()).is_err() {
trace!("receiver found what it was looking for and is no longer interested");
}
// update federation
if self.services.globals.user_is_local(user_id) {
@@ -97,7 +99,9 @@ pub async fn typing_remove(&self, user_id: &UserId, room_id: &RoomId) -> Result<
.await
.insert(room_id.to_owned(), self.services.globals.next_count()?);
self.services.sync.wake_all_joined(room_id).await;
if self.typing_update_sender.send(room_id.to_owned()).is_err() {
trace!("receiver found what it was looking for and is no longer interested");
}
// update federation
if self.services.globals.user_is_local(user_id) {
@@ -107,6 +111,15 @@ pub async fn typing_remove(&self, user_id: &UserId, room_id: &RoomId) -> Result<
Ok(())
}
pub async fn wait_for_update(&self, room_id: &RoomId) {
let mut receiver = self.typing_update_sender.subscribe();
while let Ok(next) = receiver.recv().await {
if next == room_id {
break;
}
}
}
/// Makes sure that typing events with old timestamps get removed.
async fn typings_maintain(&self, room_id: &RoomId) -> Result<()> {
let current_timestamp = utils::millis_since_unix_epoch();
@@ -139,7 +152,9 @@ async fn typings_maintain(&self, room_id: &RoomId) -> Result<()> {
.await
.insert(room_id.to_owned(), self.services.globals.next_count()?);
self.services.sync.wake_all_joined(room_id).await;
if self.typing_update_sender.send(room_id.to_owned()).is_err() {
trace!("receiver found what it was looking for and is no longer interested");
}
// update federation
for user in &removable {
+6 -9
View File
@@ -4,7 +4,7 @@
use conduwuit::{Err, Result, debug_info, err, utils::response::LimitReadExt};
use reqwest::Client;
use ruma::api::{
IncomingResponseExt, OutgoingRequest, OutgoingRequestExt,
IncomingResponse, OutgoingRequest, OutgoingRequestExt,
auth_scheme::{AppserviceToken, SendAccessToken},
path_builder::VersionHistory,
};
@@ -76,14 +76,11 @@ pub(crate) async fn send_antispam_request<T>(
};
}
let (parts, body) = http_response_builder
.body(body)
.expect("reqwest body is valid http body")
.into_parts();
let response = T::IncomingResponse::try_from_http_response(http::Response::from_parts(
parts,
body.as_ref(),
));
let response = T::IncomingResponse::try_from_http_response(
http_response_builder
.body(body)
.expect("reqwest body is valid http body"),
);
response.map_err(|e| {
err!(BadServerResponse(warn!(
+6 -9
View File
@@ -5,7 +5,7 @@
Err, Result, debug_error, err, trace, utils, utils::response::LimitReadExt, warn,
};
use ruma::api::{
IncomingResponseExt, OutgoingRequest, OutgoingRequestExt,
IncomingResponse, OutgoingRequest, OutgoingRequestExt,
appservice::Registration,
auth_scheme::{AccessToken, SendAccessToken},
path_builder::SinglePath,
@@ -97,14 +97,11 @@ pub async fn send_appservice_request<T>(
)));
}
let (parts, body) = http_response_builder
.body(body)
.expect("reqwest body is valid http body")
.into_parts();
let response = T::IncomingResponse::try_from_http_response(http::Response::from_parts(
parts,
body.as_ref(),
));
let response = T::IncomingResponse::try_from_http_response(
http_response_builder
.body(body)
.expect("reqwest body is valid http body"),
);
response.map(Some).map_err(|e| {
err!(BadServerResponse(warn!(
+6 -6
View File
@@ -441,10 +441,10 @@ fn num_senders(args: &crate::Args<'_>) -> usize {
max_senders = max_senders.min(num_cores);
}
let worker_count = args.server.config.sender_workers;
if worker_count == 0 {
max_senders
} else {
worker_count.clamp(MIN_SENDERS, max_senders)
}
// If the user doesn't override the default 0, this is intended to then default
// to 1 for now as multiple senders is experimental.
args.server
.config
.sender_workers
.clamp(MIN_SENDERS, max_senders)
}
+1 -28
View File
@@ -14,7 +14,6 @@
};
use conduwuit_core::{
Error, Event, Result, at, debug, err, error,
matrix::pdu::sticky,
result::LogErr,
utils::{
ReadyExt, calculate_hash, continue_exponential_backoff_secs,
@@ -269,10 +268,7 @@ async fn startup_netburst<'a>(
}
let entry = txns.entry(dest.clone()).or_default();
let over_budget =
self.server.config.startup_netburst_keep >= 0 && entry.len() >= keep;
if over_budget && !self.survives_netburst_trim(&event).await {
if self.server.config.startup_netburst_keep >= 0 && entry.len() >= keep {
warn!(
startup_netburst_keep = self.server.config.startup_netburst_keep,
queue_size = entry.len(),
@@ -293,29 +289,6 @@ async fn startup_netburst<'a>(
}
}
/// Whether this event skips the netburst budget, which MSC4354 asks of
/// still-sticky events. Only netburst delivers this queue, so with it
/// disabled there is nothing to keep the event for.
async fn survives_netburst_trim(&self, event: &SendingEvent) -> bool {
if !self.server.config.allow_sticky_events || !self.server.config.startup_netburst {
return false;
}
let SendingEvent::Pdu(pdu_id) = event else {
return false;
};
let Ok(pdu) = self.services.timeline.get_pdu_from_id(pdu_id).await else {
return false;
};
let now = MilliSecondsSinceUnixEpoch::now().get().into();
pdu.sticky
.as_deref()
.is_some_and(|sticky| sticky::is_sticky(pdu.origin_server_ts, sticky, now))
}
/// Selects any new events to send to the given destination.
#[tracing::instrument(
name = "select",
+20 -117
View File
@@ -1,5 +1,6 @@
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
pin::pin,
sync::Arc,
};
@@ -8,13 +9,13 @@
use ruma::{
OwnedDeviceId, OwnedRoomId, OwnedUserId, RoomId, UserId, api::client::sync::sync_events::v5,
};
use tokio::sync::{Mutex, watch};
use tokio::sync::{Mutex, Notify};
use crate::{Dep, rooms};
pub struct Service {
services: Services,
wakers: Wakers,
wakers: Mutex<HashMap<OwnedUserId, Arc<Notify>>>,
snake_connections: DbConnections<SnakeConnectionsKey, SnakeConnectionsVal>,
}
@@ -41,60 +42,16 @@ struct SnakeSyncCache {
type DbConnections<K, V> = SyncMutex<BTreeMap<K, V>>;
type DbConnectionsKey = (OwnedUserId, OwnedDeviceId, String);
pub type SnakeConnectionsKey = (OwnedUserId, OwnedDeviceId, Option<String>);
type SnakeConnectionsKey = (OwnedUserId, OwnedDeviceId, Option<String>);
type SnakeConnectionsVal = Arc<SyncMutex<SnakeSyncCache>>;
#[derive(Default)]
struct Wakers(Mutex<HashMap<OwnedUserId, watch::Sender<u64>>>);
impl Wakers {
async fn subscribe(&self, user: &UserId) -> watch::Receiver<u64> {
self.0
.lock()
.await
.entry(user.to_owned())
.or_insert_with(|| watch::channel(0).0)
.subscribe()
}
async fn wake(&self, user: &UserId) {
let mut wakers = self.0.lock().await;
wake_waker(&mut wakers, user);
}
async fn wake_all<I>(&self, users: I)
where
I: IntoIterator<Item = OwnedUserId> + Send,
{
let mut wakers = self.0.lock().await;
for user in users {
wake_waker(&mut wakers, &user);
}
}
}
/// Bumps the generation observed by anyone long-polling for this user, reaping
/// the entry once its last subscriber is gone.
fn wake_waker(wakers: &mut HashMap<OwnedUserId, watch::Sender<u64>>, user: &UserId) {
let Some(waker) = wakers.get(user) else {
return;
};
if waker.receiver_count() == 0 {
wakers.remove(user);
return;
}
waker.send_modify(|generation| *generation = generation.wrapping_add(1));
}
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services {
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
},
wakers: Wakers::default(),
wakers: Mutex::default(),
snake_connections: SyncMutex::new(BTreeMap::new()),
}))
}
@@ -103,8 +60,8 @@ fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
pub async fn subscribe_to_wake(&self, user: &UserId) -> watch::Receiver<u64> {
self.wakers.subscribe(user).await
pub async fn wait_for_wake(&self, user: &UserId) {
self.waker_for(user).await.notified().await;
}
/// Wake the target user's sync loop. Call this when something
@@ -117,20 +74,25 @@ pub async fn subscribe_to_wake(&self, user: &UserId) -> watch::Receiver<u64> {
pub async fn wake(&self, user: &UserId) {
trace!(?user, "Waking user's sync loops");
self.wakers.wake(user).await;
self.waker_for(user).await.notify_waiters();
}
/// Wake all of our users who are joined to the specified room.
pub async fn wake_all_joined(&self, room: &RoomId) {
trace!(?room, "Waking all joined users' sync loops");
let users: Vec<_> = self
.services
.state_cache
.active_local_users_in_room(room)
.collect()
.await;
let mut wakers = self.wakers.lock().await;
self.wakers.wake_all(users).await;
let mut users_in_room = pin!(self.services.state_cache.active_local_users_in_room(room));
while let Some(user) = users_in_room.next().await {
wakers.entry(user).or_default().notify_waiters();
}
}
async fn waker_for(&self, user: &UserId) -> Arc<Notify> {
let mut wakers = self.wakers.lock().await;
wakers.entry(user.to_owned()).or_default().clone()
}
pub fn snake_connection_cached(&self, key: &SnakeConnectionsKey) -> bool {
@@ -328,62 +290,3 @@ fn some_or_sticky<T>(target: &mut Option<T>, cached: Option<T>) {
*target = cached;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn wake_after_subscription_is_observable() {
let wakers = Wakers::default();
let user = UserId::parse("@alice:example.com").unwrap();
let mut receiver = wakers.subscribe(&user).await;
let generation = *receiver.borrow_and_update();
wakers.wake(&user).await;
wakers.wake(&user).await;
receiver.changed().await.unwrap();
assert_eq!(*receiver.borrow_and_update(), generation.wrapping_add(2));
}
#[tokio::test]
async fn wake_notifies_all_subscribers() {
let wakers = Wakers::default();
let user = UserId::parse("@alice:example.com").unwrap();
let mut first = wakers.subscribe(&user).await;
let mut second = wakers.subscribe(&user).await;
wakers.wake(&user).await;
first.changed().await.unwrap();
second.changed().await.unwrap();
}
#[tokio::test]
async fn waker_is_not_retained_without_subscribers() {
let wakers = Wakers::default();
let user = UserId::parse("@alice:example.com").unwrap();
wakers.wake(&user).await;
assert!(wakers.0.lock().await.is_empty());
drop(wakers.subscribe(&user).await);
assert_eq!(wakers.0.lock().await.len(), 1);
wakers.wake(&user).await;
assert!(wakers.0.lock().await.is_empty());
}
#[tokio::test]
async fn subscriber_still_woken_after_a_sibling_disconnects() {
let wakers = Wakers::default();
let user = UserId::parse("@alice:example.com").unwrap();
let mut receiver = wakers.subscribe(&user).await;
drop(wakers.subscribe(&user).await);
wakers.wake(&user).await;
receiver.changed().await.unwrap();
}
}
+1 -1
View File
@@ -114,7 +114,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
config: args.depend::<config::Service>("config"),
firstrun: args.depend::<firstrun::Service>("firstrun"),
globals: args.depend::<globals::Service>("globals"),
membership: args.depend::<membership::Service>("rooms::membership"),
membership: args.depend::<membership::Service>("membership"),
oauth: args.depend::<oauth::Service>("oauth"),
presence: args.depend::<presence::Service>("presence"),
state: args.depend::<rooms::state::Service>("rooms::state"),
+1 -1
View File
@@ -9,7 +9,7 @@ About server
<h1>About {{ server_name }}</h1>
{% if let Some(support_page) = support_page %}
<p>
Visit this server's website: <a href="{{ support_page }}" target="_blank">{{ support_page }}</a>
Visit this server's website: <a href="{{ support_page }} target="_blank">{{ support_page }}</a>
</p>
{% endif %}
{% if !contacts.is_empty() %}