Compare commits

..
101 Commits
Author SHA1 Message Date
Jason Volk 64a029ee09 Bump 1.4.6
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 19:34:51 +00:00
Jason Volk a3f6971579 docker: Add rust-sdk memcheck target.
docker: Separate and tweak cargo cache paths.

ci: Improve memcheck job targets.

cargo: Add missing debug syms to bench profile.

docker: Optimize layer copying.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 19:34:51 +00:00
Jason Volk 51681aec1b docker: Bump complement.
ci: Run complement with optimizations.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Jason Volk 39c84fabb4 Optimize filter fetch in syncv3 prologue.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Jason Volk f77bd41837 Add empty config default for rtc_transports to not generate false.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Jason Volk 6f34b8e9ca Fix missing pushrules evaluation on federation invite.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Jason Volk 5051da493a Fix pushrule evals relying on non-option RoomPowerLevels.
Add option to bypass pushrules to send everything to pushers.

Improve robustness of pushrule eval loops.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Jason Volk ff0e007c45 Fix readability of admin query account_data outputs.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-06 17:14:45 +00:00
Alexandru Scvorțov b85fb5ea6f Fix tests in nix and re-enable 2025-11-06 16:38:13 +00:00
dasha_uwuandJason Volk e905538269 Shuffle additional join servers. 2025-11-04 06:40:51 +00:00
Jason Volk 6f672b7304 Handle missing media dir as individual item errors w/o aborting check. (closes #213)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-04 06:40:51 +00:00
Jason Volk 4363ed6ec3 Adapt MSC4143 rtc_foci support to Ruma types. (e1f89b69ea)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-04 06:40:51 +00:00
Jason Volk dd50a4cb0b Fix ignored column descriptions; enhance preservation behavior. (7320d0a40b)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 23:02:42 +00:00
tototomate123andJason Volk e0a997c227 docs(reverse-proxy): restructure and add nginx guide
- Moved Caddy and Nginx configuration examples to separate files
- Added documentation for Nginx setup
- Updated generic reverse proxy section
2025-11-03 12:49:04 +00:00
tototomate123andJason Volk e1f89b69ea feat: Add Element Call / MatrixRTC support 2025-11-03 11:36:19 +00:00
dasha_uwuandJason Volk 888f72d8d0 Generalize common_prefix for AsStr inputs.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 11:36:19 +00:00
Jason Volk 06618eadab Re-establish federating with several Conduit endpoints.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 11:36:19 +00:00
Benedikt Vollmerhaus 05390d6097 Fix shared TURN secret from turn_secret_file not being used 2025-11-03 12:30:00 +01:00
Jason Volk 1f803fe3a9 Fix verification loss; workaround Nheko-Reborn/nheko#1908 (closes #146)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 04:45:31 +00:00
Jason Volk 1492d68e25 Additional tracing of request body.
Optimize router request parse types.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 04:45:31 +00:00
Jason Volk c1aa94fb91 Describe roomsynctoken_shortstatehash for deletion.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 02:05:35 +00:00
Jason Volk 7320d0a40b Improve config options for missing and dropping db columns.
Implement actual drop functionality.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-03 02:05:35 +00:00
Jason Volk abded2d442 Allow guests even when token required for normal registration. (closes #189)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 18:19:11 +00:00
Jason Volk 4afd6f347b Implement GET /_matrix/client/v3/events live room previews.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 08:06:32 +00:00
Jason Volk 6b8d6956a3 Fix timeline events order in /initialSync.
Implement acccount_data for response.

Ensure consistency of revealed events.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 08:06:32 +00:00
Jason Volk f59d62c01c Bump Ruma for CanonicalJson property name optimizations.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 03:57:11 +00:00
Jason Volk a14556da97 Implement encryption_enabled_by_default_for_room_type. (closes #29)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 00:02:49 +00:00
Jason Volk 8b1de3d8db Add update for device last_seen_ts. (closes #40)
Piggyback on presence ping for controlled device seen updates.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 00:02:49 +00:00
Jason Volk 240b498489 Add unwrap suite to future::OptionExt extensions.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 00:02:49 +00:00
Jason Volk d680a6ba53 Bump Ruma for device type optimizations.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-02 00:02:49 +00:00
Jason Volk aa3f14cd57 Optimize federation user devices response.
Modernize some error branches.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-01 23:15:37 +00:00
Jason Volk 15627bc8d0 Simplify device metadata update interface.
Add broadcast argument; reorder notify to after write.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-01 23:15:37 +00:00
Jason Volk 084facf474 Move user profile related functions to profile unit.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-11-01 23:15:37 +00:00
Jason Volk d24986edf1 sliding-sync: Stop propagation from room handlers.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-31 03:38:35 +00:00
Jason Volk ce1ac277a6 Refactor sync v3 leave handler.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-31 03:38:35 +00:00
Jason Volk 7aeed0a95a Fix next_batch pagination tokens in relations response.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-31 03:38:35 +00:00
Jason Volk 9265748a57 Remove legacy ruma return type on sync endpoint.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 16:28:27 +00:00
Jason Volk e85cfdf48a Remove errant inline. (48923b3657)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 14:01:57 +00:00
Jason Volk 48923b3657 Implement notifications retrieval. (closes #201)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk aedaf3f0c1 Fix pushing useless notifications.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk b24b59dc38 Add admin query raw command to clear table.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk 738b5e3fa5 Add attempted newtype struct name to deserialization error message.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk 1f0cfec5ca Use Result for last_notification_read as zero-default not appropriate.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk 9c5caa3a5f Move appservice request unit from sender to appservice.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk 1cf4a26ae9 docker: Use prefix match for nightly rust_toolchain settings.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-30 03:48:16 +00:00
Jason Volk 3694ffbab3 Bump Ruma w/ PushRule type optimizations.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-28 23:23:27 +00:00
Jason Volk af0e01e016 Add util to assist with Display/to_string for SmallString.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-28 23:23:27 +00:00
Jason Volk 5e89f0acae Split pusher service send/request into unit.
Refactor sender's push destination handler.

Combine remnants of service::rooms::user with pusher service.

Further split and reorg pusher service units.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-28 23:23:19 +00:00
Jason Volk 563873af77 Use signed PduCount for better bumpstamp on create and backfill.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 15:24:50 +00:00
Jason Volk d2072080c9 Add admin query for remote alias resolve.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 15:24:17 +00:00
Jason Volk e191730950 Tracing span adjustments for ret verbosity.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 15:15:12 +00:00
Jason Volk f660e00bb5 Implement Dehydrated Devices MSC3814 (closes #200)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 12:06:43 +00:00
Jason Volk f613d0c2ad Add admin command to delete a user's device.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 10:55:12 +00:00
Jason Volk 7596ad2019 Add rountripping test for database cbor serialization.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 10:41:49 +00:00
Jason Volk 384add9784 ci: Extract Nix artifacts.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:39:57 +00:00
Jason Volk a234f019b1 Advertise MSC2659 support; reorder features by MSC ascending.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk e8a87bdfa3 Remove DynamicUser from systemd files (closes #207).
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 6a4aff424f Add count value to the to_device iter item.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 8959d9e2c1 Add bulk one_time_keys adder to interface.
Add device_exists to interface.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk a30c043386 Support creating devices without access_tokens.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk a39ef994d2 Add is_err_or to Result utils.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk b714f24029 sliding-sync: Filter redacted events from bump calculation.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 2263f2e874 docker: Add dirty-runs option passed to complement.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 9abe9becd6 Split pusher/push-rule from timeline append.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 4b74c01895 Split appservice dissemination from timeline::append().
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 1223763e2b Split sliding-sync into additional units.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk 9b64c1f105 Bump Nix flake. 2025-10-27 09:06:02 +00:00
Jason Volk 212a8434a8 Bump Rust 1.89
We can't upgrade to 1.90 because NixOS breaks. 🙂‍↔️

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-27 09:06:02 +00:00
Jason Volk bfaac8b5a2 Bump dependencies.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 17:46:25 +00:00
Jason Volk 9af15ecbba Bump 1.4.5
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 13:33:32 +00:00
Jason Volk f66a83763e Persist sliding-sync state; mitigate initial-sync.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 13:33:32 +00:00
Jason Volk 718c3adcb2 ci: Expand the rust-sdk integration matrix.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 08:44:16 +00:00
Jason Volk af80482c04 Add branch in room handler for left rooms.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 08:34:49 +00:00
Jason Volk a20ddcd586 Recency bump on own membership event.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 08:08:26 +00:00
Jason Volk b483306367 Fix sliding-sync invite regression.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-25 07:52:40 +00:00
Jason Volk f5e98467be Improve sliding-sync robustness to deeper replays.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-24 18:31:48 +00:00
Jason Volk d44db45f83 Adjust the order of operations during read_marker handling.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-24 18:31:48 +00:00
Jason Volk 0397bb8237 Implement specified non-blocking semantic for sliding-sync.
Simplify sliding-sync watch loop and bounds.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-23 11:00:55 +00:00
Jason Volk 4010fc62bc Use take_while rather than filter for scanning last_receipt_count.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-23 03:37:52 +00:00
Jason Volk 0d823a2822 Revert "Involve m.fully_read for resetting notification counts."
This reverts commit 0df5e5e7ac.
2025-10-23 03:37:52 +00:00
Jason Volk 9cd175b125 Additional logging/tracing tweaks around sliding-sync.
Additional spans around receipt service interface.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-23 03:37:52 +00:00
Jason Volk 050a1a350a Add dbg profile to Cargo; default to faster builds accomodating new users. 2025-10-23 03:37:52 +00:00
Jason Volk ec0f872f8f Add tracing around db watcher notify including cf name.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 20:23:30 +00:00
Jason Volk 024e8eae62 Implement num_live for sliding-sync room response.
Avoid using origin_server_ts for recency stamp.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 09:14:41 +00:00
Jason Volk 4fd60b2605 Improve sliding-sync selector and windowing logic.
Bump Ruma; eliminate premature release compat.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 08:15:25 +00:00
Jason Volk e53968d9eb Improve tracing spans for notif counts and sliding-sync exts.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 07:05:03 +00:00
Jason Volk 7cbc2ee385 Fix the accidentally dual-keyspaced cf from conduit.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 07:03:10 +00:00
Jason Volk 0df5e5e7ac Involve m.fully_read for resetting notification counts.
EXA is not reliably removing its unread marker any other way. Clients
wishing to move the fully_read marker in the other direction should
file an issue to make themselves known.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-22 06:52:30 +00:00
Jason Volk 312eb69450 Add tool to get sibling connections for a device.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 17:48:08 +00:00
Jason Volk 5b620a2c37 Add .and_is() / .and_if() to the BoolExt smalltalk.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 17:41:42 +00:00
Jason Volk 3b4fbb8c1a Fix inverted debug assert message.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 15:48:05 +00:00
Jason Volk 158d44e1a9 docs: Remove line about other reverse proxies. (#197)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 15:02:36 +00:00
Jason Volk d2aab468cf Fixes for sliding-sync request replays.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 06:46:02 +00:00
Jason Volk aa4486dfdf Simplify types, mitigate expansion; eliminate unnecessary move.
Further simplify future::BoolExt toward type expansion mitigation.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-21 05:25:11 +00:00
Jason Volk 2a662445b6 Make account_data.last_count() upper bound argument optional.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-20 23:58:38 +00:00
Jason Volk 5b3f0fde23 Add invite rooms to sync v3 watch rooms list.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-20 23:57:52 +00:00
Jason Volk 9640afebff Join the async prologue operations in sync v3.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-20 23:55:42 +00:00
Jason Volk dd5c5c7a4a Notify watchers on delete operations in addition to inserts. 2025-10-20 20:45:38 +00:00
Jason Volk fc0f04defa Optimize watcher range collection on notify.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-20 20:44:45 +00:00
Jason Volk 7d8f7cbe5d Bump 1.4.4
Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-16 01:55:43 +00:00
Jason Volk 4e0249cd2f Fix sliding-sync again by resetting after interrupted requests. (fixes #190)
This solution is not elegant but appears to be correct for fixing the issue. A
a more efficient solution might be possible without resetting the connection
which should be further investigated.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-10-16 01:05:16 +00:00
137 changed files with 4789 additions and 3056 deletions
+11 -10
View File
@@ -20,13 +20,13 @@ jobs:
name: Init
runs-on: ${{matrix.runner}}
env:
default_cargo_profiles: '["test", "release"]'
default_cargo_profiles: '["test", "bench", "release"]'
default_feat_sets: '["none", "default", "all"]'
default_rust_toolchains: '["nightly", "stable"]'
default_sys_names: '["debian"]'
default_sys_versions: '["testing-slim"]'
default_rust_targets: '["x86_64-unknown-linux-gnu"]'
default_sys_targets: '["x86_64-v1-linux-gnu"]'
default_sys_targets: '["x86_64-v1-linux-gnu", "x86_64-v3-linux-gnu"]'
default_machines: '["X64"]'
outputs:
@@ -38,17 +38,17 @@ jobs:
sys_targets: ${{vars.SYS_TARGETS || env.default_sys_targets}}
sys_versions: ${{vars.SYS_VERSIONS || env.default_sys_versions}}
machines: ${{vars.MACHINES || env.default_machines}}
package: ${{vars.PACKAGE || !contains(github.ref, 'refs/pull/')}}
publish: ${{vars.PUBLISH || !contains(github.ref, 'refs/pull/')}}
build_nix: ${{vars.BUILD_NIX || !contains(github.ref, 'refs/pull/')}}
build_pkgs: ${{vars.BUILD_PKGS || github.ref == 'refs/heads/main' || contains(github.ref, 'tags/v')}}
package: ${{vars.PACKAGE != 'false'}}
publish: ${{vars.PUBLISH != 'false'}}
build_nix: ${{vars.BUILD_NIX != 'false'}}
build_pkgs: ${{vars.BUILD_PKGS || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/test' || contains(github.ref, 'tags/v')}}
check_pkgs: ${{vars.CHECK_PKGS || 'false'}}
complement: ${{vars.COMPLEMENT || 'true'}}
complement: ${{vars.COMPLEMENT != 'false'}}
complement_runner: 'het'
docker_repo: ${{vars.DOCKER_REPO}}
release_url: ${{steps.release.outputs.upload_url}}
pages_url: 'https://matrix-construct.github.io/tuwunel/'
rust_sdk_integration: ${{vars.RUST_SDK_INTEGRATION || 'true'}}
rust_sdk_integ: ${{vars.RUST_SDK_INTEGRATION != 'false'}}
head_msg: ${{github.event.head_commit.message || github.event.workflow_run.head_commit.message}}
strategy:
@@ -176,7 +176,7 @@ jobs:
with:
head_msg: ${{needs.init.outputs.head_msg}}
build_nix: ${{fromJSON(needs.init.outputs.build_nix)}}
rust_sdk_integration: ${{fromJSON(needs.init.outputs.rust_sdk_integration)}}
rust_sdk_integ: ${{fromJSON(needs.init.outputs.rust_sdk_integ)}}
complement: ${{fromJSON(needs.init.outputs.complement)}}
complement_runner: ${{needs.init.outputs.complement_runner}}
cargo_profiles: ${{needs.init.outputs.cargo_profiles}}
@@ -189,7 +189,6 @@ jobs:
machines: ${{needs.init.outputs.machines}}
excludes: >
[
{"feat_set": "logging"},
{"cargo_profile": "test", "rust_toolchain": "stable", "feat_set": "none"},
{"cargo_profile": "test", "rust_target": "aarch64-unknown-linux-gnu"},
{"cargo_profile": "release-debuginfo"},
@@ -223,6 +222,7 @@ jobs:
if: >
!failure() && !cancelled()
&& needs.init.outputs.package
&& !contains(github.ref, 'refs/pull')
&& !contains(needs.init.outputs.head_msg, '[ci only it]')
&& !contains(needs.init.outputs.head_msg, '[ci no build]')
&& !contains(needs.init.outputs.head_msg, '[ci no package]')
@@ -286,6 +286,7 @@ jobs:
if: >
!failure() && !cancelled()
&& needs.init.outputs.publish
&& !contains(github.ref, 'refs/pull')
&& !contains(needs.init.outputs.head_msg, '[ci only it]')
&& !contains(needs.init.outputs.head_msg, '[ci no build]')
&& !contains(needs.init.outputs.head_msg, '[ci no package]')
+3 -3
View File
@@ -181,10 +181,10 @@ jobs:
"mime": "application/x-rpm"
},
"nix": {
"dst": "tuwunel.drv.tar.zst",
"src": "/opt/tuwunel/tuwunel.drv",
"dst": "tuwunel.nix.tar.zst",
"src": "/opt/tuwunel.nix.tar",
"mime": "application/zstd",
"zstd": 6
"zstd": 11
}
}
+53 -38
View File
@@ -38,12 +38,9 @@ on:
complement:
type: boolean
default: true
complement_feat_sets:
type: string
default: '["all"]'
complement_runner:
type: string
rust_sdk_integration:
rust_sdk_integ:
type: boolean
default: true
build_nix:
@@ -84,10 +81,10 @@ jobs:
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
name: Unit
name: Module
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["unit", "integration"]'
bake_targets: '["unit", "integ"]'
cargo_profiles: '["test"]'
feat_sets: '["all"]'
rust_toolchains: ${{inputs.rust_toolchains}}
@@ -110,7 +107,7 @@ jobs:
name: Bench
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["unit", "integration"]'
bake_targets: '["unit", "integ"]'
cargo_profiles: '["bench"]'
feat_sets: '["all"]'
rust_toolchains: '["nightly"]'
@@ -122,7 +119,7 @@ jobs:
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
valgrind:
memcheck:
if: >
!contains(inputs.head_msg, '[ci only it]')
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["bench"]')[0])
@@ -133,8 +130,9 @@ jobs:
name: Memcheck
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["integration-valgrind"]'
cargo_profiles: '["bench"]'
#bake_targets: '["unit-valgrind", "integ-valgrind"]'
bake_targets: '["integ-valgrind"]'
cargo_profiles: '["bench"]' # use bench not release for debug syms
feat_sets: '["all"]'
rust_toolchains: '["nightly"]'
sys_names: ${{inputs.sys_names}}
@@ -142,8 +140,8 @@ jobs:
rust_targets: ${{inputs.rust_targets}}
sys_targets: '["x86_64-v3-linux-gnu"]'
machines: ${{inputs.machines}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
excludes: ${{inputs.excludes}}
smoke:
if: >
@@ -225,8 +223,9 @@ jobs:
!failure() && !cancelled()
&& !contains(inputs.head_msg, '[ci only it]')
&& !contains(inputs.head_msg, '[ci no build]')
&& !contains(github.ref, 'refs/pull')
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["release"]')[0])
&& contains(fromJSON(inputs.complement_feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["stable"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
&& fromJSON(inputs.build_nix)
@@ -246,61 +245,78 @@ jobs:
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
matrix-rust-sdk-integration:
rust-sdk-integ:
if: >
!failure() && !cancelled()
&& !contains(inputs.head_msg, '[ci no build]')
&& inputs.rust_sdk_integration
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.complement_feat_sets), fromJSON('["all"]')[0])
&& inputs.rust_sdk_integ
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
&& contains(fromJSON(inputs.rust_targets), fromJSON('["x86_64-unknown-linux-gnu"]')[0])
name: Matrix SDK Integration
needs: [smoke]
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["matrix-rust-sdk-integration"]'
cargo_profiles: '["test"]'
feat_sets: ${{inputs.complement_feat_sets}}
#bake_targets: '["rust-sdk-integ", "rust-sdk-valgrind"]'
bake_targets: '["rust-sdk-integ"]'
cargo_profiles: ${{inputs.cargo_profiles}}
feat_sets: '["all"]'
rust_toolchains: '["nightly"]'
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: '["x86_64-v1-linux-gnu"]'
machines: ${{inputs.machines}}
rust_targets: '["x86_64-unknown-linux-gnu"]'
sys_targets: ${{inputs.sys_targets}}
machines: '["X64"]'
runner: ${{inputs.complement_runner}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
artifact: >
{
"matrix-rust-sdk-integration": {
"rust-sdk-integ": {
"src": "/var/log/tuwunel.log",
"dst": "matrix-rust-sdk-integration.tuwunel.log",
"dst": "rust-sdk-integ.tuwunel.log",
},
"rust-sdk-valgrind": {
"src": "/var/log/tuwunel.log",
"dst": "rust-sdk-valgrind.tuwunel.log",
}
}
excludes: >
[
{"bake_target": "rust-sdk-valgrind", "cargo_profile": "test"},
{"feat_set": "none"},
{"feat_set": "logging"},
{"cargo_profile": "release"},
{"cargo_profile": "release-debuginfo"},
{"cargo_profile": "release-native"},
{"cargo_profile": "test", "sys_target": "x86_64-v2-linux-gnu"},
{"cargo_profile": "test", "sys_target": "x86_64-v3-linux-gnu"},
{"cargo_profile": "bench", "sys_target": "x86_64-v1-linux-gnu"},
{"cargo_profile": "bench", "sys_target": "x86_64-v2-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu"},
{"sys_target": "aarch64-v8-linux-gnu"},
]
complement:
if: >
!failure() && !cancelled()
&& !contains(inputs.head_msg, '[ci no build]')
&& inputs.complement
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.complement_feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["bench"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v3-linux-gnu"]')[0])
name: Complement
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["complement-tester", "complement-testee"]'
cargo_profiles: '["test"]'
feat_sets: ${{inputs.complement_feat_sets}}
cargo_profiles: '["bench"]'
feat_sets: '["logging"]'
rust_toolchains: '["nightly"]'
sys_names: ${{inputs.sys_names}}
sys_versions: ${{inputs.sys_versions}}
rust_targets: ${{inputs.rust_targets}}
sys_targets: '["x86_64-v1-linux-gnu"]'
sys_targets: '["x86_64-v3-linux-gnu"]'
machines: ${{inputs.machines}}
runner: ${{inputs.complement_runner}}
excludes: ${{inputs.excludes}}
@@ -312,10 +328,9 @@ jobs:
&& !contains(inputs.head_msg, '[ci no build]')
&& inputs.complement
&& inputs.machines
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["bench"]')[0])
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v3-linux-gnu"]')[0])
name: Matrix Compliance
needs: [complement, smoke]
@@ -327,13 +342,13 @@ jobs:
strategy:
fail-fast: false
matrix:
cargo_profile: ${{fromJSON('["test"]')}}
feat_set: ${{fromJSON(inputs.complement_feat_sets)}}
cargo_profile: ${{fromJSON('["bench"]')}}
feat_set: ${{fromJSON('["logging"]')}}
rust_toolchain: ${{fromJSON('["nightly"]')}}
sys_name: ${{fromJSON(inputs.sys_names)}}
sys_version: ${{fromJSON(inputs.sys_versions)}}
rust_target: ${{fromJSON(inputs.rust_targets)}}
sys_target: ${{fromJSON('["x86_64-v1-linux-gnu"]')}}
sys_target: ${{fromJSON('["x86_64-v3-linux-gnu"]')}}
machine: ${{fromJSON(inputs.machines)}}
exclude: ${{fromJSON(inputs.excludes)}}
include: ${{fromJSON(inputs.includes)}}
Generated
+321 -471
View File
File diff suppressed because it is too large Load Diff
+25 -18
View File
@@ -28,8 +28,8 @@ keywords = [
license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/matrix-construct/tuwunel"
rust-version = "1.88.0"
version = "1.4.3"
rust-version = "1.89.0"
version = "1.4.6"
[workspace.metadata.crane]
name = "tuwunel"
@@ -104,7 +104,7 @@ features = [
version = "1.10"
[workspace.dependencies.bytesize]
version = "2.0"
version = "2.1"
[workspace.dependencies.cargo_toml]
version = "0.22"
@@ -192,7 +192,7 @@ version = "1.3"
version = "0.1"
[workspace.dependencies.hyper]
version = "1.6"
version = "1.7"
default-features = false
features = [
"server",
@@ -262,7 +262,7 @@ version = "0.1"
version = "1.0"
[workspace.dependencies.minicbor]
version = "2.0"
version = "2.1"
features = ["std"]
[workspace.dependencies.minicbor-serde]
@@ -281,7 +281,7 @@ features = [
version = "0.2"
[workspace.dependencies.opentelemetry]
version = "0.30.0"
version = "0.31"
# Disabled until they move to opentelemetry 0.30
#[workspace.dependencies.opentelemetry-jaeger]
@@ -289,7 +289,7 @@ version = "0.30.0"
#features = ["rt-tokio"]
[workspace.dependencies.opentelemetry_sdk]
version = "0.30"
version = "0.31"
features = ["rt-tokio"]
[workspace.dependencies.proc-macro2]
@@ -302,7 +302,7 @@ version = "1.0"
version = "0.8"
[workspace.dependencies.regex]
version = "1.11"
version = "1.12"
[workspace.dependencies.reqwest]
version = "0.12"
@@ -320,7 +320,7 @@ default-features = false
[workspace.dependencies.ruma]
git = "https://github.com/matrix-construct/ruma"
rev = "3c26be1e4d670c2c71e2bc84b3f09c67c4188612"
rev = "db4bf59eaa1757735f38489f8924348ae05a6a17"
features = [
"__compat",
"appservice-api-c",
@@ -339,6 +339,7 @@ features = [
"unstable-msc2870",
"unstable-msc3026",
"unstable-msc3061",
"unstable-msc3814",
"unstable-msc3245",
"unstable-msc3381", # polls
"unstable-msc3489", # beacon / live location
@@ -348,6 +349,7 @@ features = [
"unstable-msc4121",
"unstable-msc4125",
"unstable-msc4133",
"unstable-msc4143",
"unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4310",
@@ -384,7 +386,7 @@ version = "0.4"
default-features = false
[workspace.dependencies.sentry]
version = "0.42"
version = "0.45"
default-features = false
features = [
"backtrace",
@@ -400,10 +402,10 @@ features = [
]
[workspace.dependencies.sentry-tower]
version = "0.42"
version = "0.45"
[workspace.dependencies.sentry-tracing]
version = "0.42"
version = "0.45"
[workspace.dependencies.serde]
version = "1.0"
@@ -498,7 +500,7 @@ features = [
]
[workspace.dependencies.tokio]
version = "1.47"
version = "1.48"
default-features = false
features = [
"fs",
@@ -551,7 +553,7 @@ default-features = false
version = "0.2"
[workspace.dependencies.tracing-opentelemetry]
version = "0.31"
version = "0.32"
[workspace.dependencies.tracing-subscriber]
version = "0.3"
@@ -702,8 +704,8 @@ inherits = "release-native.build-override"
#]
[profile.bench]
debug = false
strip = false
debug = "limited"
strip = "none"
#rustflags = [
# "-Cremark=all",
# '-Ztime-passes',
@@ -723,7 +725,7 @@ strip = false
# and can be raised if build times are tolerable.
[profile.dev]
debug = "full"
debug = 0
#rustflags = [
# '--cfg', 'tuwunel_mods',
# '-Ztime-passes',
@@ -783,7 +785,7 @@ inherits = "dev"
[profile.dev.package.'*']
inherits = "dev"
debug = "limited"
debug = 0
incremental = false
codegen-units = 1
opt-level = 'z'
@@ -800,6 +802,11 @@ opt-level = 'z'
# '-Clink-arg=-Wl,-z,nodelete',
#]
# same as dev but slower.
[profile.dbg]
inherits = "dev"
debug = "full"
# primarily used for CI
[profile.test]
debug = "limited"
+26 -29
View File
@@ -1,50 +1,47 @@
# Tuwunel 1.4.3
# Tuwunel 1.4.6
October 10, 2025
November 6, 2025
### Featured
### New Features
**Sync Tokens have been eliminated** now with stateless sync. Users should notice a reduction in database size after running this version. Long-time users, including from conduwuit and even Conduit will benefit the most. New users may not notice reductions, but nevertheless will be preventing database growth going forward. With the prior addition of room deletions courtesy of @dasha-uwu, only enhanced media retention remains between Tuwunel and sustainable cost-of-ownership.
- Element Call discovery support was implemented by @tototomate123 in (#209). Adding a `[[global.well_known.rtc_transports]]` section in your [config file](https://github.com/matrix-construct/tuwunel/blob/e1f89b69ea117f166be423f035a5a34f4c0e7366/tuwunel-example.toml#L1835-L1851) enables discovery. More information on setting up Element Call can be found at [Spaetzblog](https://sspaeth.de/2024/11/sfu/), skipping step one, and performing step three in your Tuwunel config.
**Sliding Sync has been fixed (Element X)** after a third pass was made to install an entirely new architecture based on the latest evolution of the highly active MSC4186. For background, the first work done earlier this year was for optimization without logical changes to what was inherited from conduwuit. The existing implementation worked by all appearances, but didn't meet specific production quality demands. The second pass made last month to rapidly prepare this passé implementation for production use against highly developed modern clients did not turn out well. More tests passed; fewer things worked. This time the core logic had been rewritten. These three iterations have now modernized the entire module to keep up with the final stages of the specification's development. It is still not perfect, so your input and issue reports are greatly appreciated as always.
- Dehydrated Device support (MSC3814) is now available (#200). This feature allows users to receive encrypted messages without being logged in. Supporting clients will setup the dehydrated device automatically and it will "just work" behind the scenes; in fact, these clients will also hide it from the sessions list to avoid confusion. Support is not widespread yet but it has been tested with matrix-js-sdk clients such as Element-web. This feature was commissioned and made public by an enterprise sponsor.
- Notification panel (the 🔔 button) has been implemented in (#201). Even though Element-web now requires enabling it in the Labs menu, the underlying support (`GET /_matrix/client/v3/notifications`) enhances the push-notification handling of other clients.
- Live room previews are now available. This support (`GET /_matrix/client/v3/events`) allows users, including guests, to sync updates for a public room without joining (4afd6f347b1).
- Thanks to a suggestion by @cyberdoors in (#29), the configuration option `encryption_enabled_by_default_for_room_type` is now available. This feature can enable encryption for a room even when the client does not. The values `all` or `invite` are accepted, the latter roughly meaning DM's only. Neither are enabled by default.
### Enhancements
- Configuration options passed after arguments `--option` or `-O` now accept dots to address the TOML tables found in the config file. Thanks to the suggestion by @lucat1 while trying to configure `ldap.bind_password_file` from the command line (#162). This was separately uncovered by @andrewerf trying to configure the `tls` section (#167).
- Thank you @tototomate123 for improving the reverse-proxy docs, adding dedicated pages for both [Caddy](https://github.com/matrix-construct/tuwunel/blob/e0a997c22784b453735b24907e607412b153ba56/docs/deploying/reverse-proxy-caddy.md) and [Nginx](https://github.com/matrix-construct/tuwunel/blob/e0a997c22784b453735b24907e607412b153ba56/docs/deploying/reverse-proxy-nginx.md) in (#209). Thanks to @tycode for pointing out the docs were missing for alternative proxies in (#197).
- Configuring `log_to_stderr` now provides an explicit way to redirect logging and tracing to stderr. This is often requested for use with systemd.
- Thanks to an observation by @iwalkalone69 in (#40), the last-seen time for a device in the session list is now updated acceptably. This function piggybacks on the presence system to prevent writing too frequently; testing has never shown it more than a minute or few out of date.
- The `!admin query raw` commands no longer require the redundant `raw-` prefix on every command name.
- Thanks to an inquiry by @EntityinArray in (#189) guest-accounts can now be enabled while registration tokens are also enabled to prevent fully open account registration. Note that registration tokens don't apply to guest-accounts and those are still fully open.
- Thanks to @SophiaH67 for pointing out that our new verbose-logging docker images aren't very useful without being pushed to registries, then taking the initiative to host it themselves until we corrected that.
- After a report by @munkinasack in (#186), @dasha-uwu determined we could solve a lot of recurring network issues by clearing the well-known cache entry for a destination that becomes unreachable.
- Thanks to @dasha-uwu for general improvements, refactoring and fixes for the room alias and presence subsystems.
- Thanks to a report by @ohitsdylan a cryptic error message from the DNS resolver has now been caught and reworded to indicate a missing or empty `/etc/resolv.conf` (#179).
- Inspired by @boarfish offering due confusion about our many build variants in (#175), some documentation about `x86_64-v1` `-v2` and `-v3` has been improved. Special thanks to @Hashbrown777 for providing a quick one-liner shell script which made its way to the documentation.
- Various performance improvements took place in s2s request handlers, and the ubiquitous matrix event `Pdu` structure.
- Nix builds have been added to CI.
- Courtesy of @dasha-uwu the list of servers attempted when joining a room is now properly shuffled to increase the odds of finding a viable server, especially if an additional join attempt is made.
### Bug Fixes
- Thanks to @harharlinks for reporting the Code of Conduct link on our github-pages was 404. Also thanks to @Tronde for reporting additional broken links in (#165). This helped discover pages had stopped deploying at some point and needed fixing.
- Special thanks to @BVollmerhaus for finding the TURN secret file configured by `turn_secret_file` was broken in (#211), forcing users to configure `turn_secret` directly. Thank you for fixing this in (#212).
- Thanks to @agx for contributing a fix for the systemd units on multiple platforms which contained unknown or deprecated keys (#168). And thanks again for adding missing documentation to the well-known sections of the example configuration which many users will greatly appreciate (#173).
- Thank you @scvalex for updating the nix build for Tuwunel's integration tests and re-enabling all checks. (#215)
- @mitch9911:matrix.org reported the `device_signing/upload` endpoint was omitted when adding JWT as a UIAA flow to other API's. This was subsequently patched (#169).
- Thanks to a report by @Anagastes in (#146) **Nheko and NeoChat users can now enjoy properly verified devices.** Special thanks for the assistance of @deepbluev7 with diagnosing the cross-signing signature issue.
- The report by @orhtej2 of an invalid `?via` parameter sent by FluffyChat when joining a version 12 room was greatly appreciated, even though there was nothing more we could do on our end (#171). Thanks to @dasha-uwu for triaging and investigating this issue to conclusion.
- Database columns intended for deletion, notably `roomsynctoken_shortstatehash`, never had the deletion command actually invoked on them 😭 explaining the lack of enthusiasm after the 1.4.3 release introduced stateless sync. **Users will now see the free disk space they were promised.** This was uncovered during an unrelated issue investigation courtesy of @frebib.
- Thanks to a follow-up by @alaviss in (#176) the DNS-passthru feature was found to still be enforcing a large minimum-TTL for its DNS cache. This was subsequently corrected.
- Thanks to investigation by @dasha-uwu the pagination tokens in the `/relations` endpoint were buggy and now operate correctly.
- Thank you @canarysnort01 for the apropos and rather surgical off-by-one fix to sliding-sync range selection in (#188). Unfortunately this entire unit had to be rewritten for the latest iteration of sliding-sync, but the fix carried value in any case to improve the rewrite.
- Thanks to @Polve for identifying the `DynamicUser=yes` directive in the systemd files was invalid and advising a replacement in (#207).
### Notices
- Thanks to @daudix for reporting an edge-case where the server will refuse to start rather than robustly reporting errors during startup checks and recreate a missing media directory (#213).
It has come to our attention courtesy of @andrewerf that the Arch packages are built with `--no-default-features`. This may be a problem for an ideal experience. The backstory is that conduwuit underwent a "feature skew" over its lifetime which still remains today: our default-features are basically minimal requirements, while `--all-features` should be default features. Let us first take a moment to reiterate our gratitude to AUR package maintainers @drrossum and @kimiblock who have supported this project from the first hours of its existence. No action is required on their part as the plan now is to remove several optional features by the next release to make `--no-default-features` viable. We still recommend default features in general unless this conflicts with AUR policies or philosophies.
- Push rule evaluation was never implemented for invites arriving over federation. Notifications are now properly sent in this case.
- Sliding-sync handlers were susceptible to errors under rare circumstances escaping to cause an HTTP 500, which wreaks havoc on the rust-sdk. This has now been prevented.
- Federating with Conduit over several non-essential endpoints was broken. It is unclear whether this affected an actual Conduit release version, but thanks to @kladki a fix is scheduled and we have included a workaround now on this end.
-1
View File
@@ -5,7 +5,6 @@ After=network-online.target
Documentation=https://tuwunel.chat/
[Service]
DynamicUser=yes
User=tuwunel
Group=tuwunel
Type=notify
+11 -8
View File
@@ -12,6 +12,7 @@ ARG CARGO_TARGET_DIR
ARG cargo_target_profile
ARG cargo_target_artifact
ARG cargo_target_share
ARG cargo_share
ARG CARGO_TERM_VERBOSE=0
ARG RUST_BACKTRACE
ARG JEMALLOC_OVERRIDE
@@ -33,6 +34,8 @@ ARG color_args="--color always"
ARG recipe_args=""
ARG cargo_args=""
ARG git_checkout
ARG targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
ARG targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
WORKDIR /
COPY --link --from=input . .
@@ -43,6 +46,7 @@ WORKDIR /usr/lib/${sys_triple}
COPY --link --from=rocksdb . .
WORKDIR /usr/src/tuwunel
SHELL ["/bin/bash", "-c"]
ENV PATH="${CARGO_HOME}/bin:$PATH"
ENV CARGO_TERM_VERBOSE="${CARGO_TERM_VERBOSE}"
ENV RUST_BACKTRACE="${RUST_BACKTRACE}"
@@ -58,17 +62,15 @@ ENV CARGO_PROFILE_RELEASE_DEBUGINFO_DEBUG="${CARGO_PROFILE_RELEASE_DEBUGINFO_DEB
ENV CARGO_PROFILE_RELEASE_DEBUGINFO_LTO="${CARGO_PROFILE_RELEASE_DEBUGINFO_LTO}"
ENV CARGO_BUILD_RUSTFLAGS="${CARGO_BUILD_RUSTFLAGS}"
ENV CARGO_TARGET_DIR="${CARGO_TARGET_DIR}"
ENV targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
ENV targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
RUN \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_target_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_target_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_share}/examples,sharing=locked \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
@@ -77,7 +79,8 @@ RUN \
<<EOF
set -eux
ulimit -n 65535
rustup run ${rust_toolchain} \
rustup run "${rust_toolchain}" \
cargo ${cargo_cmd} \
--verbose \
--locked \
+8 -7
View File
@@ -10,9 +10,12 @@ ARG CARGO_TARGET_DIR
ARG cargo_target_profile
ARG cargo_target_artifact
ARG cargo_target_share
ARG cargo_share
ARG cargo_profile
ARG cargo_features
ARG cargo_spec_features
ARG targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
ARG targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
ARG pkg_dir
ARG deb_args=""
@@ -20,17 +23,15 @@ WORKDIR /
COPY --link --from=input . .
WORKDIR /usr/src/tuwunel
ENV targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
ENV targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
RUN \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_target_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_target_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_share}/examples,sharing=locked \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
+9 -5
View File
@@ -1,6 +1,7 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS build-rpm
ARG rust_target
ARG rust_toolchain
ARG RUSTUP_HOME
ARG CARGO_HOME
@@ -9,9 +10,12 @@ ARG CARGO_TARGET_DIR
ARG cargo_target_profile
ARG cargo_target_artifact
ARG cargo_target_share
ARG cargo_share
ARG cargo_profile
ARG cargo_features
ARG cargo_spec_features
ARG targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
ARG targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
ARG pkg_dir
ARG gen_rpm_args=""
@@ -23,11 +27,11 @@ RUN \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_target_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_target_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_share}/examples,sharing=locked \
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
+5 -11
View File
@@ -103,9 +103,6 @@ EOF
FROM input AS complement-testee
WORKDIR /
COPY --link --from=input . .
EXPOSE 8008 8448
RUN mkdir /database
COPY --from=complement-config * /complement/
@@ -115,9 +112,6 @@ ENTRYPOINT tuwunel -Oserver_name=\""$SERVER_NAME\""
FROM input AS complement-testee-valgrind
WORKDIR /
COPY --link --from=input . .
EXPOSE 8008 8448
RUN mkdir /database
COPY --from=complement-config * /complement/
@@ -134,12 +128,13 @@ ENTRYPOINT valgrind \
FROM input AS complement-base
ARG var_cache
ARG var_lib_apt
ARG complement_ref="426bc3458e5287398ec39ab7d2b8f2e3103dcf39"
ARG complement_tags="conduwuit_blacklist"
ARG complement_tests="./tests/..."
ARG complement_run=".*"
WORKDIR /usr/src
ADD https://github.com/matrix-construct/complement.git#403840348f6bcc9cc8ed1671dc2f638c2b1ce4ac complement
ADD https://github.com/matrix-construct/complement.git#${complement_ref} complement
WORKDIR /usr/src/complement
ENV COMPLEMENT_BASE_IMAGE="complement-testee"
@@ -155,8 +150,9 @@ EOF
FROM input AS complement-tester
ARG complement_verbose=0
ARG complement_debug=0
ARG complement_dirty=0
ARG complement_count=1
ARG complement_parallel=16
ARG complement_parallel=1
ARG complement_shuffle=1337
ARG complement_timeout="1h"
ARG complement_run=".*"
@@ -165,9 +161,6 @@ ARG complement_tags="conduwuit_blacklist"
ARG complement_tests="./tests/..."
ARG complement_base_image
WORKDIR /
COPY --link --from=input . .
WORKDIR /usr/src/complement
ENV COMPLEMENT_DEBUG=$complement_debug
ENV complement_parallel="$complement_parallel"
@@ -179,6 +172,7 @@ ENV complement_tests="$complement_tests"
ENV complement_skip="$complement_skip"
ENV complement_run="$complement_run"
ENV complement_tests="$complement_tests"
ENV COMPLEMENT_ENABLE_DIRTY_RUNS="$complement_dirty"
ENV COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS="$complement_verbose"
ENV COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT="host.docker.internal"
ENV COMPLEMENT_HOST_MOUNTS="/var/run/docker.sock:/var/run/docker.sock"
+31 -39
View File
@@ -1,26 +1,28 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS matrix-rust-sdk-integration
FROM input AS rust-sdk-integration
ARG sys_name
ARG sys_version
ARG rust_toolchain
ARG feat_set
ARG rust_target
ARG rust_toolchain
ARG cargo_profile
ARG RUSTUP_HOME
ARG CARGO_HOME
ARG CARGO_TARGET
ARG MRSDK_TARGET_DIR="/usr/src/matrix-rust-sdk"
ARG mrsdk_profile="release"
ARG mrsdk_target_share="${MRSDK_TARGET_DIR}/${sys_name}/${sys_version}/${rust_target}/${rust_toolchain}/${mrsdk_profile}/_shared_cache"
ARG MRSDK_TARGET_DIR="/usr/src/matrix-rust-sdk/target"
ARG mrsdk_target_share
#ARG mrsdk_ref="integration"
ARG mrsdk_ref="tuwunel-changes"
ARG mrsdk_test_args=""
ARG mrsdk_test_opts=""
#TODO!!!
ARG mrsdk_skip_list="--skip test_history_share_on_invite_pin_violation --skip test_room_notification_count"
WORKDIR /
COPY --link --from=input . .
ARG mrsdk_skip_list=""
ARG mrsdk_parallel=2
ARG mrsdk_startup_delay="10s"
ARG mrsdk_testee="/usr/bin/tuwunel"
WORKDIR /usr/src
ADD --link https://github.com/matrix-construct/matrix-rust-sdk.git matrix-rust-sdk
ADD --link https://github.com/matrix-construct/matrix-rust-sdk.git#${mrsdk_ref} matrix-rust-sdk
WORKDIR /etc
COPY <<EOF tuwunel.toml
@@ -60,49 +62,39 @@ WORKDIR /usr/bin
COPY --link --from=install /usr/bin/tuwunel .
WORKDIR /usr/src/matrix-rust-sdk
ENV targ_dir="${MRSDK_TARGET_DIR}/${mrsdk_profile}"
ENV targ_targ_dir="${MRSDK_TARGET_DIR}/${rust_target}/${mrsdk_profile}"
SHELL ["/bin/bash", "-c"]
ENV RUST_BACKTRACE="full"
ENV TUWUNEL_CONFIG="/etc/tuwunel.toml"
ENV TUWUNEL_DATABASE_PATH="/var/db/tuwunel"
ENV TUWUNEL_SERVER_NAME="localhost:6167"
ENV TUWUNEL_PORT="[6167]"
ENV HOMESERVER_URL="http://localhost:6167"
ENV TUWUNEL_SERVER_NAME="localhost"
ENV TUWUNEL_PORT="[8448]"
ENV HOMESERVER_URL="http://localhost:8448"
RUN \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
--mount=type=cache,dst=${targ_dir}/deps,id=${mrsdk_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_dir}/build,id=${mrsdk_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_dir}/examples,id=${mrsdk_target_share}/examples,sharing=locked \
--mount=type=cache,dst=${targ_dir}/incremental,id=${mrsdk_target_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${mrsdk_target_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/deps,id=${mrsdk_target_share}/deps,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/build,id=${mrsdk_target_share}/build,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/examples,id=${mrsdk_target_share}/examples,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/incremental,id=${mrsdk_target_share}/incremental,sharing=locked \
--mount=type=cache,dst=${targ_targ_dir}/.fingerprint,id=${mrsdk_target_share}/fingerprint,sharing=locked \
--mount=type=cache,dst=${MRSDK_TARGET_DIR},id=${mrsdk_target_share},sharing=locked \
<<EOF
set -eux
nohup /usr/bin/tuwunel 1> /var/log/tuwunel.log &
PID=$!; trap "sleep 10s; set +ex; kill -QUIT ${PID}; wait ${PID}" EXIT
sleep 3s
nohup ${mrsdk_testee[@]} 1> /var/log/tuwunel.log &
PID=$!; trap "sleep 10s; set +e; kill -QUIT ${PID}; wait ${PID}" EXIT
sleep "${mrsdk_startup_delay}"
rustup run ${rust_toolchain} \
cargo test \
--locked \
--release \
--color always \
--features default \
--target "${rust_target}" \
--target-dir "${MRSDK_TARGET_DIR}" \
--package matrix-sdk-integration-testing \
${mrsdk_test_args} \
"--color=always" \
"--features=default" \
"--target=${rust_target}" \
"--target-dir=${MRSDK_TARGET_DIR}" \
"--package=matrix-sdk-integration-testing" \
${mrsdk_test_args[@]} \
-- \
--test-threads 1 \
--color always \
${mrsdk_skip_list} \
${mrsdk_test_opts} \
"--color=always" \
"--test-threads=${mrsdk_parallel}" \
${mrsdk_skip_list[@]} \
${mrsdk_test_opts[@]} \
;
EOF
+26 -24
View File
@@ -1,12 +1,10 @@
# syntax = docker/dockerfile:1.11-labs
FROM input AS nix-base
ARG sys_name
ARG sys_version
ARG sys_target
WORKDIR /
COPY --link --from=input . .
RUN \
--mount=type=cache,dst=/nix,sharing=shared \
--mount=type=cache,dst=/root/.cache/nix,sharing=shared \
@@ -20,9 +18,6 @@ EOF
FROM nix-base AS build-nix
ARG sys_name
ARG sys_version
ARG sys_target
WORKDIR /usr/src/tuwunel
COPY --link --from=source /usr/src/tuwunel .
@@ -44,15 +39,13 @@ RUN \
EOF
FROM build-nix AS smoke-nix
ARG sys_name
ARG sys_version
ARG sys_target
FROM input AS smoke-nix
WORKDIR /
COPY --link --from=build-nix . .
COPY --link --from=nix-base . .
WORKDIR /opt/tuwunel
WORKDIR /usr/src/tuwunel
COPY --link --from=source /usr/src/tuwunel .
ENV TUWUNEL_DATABASE_PATH="/tmp/tuwunel/smoketest.db"
ENV TUWUNEL_LOG="info"
RUN \
@@ -61,29 +54,38 @@ RUN \
--mount=type=cache,dst=/root/.local/state/nix,sharing=shared \
<<EOF
set -eux
alias nix="nix --extra-experimental-features nix-command --extra-experimental-features flakes"
bin/tuwunel \
-Otest='["smoke", "fresh"]' \
-Oserver_name=\"localhost\" \
nix run \
--verbose \
--cores 0 \
--max-jobs $(nproc) \
--log-format raw \
.#all-features \
-- \
-Otest='["smoke", "fresh"]' \
-Oserver_name=\"localhost\" \
EOF
FROM build-nix AS nix-pkg
ARG sys_name
ARG sys_version
ARG sys_target
FROM input AS nix-pkg
WORKDIR /
COPY --link --from=build-nix . .
COPY --link --from=nix-base . .
WORKDIR /usr/src/tuwunel
COPY --link --from=source /usr/src/tuwunel .
RUN \
--mount=type=cache,dst=/nix,sharing=shared \
--mount=type=cache,dst=/root/.cache/nix,sharing=shared \
--mount=type=cache,dst=/root/.local/state/nix,sharing=shared \
<<EOF
set -eux
#TODO: extract derivation?
mkdir -p /opt/tuwunel
touch /opt/tuwunel/tuwunel.drv
set -eux
alias nix="nix --extra-experimental-features nix-command --extra-experimental-features flakes"
ID=$(nix-store --realise $(nix path-info --derivation))
mkdir -p tuwunel
nix-store --export $ID > tuwunel/tuwunel.drv
tar -cvf /opt/tuwunel.nix.tar tuwunel
EOF
-6
View File
@@ -6,9 +6,6 @@ ARG rust_target
ARG rustup_version="1.28.2"
ARG rustup_profile="minimal"
WORKDIR /
COPY --link --from=input . .
WORKDIR ${RUST_HOME}
RUN <<EOF
set -eux
@@ -32,9 +29,6 @@ ARG CARGO_TERM_VERBOSE
ARG rustup_components
ARG cargo_installs
WORKDIR /
COPY --link --from=input . .
WORKDIR ${RUST_HOME}
ENV CARGO_TARGET="${rust_target}"
ENV RUSTUP_HOME="${RUSTUP_HOME}"
-7
View File
@@ -3,9 +3,6 @@
FROM input AS source
ARG git_checkout
WORKDIR /
COPY --link --from=input . .
ADD --link --keep-git-dir . /usr/src/tuwunel
WORKDIR /usr/src/tuwunel
RUN <<EOF
@@ -30,7 +27,6 @@ ARG JEMALLOC_OVERRIDE
ARG ROCKSDB_LIB_DIR
WORKDIR /
COPY --link --from=input . .
COPY --link --from=rust ${RUST_HOME} ${RUST_HOME}
COPY --link --from=source /usr/src/tuwunel /usr/src/tuwunel
@@ -67,9 +63,6 @@ ARG RUSTUP_HOME
ARG CARGO_HOME
ARG CARGO_TARGET
WORKDIR /
COPY --link --from=input . .
WORKDIR /usr/src/tuwunel
RUN \
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=locked \
-3
View File
@@ -12,9 +12,6 @@ ARG var_lib_apt
ARG packages
ARG DEBIAN_FRONTEND
WORKDIR /
COPY --link --from=input . .
ENV DEBIAN_FRONTEND="${DEBIAN_FRONTEND}"
RUN \
--mount=type=cache,dst=/var/cache,id=${var_cache},sharing=locked \
+74 -39
View File
@@ -1,4 +1,4 @@
variable "CI" {}
variable "GITHUB_ACTOR" {}
variable "GITHUB_REPOSITORY" {}
variable "GITHUB_REF" {}
@@ -247,7 +247,7 @@ group "tests" {
group "matrix-compliance" {
targets = [
"complement",
"matrix-rust-sdk-integration",
"rust-sdk-integ",
]
}
@@ -440,61 +440,96 @@ target "complement-config" {
# Integration tests
#
group "integrations" {
group "integration" {
targets = [
"matrix-rust-sdk-integration",
"integration",
"integ",
"rust-sdk-integ",
]
}
target "matrix-rust-sdk-integration" {
name = elem("matrix-rust-sdk-integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
variable "valgrind_flags" {
default = "--error-exitcode=1 --exit-on-first-error=yes --undef-value-errors=no --leak-check=no"
}
target "rust-sdk-valgrind" {
name = elem("rust-sdk-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("matrix-rust-sdk-integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
elem_tag("rust-sdk-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
output = ["type=docker,compression=zstd,mode=max,compression-level=${zstd_image_compress_level}"]
cache_to = ["type=local,compression=zstd,mode=max,compression-level=${cache_compress_level}"]
target = "matrix-rust-sdk-integration"
dockerfile = "${docker_dir}/Dockerfile.matrix-rust-sdk"
matrix = cargo_rust_feat_sys
inherits = [
elem("rust", [rust_toolchain, rust_target, sys_name, sys_version, sys_target])
elem("rust-sdk-integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
]
contexts = {
input = elem("target:rust", [rust_toolchain, rust_target, sys_name, sys_version, sys_target])
install = elem("target:install", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
mrsdk_test_args=""
mrsdk_test_opts=""
VALGRINDFLAGS = "${valgrind_flags}"
mrsdk_testee = "valgrind ${valgrind_flags} /usr/bin/tuwunel"
mrsdk_test_args = ""
mrsdk_startup_delay = "30s"
mrsdk_skip_list =<<EOF
--skip test_delayed_invite_response_and_sent_message_decryption
--skip test_history_share_on_invite_pin_violation
EOF
}
}
target "integration-valgrind" {
name = elem("integration-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
target "rust-sdk-integ" {
name = elem("rust-sdk-integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("integration-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
elem_tag("rust-sdk-integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
output = ["type=docker,compression=zstd,mode=max,compression-level=${zstd_image_compress_level}"]
cache_to = ["type=local,compression=zstd,mode=max,compression-level=${cache_compress_level}"]
target = "rust-sdk-integration"
dockerfile = "${docker_dir}/Dockerfile.matrix-rust-sdk"
matrix = cargo_rust_feat_sys
inherits = [
elem("rust", [rust_toolchain, rust_target, sys_name, sys_version, sys_target]),
elem("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
contexts = {
input = elem("target:rust", [rust_toolchain, rust_target, sys_name, sys_version, sys_target])
install = elem("target:install", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
mrsdk_target_share = "/usr/src/matrix-rust-sdk/target/${sys_name}/${sys_version}/${rust_target}/${rust_toolchain}/_shared_cache"
mrsdk_testee = "/usr/bin/tuwunel"
mrsdk_test_args = "--no-fail-fast"
mrsdk_skip_list =<<EOF
--skip test_delayed_invite_response_and_sent_message_decryption
EOF
}
}
target "integ-valgrind" {
name = elem("integ-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("integ-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
matrix = cargo_rust_feat_sys
inherits = [
elem("integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
elem("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
contexts = {
input = elem("target:integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
input = elem("target:build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
VALGRINDFLAGS = "--error-exitcode=1 --exit-on-first-error=yes --undef-value-errors=no --leak-check=no"
VALGRINDFLAGS = "${valgrind_flags}"
cargo_cmd = "valgrind test"
cargo_args = "--test=*"
}
}
target "integration" {
name = elem("integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
target "integ" {
name = elem("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
elem_tag("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
target = "cargo"
matrix = cargo_rust_feat_sys
inherits = [
elem("build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
@@ -504,7 +539,6 @@ target "integration" {
}
args = {
TUWUNEL_DATABASE_PATH = "/tmp/integration.test.db"
cargo_cmd = (cargo_profile == "bench"? "bench": "test")
cargo_args = (cargo_profile == "bench"?
"--no-fail-fast --bench=*": "--no-fail-fast --test=*"
@@ -527,16 +561,16 @@ group "smoke" {
}
target "smoke-nix" {
name = elem("smoke-nix", [sys_name, sys_version, sys_target])
name = elem("smoke-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("smoke-nix", [sys_name, sys_version, sys_target], "latest"),
elem_tag("smoke-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
output = ["type=cacheonly,compression=zstd,mode=min,compression-level=${cache_compress_level}"]
dockerfile = "${docker_dir}/Dockerfile.nix"
target = "smoke-nix"
matrix = sys
matrix = cargo_rust_feat_sys
inherits = [
elem("build-nix", [sys_name, sys_version, sys_target]),
elem("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
}
@@ -630,7 +664,7 @@ target "unit-valgrind" {
input = elem("target:unit", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
VALGRINDFLAGS = "--error-exitcode=1 --exit-on-first-error=yes --undef-value-errors=no --leak-check=no"
VALGRINDFLAGS = "${valgrind_flags}"
cargo_cmd = "valgrind test"
cargo_args = "--lib --bins"
}
@@ -961,20 +995,20 @@ target "nix" {
target = "nix-pkg"
matrix = cargo_rust_feat_sys
inherits = [
elem("build-nix", [sys_name, sys_version, sys_target]),
elem("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
]
}
target "build-nix" {
name = elem("build-nix", [sys_name, sys_version, sys_target])
name = elem("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("build-nix", [sys_name, sys_version, sys_target], "latest"),
elem_tag("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
output = ["type=cacheonly,compression=zstd,mode=min,compression-level=${cache_compress_level}"]
cache_to = ["type=local,compression=zstd,mode=max,compression-level=${cache_compress_level}"]
dockerfile = "${docker_dir}/Dockerfile.nix"
target = "build-nix"
matrix = sys
matrix = cargo_rust_feat_sys
inherits = [
elem("builder", [sys_name, sys_version, sys_target]),
elem("source", [sys_name, sys_version, sys_target]),
@@ -1006,7 +1040,6 @@ target "book" {
}
dockerfile-inline =<<EOF
FROM input AS book
COPY --link --from=input . .
RUN ["mdbook", "build", "-d", "/book", "/usr/src/tuwunel"]
EOF
}
@@ -1352,6 +1385,8 @@ target "deps-base" {
# cache key for unique artifact area
cargo_target_artifact = "${cargo_tgt_dir_base}/${sys_name}/${sys_version}/${rust_target}/${rust_toolchain}/${cargo_profile}/${feat_set}/${git_ref_sha}"
# cache key for hashed subdirs
cargo_share = "${cargo_tgt_dir_base}/${sys_name}/${sys_version}/${rust_toolchain}/${cargo_profile}/_shared_cache"
# cache key for hashed subdirs
cargo_target_share = "${cargo_tgt_dir_base}/${sys_name}/${sys_version}/${rust_target}/${rust_toolchain}/${cargo_profile}/_shared_cache"
# cased name of profile subdir within target complex
cargo_target_profile = (
@@ -1390,7 +1425,7 @@ target "deps-base" {
"-C link-arg=-l:libgcc.a": "",
]):
(cargo_profile == "release" || cargo_profile == "bench") && rust_toolchain == "nightly"?
(cargo_profile == "release" || cargo_profile == "bench") && substr(rust_toolchain, 0, 7) == "nightly"?
join(" ", [
join(" ", rustflags),
join(" ", nightly_rustflags),
@@ -1432,7 +1467,7 @@ target "deps-base" {
"-C link-arg=-l:libgcc.a": "",
]):
rust_toolchain == "stable"?
substr(rust_toolchain, 0, 6) == "stable"?
join(" ", [
join(" ", rustflags),
join(" ", static_rustflags),
@@ -1452,7 +1487,7 @@ target "deps-base" {
"-C link-arg=-l:libgcc.a": "",
]):
rust_toolchain == "nightly"?
substr(rust_toolchain, 0, 7) == "nightly"?
join(" ", [
join(" ", rustflags),
join(" ", nightly_rustflags),
+3 -3
View File
@@ -16,6 +16,7 @@ default_sys_target="x86_64-v1-linux-gnu"
default_sys_version="testing-slim"
default_complement_verbose=0
default_complement_dirty=0
default_complement_count=1
default_complement_parallel=1
default_complement_shuffle=0
@@ -31,9 +32,7 @@ skip="${skip}|TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_topic
skip="${skip}|TestLogin/parallel/POST_/"
skip="${skip}|TestUnbanViaInvite"
skip="${skip}|TestRoomState/Parallel/GET_/publicRooms_lists_newly-created_room"
# flakes due to timeout in debug-mode
skip="${skip}|TestMSC4297StateResolutionV2_1_starts_from_empty_set"
skip="${skip}|TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph"
skip="${skip}|TestThreadReceiptsInSyncMSC4102"
set -a
cargo_profile="${cargo_profile:-$default_cargo_profile}"
@@ -53,6 +52,7 @@ set +a
envs=""
envs="$envs -e complement_verbose=${complement_verbose:-$default_complement_verbose}"
envs="$envs -e complement_count=${complement_count:-$default_complement_count}"
envs="$envs -e complement_dirty=${complement_dirty:-$default_complement_dirty}"
envs="$envs -e complement_parallel=${complement_parallel:-$default_complement_parallel}"
envs="$envs -e complement_shuffle=${complement_shuffle:-$default_complement_shuffle}"
envs="$envs -e complement_timeout=${complement_timeout:-$default_complement_timeout}"
+2
View File
@@ -5,6 +5,8 @@ # Summary
- [Examples](configuration/examples.md)
- [Deploying](deploying.md)
- [Generic](deploying/generic.md)
- [Reverse Proxy - Caddy](deploying/reverse-proxy-caddy.md)
- [Reverse Proxy - Nginx](deploying/reverse-proxy-nginx.md)
- [NixOS](deploying/nixos.md)
- [Docker](deploying/docker.md)
- [Kubernetes](deploying/kubernetes.md)
+23 -44
View File
@@ -141,46 +141,31 @@ ## Setting the correct file permissions
## Setting up the Reverse Proxy
We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc transparently with proper defaults.
For other software, please refer to their respective documentation or online guides.
We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc. transparently with proper defaults. However, Nginx is also well-supported and widely used.
### Caddy
**Choose your reverse proxy:**
After installing Caddy via your preferred method, create `/etc/caddy/conf.d/tuwunel_caddyfile`
and enter this (substitute for your server name).
- **[Caddy Setup Guide](reverse-proxy-caddy.md)** - Recommended for ease of use and automatic TLS
- **[Nginx Setup Guide](reverse-proxy-nginx.md)** - Popular choice with extensive documentation
```caddyfile
your.server.name, your.server.name:8448 {
# TCP reverse_proxy
reverse_proxy localhost:8008
# UNIX socket
#reverse_proxy unix//run/tuwunel/tuwunel.sock
}
```
### Quick Overview
That's it! Just start and enable the service and you're set.
Regardless of which reverse proxy you choose, you will need to:
```bash
sudo systemctl enable --now caddy
```
1. **Reverse proxy the following routes:**
- `/_matrix/` - core Matrix C-S and S-S APIs
- `/_tuwunel/` - ad-hoc Tuwunel routes such as `/local_user_count` and `/server_version`
### Other Reverse Proxies
2. **Optionally reverse proxy (recommended):**
- `/.well-known/matrix/client` and `/.well-known/matrix/server` if using Tuwunel to perform delegation (see the `[global.well_known]` config section)
- `/.well-known/matrix/support` if using Tuwunel to send the homeserver admin contact and support page (formerly known as MSC1929)
- `/` if you would like to see `hewwo from tuwunel woof!` at the root
As we would prefer our users to use Caddy, we will not provide configuration files for other proxies.
3. **Handle ports:**
- Port 443 (HTTPS) for client-server API
- Port 8448 for federation (if federating with other homeservers)
You will need to reverse proxy everything under following routes:
- `/_matrix/` - core Matrix C-S and S-S APIs
- `/_tuwunel/` - ad-hoc Tuwunel routes such as `/local_user_count` and
`/server_version`
You can optionally reverse proxy the following individual routes:
- `/.well-known/matrix/client` and `/.well-known/matrix/server` if using
Tuwunel to perform delegation (see the `[global.well_known]` config section)
- `/.well-known/matrix/support` if using Tuwunel to send the homeserver admin
contact and support page (formerly known as MSC1929)
- `/` if you would like to see `hewwo from tuwunel woof!` at the root
See the following spec pages for more details on these files:
See the following spec pages for more details on well-known files:
- [`/.well-known/matrix/server`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixserver)
- [`/.well-known/matrix/client`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient)
- [`/.well-known/matrix/support`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixsupport)
@@ -189,21 +174,15 @@ ### Other Reverse Proxies
- <https://matrix.org/.well-known/matrix/server>
- <https://matrix.org/.well-known/matrix/client>
For Apache and Nginx there are many examples available online.
### Other Reverse Proxies
Lighttpd is not supported as it seems to mess with the `X-Matrix` Authorization
header, making federation non-functional. If a workaround is found, feel free to share to get it added to the documentation here.
_Specific contributions for other proxies are welcome!_
If using Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent httpd from messing with the `X-Matrix` header (note that Apache isn't very good as a general reverse proxy and we discourage the usage of it if you can).
**Not Recommended:**
- **Apache**: While possible, Apache requires special configuration (`nocanon` in `ProxyPass`) to prevent corruption of the `X-Matrix` header.
- **Lighttpd**: Its proxy module alters the `X-Matrix` authorization header, breaking federation functionality.
If using Nginx, you need to give Tuwunel the request URI using `$request_uri`, or like so:
- `proxy_pass http://127.0.0.1:6167$request_uri;`
- `proxy_pass http://127.0.0.1:6167;`
Nginx users need to increase `client_max_body_size` (default is 1M) to match
`max_request_size` defined in tuwunel.toml.
## You're done
## You are done
Now you can start Tuwunel with:
+48
View File
@@ -0,0 +1,48 @@
# Reverse Proxy Setup - Caddy
[<= Back to Generic Deployment Guide](generic.md#setting-up-the-reverse-proxy)
We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc. transparently with proper defaults.
## Installation
Install Caddy via your preferred method. Refer to the [official Caddy installation guide](https://caddyserver.com/docs/install) for your distribution.
## Configuration
After installing Caddy, create `/etc/caddy/conf.d/tuwunel_caddyfile` and enter this (substitute `your.server.name` with your actual server name):
```caddyfile
your.server.name, your.server.name:8448 {
# TCP reverse_proxy
reverse_proxy localhost:8008
# UNIX socket (alternative - comment out the line above and uncomment this)
#reverse_proxy unix//run/tuwunel/tuwunel.sock
}
```
### What this does
- Handles both port 443 (HTTPS) and port 8448 (Matrix federation) automatically
- Automatically provisions and renews TLS certificates via Let's Encrypt
- Sets all necessary reverse proxy headers correctly
- Routes all traffic to Tuwunel listening on `localhost:8008`
That's it! Just start and enable the service and you're set.
```bash
sudo systemctl enable --now caddy
```
## Verification
After starting Caddy, verify it's working by checking:
```bash
curl https://your.server.name/_tuwunel/server_version
curl https://your.server.name:8448/_tuwunel/server_version
```
---
[=> Continue with "You're Done"](generic.md#you-are-done)
+164
View File
@@ -0,0 +1,164 @@
# Reverse Proxy Setup - Nginx
[<= Back to Generic Deployment Guide](generic.md#setting-up-the-reverse-proxy)
This guide shows you how to configure Nginx as a reverse proxy for Tuwunel with TLS support.
## Installation
Install Nginx via your preferred method. Most distributions include Nginx in their package repositories:
```bash
# Debian/Ubuntu
sudo apt install nginx
# Red Hat/Fedora
sudo dnf install nginx
# Arch Linux
sudo pacman -S nginx
```
## Configuration
Create a new configuration file at `/etc/nginx/sites-available/tuwunel` (or `/etc/nginx/conf.d/tuwunel.conf` on some distributions):
```nginx
# Client-Server API over HTTPS (port 443)
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name matrix.example.com;
# Nginx standard body size is 1MB, which is quite small for media uploads
# Increase this to match the max_request_size in your tuwunel.toml
client_max_body_size 100M;
# Forward requests to Tuwunel (listening on 127.0.0.1:8008)
location / {
proxy_pass http://127.0.0.1:8008;
# Preserve host and scheme - critical for proper Matrix operation
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
# TLS configuration (Let's Encrypt example using certbot)
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
}
# Matrix Federation over HTTPS (port 8448)
# Only needed if you want to federate with other homeservers
# Don't forget to open port 8448 in your firewall!
server {
listen 8448 ssl http2;
listen [::]:8448 ssl http2;
server_name matrix.example.com;
# Same body size increase for larger files
client_max_body_size 100M;
# Forward to the same local port as client-server API
location / {
proxy_pass http://127.0.0.1:8008;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
# TLS configuration (same certificates as above)
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
}
```
### Important Notes
- **Replace `matrix.example.com`** with your actual server name
- **`client_max_body_size`**: Must match or exceed `max_request_size` in your `tuwunel.toml`
- **Do NOT use `$request_uri`** in `proxy_pass` - while some guides suggest this, it's not necessary for Tuwunel and can cause issues
- **IPv6**: The `listen [::]:443` and `listen [::]:8448` lines enable IPv6 support. Remove them if you don't need IPv6
### TLS Certificates
The example above uses Let's Encrypt certificates via certbot. To obtain certificates:
```bash
sudo certbot certonly --nginx -d matrix.example.com
```
Certbot will automatically handle renewal. Make sure to reload Nginx after certificate renewal:
```bash
sudo systemctl reload nginx
```
### Optional: Timeout Configuration
The default Nginx timeouts are usually sufficient for Matrix operations. Element's long-polling `/sync` requests typically run for 30 seconds, which is within Nginx's default timeouts.
However, if you experience federation retries or dropped long-poll connections, you can extend the timeouts by adding these lines inside your `location /` blocks:
```nginx
location / {
proxy_pass http://127.0.0.1:8008;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
# Optional: Extend timeouts if experiencing issues
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
```
## Enable the Configuration
If using sites-available/sites-enabled structure:
```bash
sudo ln -s /etc/nginx/sites-available/tuwunel /etc/nginx/sites-enabled/
```
Test the configuration:
```bash
sudo nginx -t
```
If the test passes, reload Nginx:
```bash
sudo systemctl reload nginx
```
Enable Nginx to start on boot:
```bash
sudo systemctl enable nginx
```
## Verification
After configuring Nginx, verify it's working by checking:
```bash
curl https://matrix.example.com/_tuwunel/server_version
curl https://matrix.example.com:8448/_tuwunel/server_version
```
## Troubleshooting
### Apache Compatibility Note
If you're considering Apache instead of Nginx: Apache is not well-suited as a reverse proxy for Matrix homeservers. If you must use Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent httpd from corrupting the `X-Matrix` authorization header, which will break federation.
### Lighttpd is Not Supported
Lighttpd has known issues with the `X-Matrix` authorization header, making federation non-functional. We do not recommend using Lighttpd with Tuwunel.
---
[=> Continue with "You're Done"](generic.md#you-are-done)
Generated
+43 -39
View File
@@ -32,11 +32,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1756385612,
"narHash": "sha256-+NU5MMhuPHHRyvZZWNFG7zt+leRSPsJu1MwhOUzkPUk=",
"lastModified": 1761150191,
"narHash": "sha256-fPQmV+K836jR/Tu6slfMdm9HL3p0IEPB5x5I7Zc6v5E=",
"owner": "cachix",
"repo": "cachix",
"rev": "dc24688cd67518c3711d511fa369c0f5a131063a",
"rev": "5629a7d84c91df9c06650b18eb428cce4166c4fc",
"type": "github"
},
"original": {
@@ -68,11 +68,11 @@
]
},
"locked": {
"lastModified": 1748883665,
"narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=",
"lastModified": 1752264895,
"narHash": "sha256-1zBPE/PNAkPNUsOWFET4J0cjlvziH8DOekesDmjND+w=",
"owner": "cachix",
"repo": "cachix",
"rev": "f707778d902af4d62d8dd92c269f8e70de09acbe",
"rev": "47053aef762f452e816e44eb9a23fbc3827b241a",
"type": "github"
},
"original": {
@@ -116,11 +116,11 @@
},
"crane_2": {
"locked": {
"lastModified": 1758758545,
"narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=",
"lastModified": 1760924934,
"narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=",
"owner": "ipetkov",
"repo": "crane",
"rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364",
"rev": "c6b4d5308293d0d04fcfeee92705017537cad02f",
"type": "github"
},
"original": {
@@ -137,6 +137,7 @@
"cachix",
"flake-compat"
],
"flake-parts": "flake-parts_2",
"git-hooks": [
"cachix",
"git-hooks"
@@ -148,11 +149,11 @@
]
},
"locked": {
"lastModified": 1754404745,
"narHash": "sha256-BdbW/iTImczgcuATgQIa9sPGuYIBxVq2xqcvICsa2AQ=",
"lastModified": 1760560333,
"narHash": "sha256-goJQdVl9oDgCxF9CggPUw1DvB4gsot1jzMmz9px8Du8=",
"owner": "cachix",
"repo": "devenv",
"rev": "6563b21105168f90394dfaf58284b078af2d7275",
"rev": "0a4043938f540027e562c5a0feebbe6be872c3ea",
"type": "github"
},
"original": {
@@ -169,11 +170,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1759251341,
"narHash": "sha256-0vt4IQnTLyAhaeom3h9GOCpO2+av+wK4zP7O8BSKr6I=",
"lastModified": 1761547629,
"narHash": "sha256-4OH1CVm2PdjKRqEJ3RLfkQMDSBdn7VId6iyYCwKOK+U=",
"owner": "nix-community",
"repo": "fenix",
"rev": "af8a7505a1c62fab493157ea380a05c094bb63af",
"rev": "d82a7c64ea441e397914577c9a18f2867e5b364b",
"type": "github"
},
"original": {
@@ -258,16 +259,15 @@
"nixpkgs-lib": [
"cachix",
"devenv",
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"lastModified": 1756770412,
"narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"rev": "4524271976b625a4a605beefd893f270620fd751",
"type": "github"
},
"original": {
@@ -308,11 +308,11 @@
]
},
"locked": {
"lastModified": 1750779888,
"narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
"lastModified": 1760392170,
"narHash": "sha256-WftxJgr2MeDDFK47fQKywzC72L2jRc/PWcyGdjaDzkw=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
"rev": "46d55f0aeb1d567a78223e69729734f3dca25a85",
"type": "github"
},
"original": {
@@ -346,11 +346,11 @@
"liburing": {
"flake": false,
"locked": {
"lastModified": 1759268303,
"narHash": "sha256-nT3JWey0P2lPWlnwN57XHmuzkeW8Mo795+Ahf8S5TMI=",
"lastModified": 1761487364,
"narHash": "sha256-PmRdt9dQUFGcVrX7XCh7z2FpRm1SqKgiYbIwpkJPjRU=",
"owner": "axboe",
"repo": "liburing",
"rev": "1671cb952a8f680214b04ae9546d4f649f1b367b",
"rev": "65ec4223032a38870dde2bce646ba4de13d2feff",
"type": "github"
},
"original": {
@@ -367,7 +367,11 @@
"devenv",
"flake-compat"
],
"flake-parts": "flake-parts_2",
"flake-parts": [
"cachix",
"devenv",
"flake-parts"
],
"git-hooks-nix": [
"cachix",
"devenv",
@@ -388,16 +392,16 @@
]
},
"locked": {
"lastModified": 1752773918,
"narHash": "sha256-dOi/M6yNeuJlj88exI+7k154z+hAhFcuB8tZktiW7rg=",
"lastModified": 1758763079,
"narHash": "sha256-Bx1A+lShhOWwMuy3uDzZQvYiBKBFcKwy6G6NEohhv6A=",
"owner": "cachix",
"repo": "nix",
"rev": "031c3cf42d2e9391eee373507d8c12e0f9606779",
"rev": "6f0140527c2b0346df4afad7497baa08decb929f",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "devenv-2.30",
"ref": "devenv-2.30.5",
"repo": "nix",
"type": "github"
}
@@ -473,11 +477,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1754214453,
"narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=",
"lastModified": 1760524057,
"narHash": "sha256-EVAqOteLBFmd7pKkb0+FIUyzTF61VKi7YmvP1tw4nEw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376",
"rev": "544961dfcce86422ba200ed9a0b00dd4b1486ec5",
"type": "github"
},
"original": {
@@ -489,11 +493,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1759070547,
"narHash": "sha256-JVZl8NaVRYb0+381nl7LvPE+A774/dRpif01FKLrYFQ=",
"lastModified": 1761349956,
"narHash": "sha256-tH3wHnOJms+U4k/rK2Nn1RfBrhffX92jLP/2VndSn0w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "647e5c14cbd5067f44ac86b74f014962df460840",
"rev": "02f2cb8e0feb4596d20cc52fda73ccee960e3538",
"type": "github"
},
"original": {
@@ -538,11 +542,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1759134797,
"narHash": "sha256-YPi+jL3tx/yC5J5l7/OB7Lnlr9BMTzYnZtm7tRJzUNg=",
"lastModified": 1761500479,
"narHash": "sha256-syeBTCCU96qPJHcVpwHeCwmPCiLTDHHgYQYhpZ0iwLo=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "062ac7a5451e8e92a32e22a60d86882d6a034f3f",
"rev": "049767e6faa84b2d1a951d8f227e6ebd99d728a2",
"type": "github"
},
"original": {
+1 -1
View File
@@ -26,7 +26,7 @@
file = ./rust-toolchain.toml;
# See also `rust-toolchain.toml`
sha256 = "sha256-Qxt8XAuaUR2OMdKbN4u8dBJOhSHxS+uS06Wl9+flVEk=";
sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE=";
};
mkScope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
+19 -2
View File
@@ -202,8 +202,25 @@ craneLib.buildPackage ( commonAttrs // {
env = buildDepsOnlyEnv;
});
# Disabled due to integration test failing to find /etc/resolv.conf
doCheck = false;
nativeCheckInputs = [
pkgsBuildHost.libredirect.hook
];
preCheck =
let
fakeResolvConf = pkgsBuildHost.writeTextFile {
name = "resolv.conf";
text = ''
nameserver 0.0.0.0
'';
};
in
''
export NIX_REDIRECTS="/etc/resolv.conf=${fakeResolvConf}"
export TUWUNEL_DATABASE_PATH="$(mktemp -d)/smoketest.db"
'';
doCheck = true;
doBenchmark = false;
cargoExtraArgs = "--no-default-features --locked "
-1
View File
@@ -5,7 +5,6 @@ After=network-online.target
Documentation=https://tuwunel.chat/
[Service]
DynamicUser=yes
User=tuwunel
Group=tuwunel
Type=notify
+1 -1
View File
@@ -9,7 +9,7 @@
# If you're having trouble making the relevant changes, bug a maintainer.
[toolchain]
channel = "1.88.0"
channel = "1.89.0"
profile = "minimal"
components = [
# For rust-analyzer
+5 -3
View File
@@ -2,6 +2,7 @@
use futures::StreamExt;
use ruma::{OwnedRoomId, OwnedUserId};
use tuwunel_core::Result;
use tuwunel_database::Deserialized;
use crate::{admin_command, admin_command_dispatch};
@@ -46,7 +47,7 @@ async fn changes_since(
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"))
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:?}\n```"))
.await
}
@@ -58,11 +59,12 @@ async fn account_data_get(
room_id: Option<OwnedRoomId>,
) -> Result {
let timer = tokio::time::Instant::now();
let results = self
let results: serde_json::Value = self
.services
.account_data
.get_raw(room_id.as_deref(), &user_id, &kind)
.await;
.await
.deserialized()?;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"))
+42 -10
View File
@@ -37,15 +37,6 @@ pub(crate) enum RawCommand {
base64: bool,
},
/// - Raw database delete (for string keys)
Del {
/// Map name
map: String,
/// Key
key: String,
},
/// - Raw database keys iteration
Keys {
/// Map name
@@ -135,7 +126,26 @@ pub(crate) enum RawCommand {
prefix: Option<String>,
},
/// - Compact database
/// - Raw database delete (for string keys) DANGER!!!
Del {
/// Map name
map: String,
/// Key
key: String,
},
/// - Clear database table DANGER!!!
Clear {
/// Map name
map: String,
/// Confirm
#[arg(long)]
confirm: bool,
},
/// - Compact database DANGER!!!
Compact {
#[arg(short, long, alias("column"))]
map: Option<Vec<String>>,
@@ -425,6 +435,28 @@ pub(super) async fn raw_del(&self, map: String, key: String) -> Result {
.await
}
#[admin_command]
pub(super) async fn raw_clear(&self, map: String, confirm: bool) -> Result {
let map = self.services.db.get(&map)?;
if !confirm {
return Err!("Are you really sure you want to clear all data? Add the --confirm option.");
}
let timer = Instant::now();
let cork = self.services.db.cork();
map.raw_keys()
.ignore_err()
.ready_for_each(|key| map.remove(&key))
.boxed()
.await;
drop(cork);
let query_time = timer.elapsed();
self.write_str(&format!("Operation completed in {query_time:?}"))
.await
}
#[admin_command]
pub(super) async fn raw_get(&self, map: String, key: String, base64: bool) -> Result {
let map = self.services.db.get(&map)?;
+14
View File
@@ -8,6 +8,13 @@
#[derive(Debug, Subcommand)]
/// All the getters and iterators from src/database/key_value/rooms/alias.rs
pub(crate) enum RoomAliasCommand {
/// - Resolve any local or remote alias.
ResolveAlias {
/// Full room alias
alias: OwnedRoomAliasId,
},
/// - Resolve an alias on this server.
ResolveLocalAlias {
/// Full room alias
alias: OwnedRoomAliasId,
@@ -28,6 +35,13 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Context<'_>)
let services = context.services;
match subcommand {
| RoomAliasCommand::ResolveAlias { alias } => {
let timer = tokio::time::Instant::now();
let results = services.alias.resolve_alias(&alias).await;
let query_time = timer.elapsed();
write!(context, "Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")
},
| RoomAliasCommand::ResolveLocalAlias { alias } => {
let timer = tokio::time::Instant::now();
let results = services.alias.resolve_local_alias(&alias).await;
+14 -7
View File
@@ -29,7 +29,7 @@ pub(crate) enum SyncCommand {
#[admin_command]
pub(super) async fn list_connections(&self) -> Result {
let connections = self.services.sync.list_connections();
let connections = self.services.sync.list_loaded_connections().await;
for connection_key in connections {
self.write_str(&format!("{connection_key:?}\n"))
@@ -47,7 +47,11 @@ pub(super) async fn show_connection(
conn_id: Option<String>,
) -> Result {
let key = into_connection_key(user_id, device_id, conn_id);
let cache = self.services.sync.find_connection(&key)?;
let cache = self
.services
.sync
.get_loaded_connection(&key)
.await?;
let out;
{
@@ -65,11 +69,14 @@ pub(super) async fn drop_connections(
device_id: Option<OwnedDeviceId>,
conn_id: Option<String>,
) -> Result {
self.services.sync.clear_connections(
user_id.as_deref(),
device_id.as_deref(),
conn_id.map(Into::into).as_ref(),
);
self.services
.sync
.clear_connections(
user_id.as_deref(),
device_id.as_deref(),
conn_id.map(Into::into).as_ref(),
)
.await;
Ok(())
}
+20 -1
View File
@@ -2,7 +2,7 @@
use futures::{FutureExt, StreamExt};
use ruma::{
Int, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
Int, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
events::{
RoomAccountDataEventType, StateEventType,
room::{
@@ -224,6 +224,25 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
.await
}
#[admin_command]
pub(super) async fn delete_device(
&self,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
) -> Result {
if !self.services.globals.user_is_local(&user_id) {
return Err!("Cannot delete device of remote user");
}
self.services
.users
.remove_device(&user_id, &device_id)
.await;
self.write_str(&format!("User {user_id}'s device {device_id} removed."))
.await
}
#[admin_command]
pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> Result {
let user_id = parse_local_user_id(self.services, &username)?;
+7 -1
View File
@@ -1,7 +1,7 @@
mod commands;
use clap::Subcommand;
use ruma::{OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId};
use ruma::{OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId};
use tuwunel_core::Result;
use crate::admin_command_dispatch;
@@ -59,6 +59,12 @@ pub(super) enum UserCommand {
force: bool,
},
/// - Deletes a user's device.
DeleteDevice {
user_id: OwnedUserId,
device_id: OwnedDeviceId,
},
/// - List local users in the database
#[clap(alias = "list")]
ListUsers,
+132
View File
@@ -0,0 +1,132 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::StreamExt;
use ruma::api::client::dehydrated_device::{
delete_dehydrated_device::unstable as delete_dehydrated_device,
get_dehydrated_device::unstable as get_dehydrated_device, get_events::unstable as get_events,
put_dehydrated_device::unstable as put_dehydrated_device,
};
use tuwunel_core::{Err, Result, at, utils::result::IsErrOr};
use crate::Ruma;
const MAX_BATCH_EVENTS: usize = 50;
/// # `PUT /_matrix/client/../dehydrated_device`
///
/// Creates or overwrites the user's dehydrated device.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn put_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<put_dehydrated_device::Request>,
) -> Result<put_dehydrated_device::Response> {
let sender_user = body
.sender_user
.as_deref()
.expect("AccessToken authentication required");
let device_id = body.body.device_id.clone();
services
.users
.set_dehydrated_device(sender_user, body.body)
.await?;
Ok(put_dehydrated_device::Response { device_id })
}
/// # `DELETE /_matrix/client/../dehydrated_device`
///
/// Deletes the user's dehydrated device without replacement.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn delete_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<delete_dehydrated_device::Request>,
) -> Result<delete_dehydrated_device::Response> {
let sender_user = body.sender_user();
let device_id = services
.users
.get_dehydrated_device_id(sender_user)
.await?;
services
.users
.remove_device(sender_user, &device_id)
.await;
Ok(delete_dehydrated_device::Response { device_id })
}
/// # `GET /_matrix/client/../dehydrated_device`
///
/// Gets the user's dehydrated device
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn get_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_dehydrated_device::Request>,
) -> Result<get_dehydrated_device::Response> {
let sender_user = body.sender_user();
let device = services
.users
.get_dehydrated_device(sender_user)
.await?;
Ok(get_dehydrated_device::Response {
device_id: device.device_id,
device_data: device.device_data,
})
}
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
///
/// Paginates the events of the dehydrated device.
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn get_dehydrated_events_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_events::Request>,
) -> Result<get_events::Response> {
let sender_user = body.sender_user();
let device_id = &body.body.device_id;
let existing_id = services
.users
.get_dehydrated_device_id(sender_user)
.await;
if existing_id
.as_ref()
.is_err_or(|existing_id| existing_id != device_id)
{
return Err!(Request(Forbidden("Not the dehydrated device_id.")));
}
let since: Option<u64> = body
.body
.next_batch
.as_deref()
.map(str::parse)
.transpose()?;
let mut next_batch: Option<u64> = None;
let events = services
.users
.get_to_device_events(sender_user, device_id, since, None)
.take(MAX_BATCH_EVENTS)
.inspect(|&(count, _)| {
next_batch.replace(count);
})
.map(at!(1))
.collect()
.await;
Ok(get_events::Response {
events,
next_batch: next_batch.as_ref().map(ToString::to_string),
})
}
+9 -5
View File
@@ -7,7 +7,7 @@
self, delete_device, delete_devices, get_device, get_devices, update_device,
},
};
use tuwunel_core::{Err, Result, debug, err, utils};
use tuwunel_core::{Err, Result, debug, err, utils, utils::string::to_small_string};
use crate::{Ruma, client::DEVICE_ID_LENGTH, router::auth_uiaa};
@@ -61,18 +61,21 @@ pub(crate) async fn update_device_route(
.await
{
| Ok(mut device) => {
let notify = device.display_name != body.display_name;
device.display_name.clone_from(&body.display_name);
device
.last_seen_ip
.clone_from(&Some(client.to_string()));
.clone_from(&Some(to_small_string(client)));
device
.last_seen_ts
.clone_from(&Some(MilliSecondsSinceUnixEpoch::now()));
assert_eq!(device.device_id, body.device_id, "device_id mismatch");
services
.users
.update_device_metadata(sender_user, &body.device_id, &device)
.await?;
.put_device_metadata(sender_user, notify, &device);
Ok(update_device::v3::Response {})
},
@@ -80,6 +83,7 @@ pub(crate) async fn update_device_route(
let Some(appservice) = appservice else {
return Err!(Request(NotFound("Device not found.")));
};
if !appservice.registration.device_management {
return Err!(Request(NotFound("Device not found.")));
}
@@ -97,7 +101,7 @@ pub(crate) async fn update_device_route(
.create_device(
sender_user,
&device_id,
(&appservice.registration.as_token, None),
(Some(&appservice.registration.as_token), None),
None,
None,
Some(client.to_string()),
+116
View File
@@ -0,0 +1,116 @@
use std::iter::once;
use axum::extract::State;
use futures::StreamExt;
use ruma::api::client::peeking::listen_to_new_events::v3::{Request, Response};
use tokio::time::{Duration, Instant, timeout_at};
use tuwunel_core::{
Err, Event, Result, at,
matrix::PduCount,
utils::{
BoolExt,
result::FlatOk,
stream::{IterStream, ReadyExt},
},
};
use crate::Ruma;
const EVENT_LIMIT: usize = 50;
/// GET `/_matrix/client/v3/events`
pub(crate) async fn events_route(
State(services): State<crate::State>,
body: Ruma<Request>,
) -> Result<Response> {
let (sender_user, sender_device) = body.sender();
let from = body
.body
.from
.as_deref()
.map(str::parse)
.flat_ok()
.unwrap_or_default();
let timeout = body
.body
.timeout
.as_ref()
.map(Duration::as_millis)
.map(TryInto::try_into)
.flat_ok()
.unwrap_or(services.config.client_sync_timeout_default)
.max(services.config.client_sync_timeout_min)
.min(services.config.client_sync_timeout_max);
let Some(room_id) = body.room_id.as_deref() else {
//TODO: upgrade ruma
return Err!(Request(InvalidParam("Missing RoomId parameter.")));
};
if !services
.state_accessor
.user_can_see_state_events(sender_user, room_id)
.await
{
return Err!(Request(Forbidden("No room preview available.")));
}
let stop_at = Instant::now()
.checked_add(Duration::from_millis(timeout))
.expect("configuration must limit maximum timeout");
loop {
let watchers = services
.sync
.watch(sender_user, sender_device, once(room_id).stream());
let next_batch = services.globals.wait_pending().await?;
let events = services
.timeline
.pdus(Some(sender_user), room_id, Some(PduCount::Normal(from)))
.ready_filter_map(Result::ok)
.ready_take_while(|(count, _)| PduCount::Normal(next_batch).ge(count))
.take(EVENT_LIMIT)
.collect::<Vec<_>>()
.await;
if !events.is_empty() {
return Ok(Response {
start: events
.first()
.map(at!(0))
.as_ref()
.map(ToString::to_string),
end: events
.last()
.map(at!(0))
.as_ref()
.map(ToString::to_string),
chunk: events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.collect(),
});
}
if timeout_at(stop_at, watchers).await.is_err() || services.server.is_stopping() {
return Ok(Response {
chunk: Default::default(),
start: body.body.from,
end: services
.server
.is_stopping()
.is_false()
.then_some(next_batch)
.as_ref()
.map(ToString::to_string),
});
}
}
}
+11 -22
View File
@@ -37,30 +37,16 @@ pub(crate) async fn upload_keys_route(
) -> Result<upload_keys::v3::Response> {
let (sender_user, sender_device) = body.sender();
for (key_id, one_time_key) in body
let one_time_keys = body
.one_time_keys
.iter()
.take(services.config.one_time_key_limit)
{
if one_time_key
.deserialize()
.inspect_err(|e| {
debug_warn!(
?key_id,
?one_time_key,
"Invalid one time key JSON submitted by client, skipping: {e}"
);
})
.is_err()
{
continue;
}
.map(|(id, val)| (id.as_ref(), val));
services
.users
.add_one_time_key(sender_user, sender_device, key_id, one_time_key)
.await?;
}
services
.users
.add_one_time_keys(sender_user, sender_device, one_time_keys)
.await?;
if let Some(device_keys) = &body.device_keys {
let deser_device_keys = device_keys.deserialize().map_err(|e| {
@@ -85,8 +71,11 @@ pub(crate) async fn upload_keys_route(
.users
.get_device_keys(sender_user, sender_device)
.await
.and_then(|keys| keys.deserialize().map_err(Into::into))
{
if existing_keys.json().get() == device_keys.json().get() {
// NOTE: also serves as a workaround for a nheko bug which omits cross-signing
// NOTE: signatures when re-uploading the same DeviceKeys.
if existing_keys.keys == deser_device_keys.keys {
debug!(
?sender_user,
?sender_device,
@@ -585,7 +574,7 @@ fn add_unsigned_device_display_name(
.or_insert_with(|| CanonicalJsonObject::default().into())
{
let display_name = if include_display_names {
CanonicalJsonValue::String(display_name)
CanonicalJsonValue::String(display_name.to_string())
} else {
CanonicalJsonValue::String(metadata.device_id.into())
};
+8 -8
View File
@@ -210,11 +210,11 @@ async fn knock_room_helper_local(
})?;
knock_event_stub.insert(
"origin".to_owned(),
"origin".into(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
knock_event_stub.insert(
"origin_server_ts".to_owned(),
"origin_server_ts".into(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
@@ -222,7 +222,7 @@ async fn knock_room_helper_local(
),
);
knock_event_stub.insert(
"content".to_owned(),
"content".into(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
@@ -244,7 +244,7 @@ async fn knock_room_helper_local(
// Add event_id
knock_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let knock_event = knock_event_stub;
@@ -346,11 +346,11 @@ async fn knock_room_helper_remote(
})?;
knock_event_stub.insert(
"origin".to_owned(),
"origin".into(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
knock_event_stub.insert(
"origin_server_ts".to_owned(),
"origin_server_ts".into(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
@@ -358,7 +358,7 @@ async fn knock_room_helper_remote(
),
);
knock_event_stub.insert(
"content".to_owned(),
"content".into(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
@@ -380,7 +380,7 @@ async fn knock_room_helper_remote(
// Add event_id
knock_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let knock_event = knock_event_stub;
+2 -4
View File
@@ -195,12 +195,10 @@ async fn get_join_params(
);
// shuffle additionals, append to base servers
additional_servers.sort_unstable();
additional_servers.dedup();
shuffle(&mut additional_servers);
servers.append(&mut additional_servers);
servers.sort_unstable();
servers.dedup();
servers.append(&mut additional_servers);
shuffle(&mut servers);
// sort deprioritized servers last
servers.sort_by(|a, b| {
+4
View File
@@ -5,8 +5,10 @@
pub(super) mod backup;
pub(super) mod capabilities;
pub(super) mod context;
pub(super) mod dehydrated_device;
pub(super) mod device;
pub(super) mod directory;
pub(super) mod events;
pub(super) mod filter;
pub(super) mod keys;
pub(super) mod media;
@@ -49,8 +51,10 @@
pub(super) use backup::*;
pub(super) use capabilities::*;
pub(super) use context::*;
pub(super) use dehydrated_device::*;
pub(super) use device::*;
pub(super) use directory::*;
pub(super) use events::*;
pub(super) use filter::*;
pub(super) use keys::*;
pub(super) use media::*;
+2 -2
View File
@@ -49,7 +49,7 @@ pub(crate) async fn set_displayname_route(
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await?;
Ok(set_display_name::v3::Response {})
@@ -149,7 +149,7 @@ pub(crate) async fn set_avatar_url_route(
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await
.ok();
+108 -5
View File
@@ -1,10 +1,11 @@
use axum::extract::State;
use futures::StreamExt;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue,
CanonicalJsonObject, CanonicalJsonValue, MilliSecondsSinceUnixEpoch,
api::client::{
error::ErrorKind,
push::{
delete_pushrule, get_pushers, get_pushrule, get_pushrule_actions,
delete_pushrule, get_notifications, get_pushers, get_pushrule, get_pushrule_actions,
get_pushrule_enabled, get_pushrules_all, get_pushrules_global_scope, set_pusher,
set_pushrule, set_pushrule_actions, set_pushrule_enabled,
},
@@ -14,15 +15,117 @@
push_rules::{PushRulesEvent, PushRulesEventContent},
},
push::{
InsertPushRuleError, PredefinedContentRuleId, PredefinedOverrideRuleId,
Action, InsertPushRuleError, PredefinedContentRuleId, PredefinedOverrideRuleId,
RemovePushRuleError, Ruleset,
},
};
use tuwunel_core::{Err, Error, Result, err};
use tuwunel_core::{
Err, Error, Result, at, err,
matrix::{Event, PduId},
utils::{
stream::{ReadyExt, WidebandExt},
string::to_small_string,
},
};
use tuwunel_service::Services;
use crate::Ruma;
/// # `GET /_matrix/client/r0/notifications/`
///
/// Paginate through the list of events the user has been, or would have been
/// notified about.
pub(crate) async fn get_notifications_route(
State(services): State<crate::State>,
body: Ruma<get_notifications::v3::Request>,
) -> Result<get_notifications::v3::Response> {
use get_notifications::v3::Notification;
let sender_user = body.sender_user();
let from = body
.body
.from
.as_deref()
.map(str::parse)
.transpose()
.map_err(|e| err!(Request(InvalidParam("Invalid `from' parameter: {e}"))))?;
let limit: usize = body
.body
.limit
.map(TryInto::try_into)
.transpose()?
.unwrap_or(50)
.clamp(1, 100);
let only_highlight = body
.body
.only
.as_deref()
.is_some_and(|only| only.contains("highlight"));
let mut next_token: Option<u64> = None;
let notifications = services
.pusher
.get_notifications(sender_user, from)
.ready_filter(|(_, notify)| {
if only_highlight && !notify.actions.iter().any(Action::is_highlight) {
return false;
}
true
})
.wide_filter_map(async |(count, notify)| {
let pdu_id = PduId {
shortroomid: notify.sroomid,
count: count.into(),
};
let event = services
.timeline
.get_pdu_from_id(&pdu_id.into())
.await
.ok()
.filter(|event| !event.is_redacted())?;
let read = services
.pusher
.last_notification_read(sender_user, event.room_id())
.await
.is_ok_and(|last_read| last_read.ge(&count));
let ts = notify
.ts
.try_into()
.map(MilliSecondsSinceUnixEpoch)
.ok()?;
let notification = Notification {
room_id: event.room_id().into(),
event: event.into_format(),
ts,
read,
profile_tag: notify.tag,
actions: notify.actions,
};
Some((count, notification))
})
.take(limit)
.inspect(|(count, _)| {
next_token.replace(*count);
})
.map(at!(1))
.collect::<Vec<_>>()
.await;
Ok(get_notifications::v3::Response {
next_token: next_token.map(to_small_string),
notifications,
})
}
/// # `GET /_matrix/client/r0/pushrules/`
///
/// Retrieves the push rules event for this user.
@@ -335,7 +438,7 @@ pub(crate) async fn set_pushrule_actions_route(
if account_data
.content
.global
.set_actions(body.kind.clone(), &body.rule_id, body.actions.clone())
.set_actions(body.kind.clone(), &body.rule_id, body.actions.clone().into())
.is_err()
{
return Err!(Request(NotFound("Push rule not found.")));
+42 -31
View File
@@ -26,6 +26,12 @@ pub(crate) async fn set_read_marker_route(
) -> Result<set_read_marker::v3::Response> {
let sender_user = body.sender_user();
if body.private_read_receipt.is_some() || body.read_receipt.is_some() {
services
.pusher
.reset_notification_counts(sender_user, &body.room_id);
}
if let Some(event) = &body.fully_read {
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
content: ruma::events::fully_read::FullyReadEventContent { event_id: event.clone() },
@@ -39,21 +45,29 @@ pub(crate) async fn set_read_marker_route(
RoomAccountDataEventType::FullyRead,
&serde_json::to_value(fully_read_event)?,
)
.await?;
.await
.ok();
}
if body.private_read_receipt.is_some() || body.read_receipt.is_some() {
if let Some(event) = &body.private_read_receipt {
let count = services
.timeline
.get_pdu_count(event)
.await
.map_err(|_| err!(Request(NotFound("Event not found."))))?;
let PduCount::Normal(count) = count else {
return Err!(Request(InvalidParam(
"Event is a backfilled PDU and cannot be marked as read."
)));
};
services
.user
.reset_notification_counts(sender_user, &body.room_id);
.read_receipt
.private_read_set(&body.room_id, sender_user, count);
}
if let Some(event) = &body.read_receipt {
services
.presence
.maybe_ping_presence(sender_user, &ruma::presence::PresenceState::Online)
.await?;
let receipt_content = BTreeMap::from_iter([(
event.to_owned(),
BTreeMap::from_iter([(
@@ -76,24 +90,16 @@ pub(crate) async fn set_read_marker_route(
},
)
.await;
}
if let Some(event) = &body.private_read_receipt {
let count = services
.timeline
.get_pdu_count(event)
.await
.map_err(|_| err!(Request(NotFound("Event not found."))))?;
let PduCount::Normal(count) = count else {
return Err!(Request(InvalidParam(
"Event is a backfilled PDU and cannot be marked as read."
)));
};
services
.read_receipt
.private_read_set(&body.room_id, sender_user, count);
.presence
.maybe_ping_presence(
sender_user,
body.sender_device.as_deref(),
&ruma::presence::PresenceState::Online,
)
.await
.ok();
}
Ok(set_read_marker::v3::Response {})
@@ -113,7 +119,7 @@ pub(crate) async fn create_receipt_route(
create_receipt::v3::ReceiptType::Read | create_receipt::v3::ReceiptType::ReadPrivate
) {
services
.user
.pusher
.reset_notification_counts(sender_user, &body.room_id);
}
@@ -135,11 +141,6 @@ pub(crate) async fn create_receipt_route(
.await?;
},
| create_receipt::v3::ReceiptType::Read => {
services
.presence
.maybe_ping_presence(sender_user, &ruma::presence::PresenceState::Online)
.await?;
let receipt_content = BTreeMap::from_iter([(
body.event_id.clone(),
BTreeMap::from_iter([(
@@ -165,6 +166,16 @@ pub(crate) async fn create_receipt_route(
},
)
.await;
services
.presence
.maybe_ping_presence(
sender_user,
body.sender_device.as_deref(),
&ruma::presence::PresenceState::Online,
)
.await
.ok();
},
| create_receipt::v3::ReceiptType::ReadPrivate => {
let count = services
+23 -17
View File
@@ -16,7 +16,9 @@
events::GlobalAccountDataEventType,
push,
};
use tuwunel_core::{Err, Error, Result, debug_info, error, info, is_equal_to, utils, warn};
use tuwunel_core::{
Err, Error, Result, debug_info, debug_warn, error, info, is_equal_to, utils, warn,
};
use tuwunel_service::users::device::generate_refresh_token;
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH};
@@ -181,31 +183,33 @@ pub(crate) async fn register_route(
return Err!(Request(Forbidden("Registration has been disabled.")));
}
if is_guest
&& (!services.config.allow_guest_registration
|| (services.config.allow_registration
&& services.globals.registration_token.is_some()))
{
info!(
if is_guest && !services.config.allow_guest_registration {
let display_name = body
.initial_device_display_name
.as_deref()
.unwrap_or("");
debug_warn!(
"Guest registration disabled / registration enabled with token configured, \
rejecting guest registration attempt, initial device name: \"{}\"",
body.initial_device_display_name
.as_deref()
.unwrap_or("")
rejecting guest registration attempt, initial device name: \"{display_name}\""
);
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
}
// forbid guests from registering if there is not a real admin user yet. give
// generic user error.
if is_guest && services.users.count().await < 2 {
let display_name = body
.initial_device_display_name
.as_deref()
.unwrap_or("");
warn!(
"Guest account attempted to register before a real admin user has been registered, \
rejecting registration. Guest's initial device name: \"{}\"",
body.initial_device_display_name
.as_deref()
.unwrap_or("")
rejecting registration. Guest's initial device name: \"{display_name}\""
);
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
}
@@ -309,7 +313,7 @@ pub(crate) async fn register_route(
// UIAA
let mut uiaainfo;
let skip_auth = if services.globals.registration_token.is_some() {
let skip_auth = if services.globals.registration_token.is_some() && !is_guest {
// Registration token required
uiaainfo = UiaaInfo {
flows: vec![AuthFlow {
@@ -320,6 +324,7 @@ pub(crate) async fn register_route(
session: None,
auth_error: None,
};
body.appservice_info.is_some()
} else {
// No registration token necessary, but clients must still go through the flow
@@ -330,6 +335,7 @@ pub(crate) async fn register_route(
session: None,
auth_error: None,
};
body.appservice_info.is_some() || is_guest
};
@@ -446,7 +452,7 @@ pub(crate) async fn register_route(
.create_device(
&user_id,
&device_id,
(&access_token, expires_in),
(Some(&access_token), expires_in),
refresh_token.as_deref(),
body.initial_device_display_name.clone(),
Some(client.to_string()),
+5 -7
View File
@@ -154,13 +154,11 @@ async fn paginate_relations_with_filter(
.collect()
.await;
let next_batch = match dir {
| Direction::Forward => events.last(),
| Direction::Backward => events.first(),
}
.map(at!(0))
.as_ref()
.map(ToString::to_string);
let next_batch = events
.last()
.map(at!(0))
.as_ref()
.map(ToString::to_string);
Ok(get_relating_events::v1::Response {
next_batch,
+35 -1
View File
@@ -3,7 +3,8 @@
use axum::extract::State;
use futures::{FutureExt, future::OptionFuture};
use ruma::{
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
CanonicalJsonObject, EventEncryptionAlgorithm, Int, OwnedRoomAliasId, OwnedRoomId,
OwnedUserId, RoomId, RoomVersionId,
api::client::room::{
self, create_room,
create_room::v3::{CreationContent, RoomPreset},
@@ -13,6 +14,7 @@
room::{
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
encryption::RoomEncryptionEventContent,
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent},
@@ -262,6 +264,7 @@ pub(crate) async fn create_room_route(
.await?;
// 6. Events listed in initial_state
let mut is_encrypted = false;
for event in &body.initial_state {
let mut pdu_builder = event
.deserialize_as_unchecked::<PduBuilder>()
@@ -292,6 +295,10 @@ pub(crate) async fn create_room_route(
continue;
}
if pdu_builder.event_type == TimelineEventType::RoomEncryption {
is_encrypted = true;
}
services
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
@@ -299,6 +306,33 @@ pub(crate) async fn create_room_route(
.await?;
}
if services.config.allow_encryption && !is_encrypted {
use RoomPreset::*;
let config = services
.config
.encryption_enabled_by_default_for_room_type
.as_deref()
.unwrap_or("off");
let invite = matches!(config, "invite");
let always = matches!(config, "all" | "invite");
if always || (invite && matches!(preset, PrivateChat | TrustedPrivateChat)) {
let algorithm = EventEncryptionAlgorithm::MegolmV1AesSha2;
let content = RoomEncryptionEventContent::new(algorithm);
services
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &content),
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
}
}
// 7. Events implied by name and topic
if let Some(name) = &body.name {
services
+53 -36
View File
@@ -1,15 +1,20 @@
use axum::extract::State;
use futures::{FutureExt, TryStreamExt, future::try_join4};
use ruma::api::client::room::initial_sync::v3::{PaginationChunk, Request, Response};
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::try_join5};
use ruma::{
api::client::room::initial_sync::v3::{PaginationChunk, Request, Response},
events::AnyRawAccountDataEvent,
};
use tuwunel_core::{
Err, Event, Result, at,
utils::{BoolExt, stream::TryTools},
Err, Event, Result, at, extract_variant,
matrix::PduCount,
utils::stream::{ReadyExt, TryTools},
};
use crate::Ruma;
const LIMIT_MAX: usize = 100;
const LIMIT_MAX: usize = 50;
/// GET `/_matrix/client/v3/rooms/{roomId}/initialSync`
pub(crate) async fn room_initial_sync_route(
State(services): State<crate::State>,
body: Ruma<Request>,
@@ -24,13 +29,15 @@ pub(crate) async fn room_initial_sync_route(
return Err!(Request(Forbidden("No room preview available.")));
}
let next_batch = services.globals.current_count();
let visibility = services.directory.visibility(room_id).map(Ok);
let membership = services
.state_cache
.user_membership(body.sender_user(), room_id)
.map(Ok);
let visibility = services.directory.visibility(room_id).map(Ok);
let state = services
.state_accessor
.room_state_full_pdus(room_id)
@@ -40,42 +47,52 @@ pub(crate) async fn room_initial_sync_route(
let limit = LIMIT_MAX;
let events = services
.timeline
.pdus_rev(None, room_id, None)
.pdus_rev(None, room_id, Some(PduCount::Normal(next_batch).saturating_add(1)))
.try_take(limit)
.try_collect::<Vec<_>>();
.try_collect()
.map_ok(|mut vec: Vec<_>| {
vec.reverse();
vec
});
let (membership, visibility, state, events) =
try_join4(membership, visibility, state, events)
let account_data = services
.account_data
.changes_since(Some(room_id), body.sender_user(), 0, Some(next_batch))
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
.collect::<Vec<_>>()
.map(Ok);
let (membership, visibility, state, events, account_data) =
try_join5(membership, visibility, state, events, account_data)
.boxed()
.await?;
let messages = PaginationChunk {
start: events
.last()
.map(at!(0))
.as_ref()
.map(ToString::to_string),
end: events
.first()
.map(at!(0))
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
chunk: events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.collect(),
};
Ok(Response {
room_id: room_id.to_owned(),
account_data: None,
state: state.into(),
messages: messages.chunk.is_empty().or_some(messages),
visibility: visibility.into(),
membership,
visibility: visibility.into(),
account_data: Some(account_data),
state: state.into(),
messages: PaginationChunk {
start: events
.first()
.map(at!(0))
.as_ref()
.map(ToString::to_string),
end: events
.last()
.map(at!(0))
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
chunk: events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.collect(),
}
.into(),
})
}
+1 -1
View File
@@ -116,7 +116,7 @@ pub(crate) async fn login_route(
.create_device(
&user_id,
&device_id,
(&access_token, expires_in),
(Some(&access_token), expires_in),
refresh_token.as_deref(),
body.initial_device_display_name.clone(),
Some(client.to_string()),
+5 -3
View File
@@ -1,7 +1,7 @@
mod v3;
mod v5;
use futures::{StreamExt, pin_mut};
use futures::{FutureExt, StreamExt, pin_mut};
use ruma::{RoomId, UserId};
use tuwunel_core::{
Error, PduCount, Result,
@@ -42,10 +42,12 @@ async fn load_timeline(
.by_ref()
.take(limit)
.collect()
.map(|mut pdus: Vec<_>| {
pdus.reverse();
pdus
})
.await;
let timeline_pdus: Vec<_> = timeline_pdus.into_iter().rev().collect();
// They /sync response doesn't always return all messages, so we say the output
// is limited unless there are events in non_timeline_pdus
let limited = non_timeline_pdus.next().await.is_some();
+212 -147
View File
@@ -10,7 +10,7 @@
pin_mut,
};
use ruma::{
DeviceId, EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
DeviceId, EventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
api::client::{
filter::FilterDefinition,
sync::sync_events::{
@@ -21,7 +21,6 @@
State as RoomState, StateEvents, Timeline, ToDevice,
},
},
uiaa::UiaaResponse,
},
events::{
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType,
@@ -38,8 +37,8 @@
use tuwunel_core::{
Error, Result, at,
debug::INFO_SPAN_LEVEL,
err, error,
error::inspect_debug_log,
err,
error::{inspect_debug_log, inspect_log},
extract_variant, is_equal_to,
matrix::{
Event,
@@ -53,10 +52,10 @@
self, BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt,
future::{OptionStream, ReadyEqExt},
math::ruma_from_u64,
result::LogErr,
result::MapExpect,
stream::{BroadbandExt, Tools, TryExpect, WidebandExt},
string::to_small_string,
},
warn,
};
use tuwunel_service::{
Services,
@@ -68,7 +67,7 @@
};
use super::{load_timeline, share_encrypted_room};
use crate::{Ruma, RumaResponse, client::ignored_filter};
use crate::{Ruma, client::ignored_filter};
#[derive(Default)]
struct StateChanges {
@@ -126,18 +125,36 @@ struct StateChanges {
pub(crate) async fn sync_events_route(
State(services): State<crate::State>,
body: Ruma<sync_events::v3::Request>,
) -> Result<sync_events::v3::Response, RumaResponse<UiaaResponse>> {
) -> Result<sync_events::v3::Response> {
let (sender_user, sender_device) = body.sender();
services
let filter: OptionFuture<_> = body
.body
.filter
.as_ref()
.map(async |filter| match filter {
| Filter::FilterDefinition(filter) => filter.clone(),
| Filter::FilterId(filter_id) => services
.users
.get_filter(sender_user, filter_id)
.await
.unwrap_or_default(),
})
.into();
let filter = filter.map(Option::unwrap_or_default);
let full_state = body.body.full_state;
let set_presence = &body.body.set_presence;
let ping_presence = services
.presence
.maybe_ping_presence(sender_user, &body.body.set_presence)
.await
.log_err()
.maybe_ping_presence(sender_user, body.sender_device.as_deref(), set_presence)
.inspect_err(inspect_log)
.ok();
// Record user as actively syncing for push suppression heuristic.
services.presence.note_sync(sender_user).await;
let note_sync = services.presence.note_sync(sender_user);
let (filter, ..) = join3(filter, ping_presence, note_sync).await;
let mut since = body
.body
@@ -163,7 +180,11 @@ pub(crate) async fn sync_events_route(
.expect("configuration must limit maximum timeout");
loop {
let watch_rooms = services.state_cache.rooms_joined(sender_user);
let watch_rooms = services
.state_cache
.rooms_joined(sender_user)
.chain(services.state_cache.rooms_invited(sender_user));
let watchers = services
.sync
.watch(sender_user, sender_device, watch_rooms);
@@ -171,22 +192,31 @@ pub(crate) async fn sync_events_route(
let next_batch = services.globals.wait_pending().await?;
debug_assert!(since <= next_batch, "next_batch is monotonic");
if since < next_batch || body.body.full_state {
let response = build_sync_events(&services, &body, since, next_batch).await?;
if since < next_batch || full_state {
let response = build_sync_events(
&services,
body.sender(),
since,
next_batch,
full_state,
&filter,
)
.await?;
let empty = response.rooms.is_empty()
&& response.presence.is_empty()
&& response.account_data.is_empty()
&& response.device_lists.is_empty()
&& response.to_device.is_empty();
if !empty || body.body.full_state {
if !empty || full_state {
return Ok(response);
}
}
// Wait for activity
if time::timeout_at(stop_at, watchers).await.is_err() || services.server.is_stopping() {
let response = build_empty_response(&services, &body, next_batch).await;
let response = build_empty_response(&services, body.sender(), next_batch).await;
trace!(since, next_batch, "empty response");
return Ok(response);
}
@@ -205,16 +235,16 @@ pub(crate) async fn sync_events_route(
async fn build_empty_response(
services: &Services,
body: &Ruma<sync_events::v3::Request>,
(sender_user, sender_device): (&UserId, &DeviceId),
next_batch: u64,
) -> sync_events::v3::Response {
sync_events::v3::Response {
device_one_time_keys_count: services
.users
.count_one_time_keys(body.sender_user(), body.sender_device())
.count_one_time_keys(sender_user, sender_device)
.await,
..sync_events::v3::Response::new(next_batch.to_string())
..sync_events::v3::Response::new(to_small_string(next_batch))
}
}
@@ -231,23 +261,12 @@ async fn build_empty_response(
)]
async fn build_sync_events(
services: &Services,
body: &Ruma<sync_events::v3::Request>,
(sender_user, sender_device): (&UserId, &DeviceId),
since: u64,
next_batch: u64,
full_state: bool,
filter: &FilterDefinition,
) -> Result<sync_events::v3::Response> {
let (sender_user, sender_device) = body.sender();
let full_state = body.body.full_state;
let filter = match body.body.filter.as_ref() {
| None => FilterDefinition::default(),
| Some(Filter::FilterDefinition(filter)) => filter.clone(),
| Some(Filter::FilterId(filter_id)) => services
.users
.get_filter(sender_user, filter_id)
.await
.unwrap_or_default(),
};
let joined_rooms = services
.state_cache
.rooms_joined(sender_user)
@@ -262,7 +281,7 @@ async fn build_sync_events(
since,
next_batch,
full_state,
&filter,
filter,
)
.map_ok(move |(joined_room, dlu, jeu)| (room_id, joined_room, dlu, jeu))
.ok()
@@ -293,7 +312,7 @@ async fn build_sync_events(
sender_user,
next_batch,
full_state,
&filter,
filter,
)
.map_ok(move |left_room| (room_id, left_room))
.ok()
@@ -352,7 +371,7 @@ async fn build_sync_events(
let presence_updates: OptionFuture<_> = services
.config
.allow_local_presence
.then(|| process_presence_updates(services, since, next_batch, sender_user, &filter))
.then(|| process_presence_updates(services, since, next_batch, sender_user, filter))
.into();
let account_data = services
@@ -371,6 +390,7 @@ async fn build_sync_events(
let to_device_events = services
.users
.get_to_device_events(sender_user, sender_device, Some(since), Some(next_batch))
.map(at!(1))
.collect::<Vec<_>>();
let device_one_time_keys_count = services
@@ -437,7 +457,7 @@ async fn build_sync_events(
device_one_time_keys_count,
// Fallback keys are not yet supported
device_unused_fallback_key_types: None,
next_batch: next_batch.to_string(),
next_batch: to_small_string(next_batch),
presence: Presence { events: presence_events },
rooms: Rooms {
leave: left_rooms,
@@ -502,28 +522,20 @@ async fn handle_left_room(
.state_cache
.get_left_count(room_id, sender_user)
.await
.ok();
.unwrap_or(0);
let filter_exclude = filter
.room
.not_rooms
.iter()
.any(is_equal_to!(room_id));
if left_count == 0 || left_count > next_batch {
return Ok(None);
}
let filter_include = filter
.room
.rooms
.as_ref()
.is_some_and(|rooms| rooms.iter().any(is_equal_to!(room_id)));
let include_leave = filter.room.include_leave;
if since == 0 && !include_leave {
return Ok(None);
}
let too_soon = Some(next_batch) < left_count;
let too_late = Some(since) >= left_count;
let initial_sync = since == 0;
let include_leave =
filter.room.include_leave && !filter_exclude && (filter_include || initial_sync);
// Left before last sync or after cutoff for next sync
if (too_late && !include_leave) || too_soon {
// Cannot sync unless the event falls within the snapshot. The room is only
// sync'ed once to the client, after that it's too late.
if since != 0 && left_count <= since {
return Ok(None);
}
@@ -535,125 +547,176 @@ async fn handle_left_room(
pin_mut!(is_not_found, is_disabled, is_banned);
if is_not_found.or(is_disabled).or(is_banned).await {
// This is just a rejected invite, not a room we know
// Insert a leave event anyways for the client
// For rejected invites, deleted, missing, or broken room state this is the last
// resort to convey a the minimum of information to the client.
let event = PduEvent {
event_id: EventId::new(services.globals.server_name()),
sender: sender_user.to_owned(),
origin: None,
origin_server_ts: utils::millis_since_unix_epoch().try_into()?,
kind: RoomMember,
content: serde_json::from_str(r#"{"membership":"leave"}"#)?,
state_key: Some(sender_user.as_str().into()),
unsigned: None,
sender: sender_user.to_owned(),
content: serde_json::from_str(r#"{"membership":"leave"}"#)?,
// The following keys are dropped on conversion
room_id: room_id.clone(),
prev_events: Default::default(),
auth_events: Default::default(),
depth: uint!(1),
origin: None,
unsigned: None,
redacts: None,
hashes: EventHash::default(),
auth_events: Default::default(),
prev_events: Default::default(),
signatures: None,
};
return Ok(Some(LeftRoom {
account_data: RoomAccountData { events: Vec::new() },
account_data: RoomAccountData::default(),
state: RoomState::Before(StateEvents { events: vec![event.into_format()] }),
timeline: Timeline {
limited: false,
prev_batch: Some(next_batch.to_string()),
events: Vec::new(),
events: Default::default(),
prev_batch: Some(left_count.to_string()),
},
state: RoomState::Before(StateEvents { events: vec![event.into_format()] }),
}));
}
let mut left_state_events = Vec::new();
load_left_room(services, sender_user, room_id, since, left_count, full_state, filter).await
}
#[tracing::instrument(name = "load", level = "debug", skip_all)]
async fn load_left_room(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
since: u64,
left_count: u64,
full_state: bool,
filter: &FilterDefinition,
) -> Result<Option<LeftRoom>> {
let initial = since == 0;
let timeline_limit: usize = filter
.room
.timeline
.limit
.map(TryInto::try_into)
.map_expect("UInt to usize")
.unwrap_or(10)
.min(100);
let (timeline_pdus, limited, _) = load_timeline(
services,
sender_user,
room_id,
PduCount::Normal(since),
Some(PduCount::Normal(left_count)),
timeline_limit.max(1),
)
.await
.unwrap_or_default();
let since_shortstatehash = services
.timeline
.prev_shortstatehash(room_id, PduCount::Normal(since).saturating_add(1))
.await
.ok();
let since_state_ids: HashMap<_, OwnedEventId> = since_shortstatehash
.map(|since_shortstatehash| {
let horizon_shortstatehash: OptionFuture<_> = timeline_pdus
.first()
.map(at!(0))
.map(|count| {
services
.state_accessor
.state_full_ids(since_shortstatehash)
.timeline
.get_shortstatehash(room_id, count)
.inspect_err(inspect_debug_log)
.ok()
})
.into();
let left_shortstatehash = services
.timeline
.get_shortstatehash(room_id, PduCount::Normal(left_count))
.inspect_err(inspect_debug_log)
.or_else(|_| services.state.get_room_shortstatehash(room_id))
.map_err(|_| err!(Database(error!("Room {room_id} has no state"))));
let (since_shortstatehash, horizon_shortstatehash, left_shortstatehash) =
join3(since_shortstatehash, horizon_shortstatehash, left_shortstatehash)
.boxed()
.await;
let StateChanges { state_events, .. } = calculate_state_changes(
services,
sender_user,
room_id,
full_state || initial,
since_shortstatehash,
horizon_shortstatehash.flatten(),
left_shortstatehash?,
false,
None,
)
.boxed()
.await?;
let is_sender_membership = |event: &PduEvent| {
*event.kind() == RoomMember && event.state_key() == Some(sender_user.as_str())
};
let timeline_sender_member = timeline_limit
.eq(&0)
.then(|| timeline_pdus.last().map(ref_at!(1)).cloned())
.into_iter()
.flat_map(Option::into_iter);
let state_events = state_events
.into_iter()
.filter(|pdu| filter.room.state.matches(pdu))
.filter(|pdu| timeline_limit > 0 || !is_sender_membership(pdu))
.chain(timeline_sender_member)
.map(Event::into_format)
.collect();
let left_prev_batch = timeline_limit
.eq(&0)
.then_some(left_count)
.map(PduCount::Normal);
let prev_batch = timeline_pdus
.first()
.filter(|_| timeline_limit > 0)
.map(at!(0))
.or(left_prev_batch)
.as_ref()
.map(ToString::to_string);
let timeline_events = timeline_pdus
.into_iter()
.stream()
.flatten()
.collect()
.wide_filter_map(|item| ignored_filter(services, item, sender_user))
.map(at!(1))
.ready_filter(|pdu| filter.room.timeline.matches(pdu))
.take(timeline_limit)
.collect::<Vec<_>>();
let account_data_events = services
.account_data
.changes_since(Some(room_id), sender_user, since, None)
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
.collect();
let (account_data_events, timeline_events) = join(account_data_events, timeline_events)
.boxed()
.await;
let Ok(left_event_id): Result<OwnedEventId> = services
.state_accessor
.room_state_get_id(room_id, &StateEventType::RoomMember, sender_user.as_str())
.await
else {
warn!("Left {room_id} but no left state event");
return Ok(None);
};
let Ok(left_shortstatehash) = services
.state
.pdu_shortstatehash(&left_event_id)
.await
else {
warn!(event_id = %left_event_id, "Leave event has no state in {room_id}");
return Ok(None);
};
let mut left_state_ids: HashMap<_, _> = services
.state_accessor
.state_full_ids(left_shortstatehash)
.collect()
.await;
let leave_shortstatekey = services
.short
.get_or_create_shortstatekey(&StateEventType::RoomMember, sender_user.as_str())
.await;
left_state_ids.insert(leave_shortstatekey, left_event_id);
for (shortstatekey, event_id) in left_state_ids {
if full_state || since_state_ids.get(&shortstatekey) != Some(&event_id) {
let (event_type, state_key) = services
.short
.get_statekey_from_short(shortstatekey)
.await?;
if filter.room.state.lazy_load_options.is_enabled()
&& event_type == StateEventType::RoomMember
&& !full_state
&& state_key
.as_str()
.try_into()
.is_ok_and(|user_id: &UserId| sender_user != user_id)
{
continue;
}
let Ok(pdu) = services.timeline.get_pdu(&event_id).await else {
error!("Pdu in state not found: {event_id}");
continue;
};
left_state_events.push(pdu.into_format());
}
}
Ok(Some(LeftRoom {
account_data: RoomAccountData { events: Vec::new() },
account_data: RoomAccountData { events: account_data_events },
state: RoomState::Before(StateEvents { events: state_events }),
timeline: Timeline {
// TODO: support left timeline events so we dont need to set limited to true
limited: true,
prev_batch: Some(next_batch.to_string()),
events: Vec::new(), // and so we dont need to set this to empty vec
prev_batch,
limited: limited || timeline_limit == 0,
events: timeline_events
.into_iter()
.map(Event::into_format)
.collect(),
},
state: RoomState::Before(StateEvents { events: left_state_events }),
}))
}
@@ -804,8 +867,9 @@ async fn load_joined_room(
.is_empty()
.then(|| {
services
.user
.pusher
.last_notification_read(sender_user, room_id)
.ok()
})
.into();
@@ -866,13 +930,14 @@ async fn load_joined_room(
.map(Into::into)
});
let send_notification_counts =
last_notification_read.is_none_or(|last_count| last_count.gt(&since));
let send_notification_counts = last_notification_read
.flatten()
.is_none_or(|last_count| last_count.gt(&since));
let notification_count: OptionFuture<_> = send_notification_counts
.then(|| {
services
.user
.pusher
.notification_count(sender_user, room_id)
.map(TryInto::try_into)
.unwrap_or(uint!(0))
@@ -882,7 +947,7 @@ async fn load_joined_room(
let highlight_count: OptionFuture<_> = send_notification_counts
.then(|| {
services
.user
.pusher
.highlight_count(sender_user, room_id)
.map(TryInto::try_into)
.unwrap_or(uint!(0))
+96 -248
View File
@@ -1,50 +1,35 @@
mod account_data;
mod e2ee;
mod extensions;
mod filter;
mod receipts;
mod room;
mod rooms;
mod selector;
mod to_device;
mod typing;
use std::{collections::BTreeMap, fmt::Debug, time::Duration};
use axum::extract::State;
use futures::{
FutureExt, TryFutureExt, TryStreamExt,
future::{OptionFuture, join, join5, try_join},
FutureExt, TryFutureExt,
future::{join, try_join},
};
use ruma::{
DeviceId, OwnedRoomId, RoomId, UserId,
api::client::sync::sync_events::v5::{
ListId, Request, Response, request::ExtensionRoomConfig, response,
},
DeviceId, OwnedRoomId, UserId,
api::client::sync::sync_events::v5::{ListId, Request, Response, response},
events::room::member::MembershipState,
};
use tokio::time::{Instant, timeout_at};
use tuwunel_core::{
Result, apply, at,
Err, Result, debug,
debug::INFO_SPAN_LEVEL,
err,
debug_warn,
error::inspect_log,
extract_variant,
smallvec::SmallVec,
trace,
utils::{
BoolExt, IterStream, TryFutureExtExt,
result::FlatOk,
stream::{TryBroadbandExt, TryReadyExt},
},
utils::{TryFutureExtExt, result::FlatOk},
};
use tuwunel_service::{
Services,
sync::{Connection, into_connection_key},
};
use self::{
filter::{filter_room, filter_room_meta},
selector::selector,
};
use super::share_encrypted_room;
use crate::Ruma;
@@ -53,7 +38,6 @@ struct SyncInfo<'a> {
services: &'a Services,
sender_user: &'a UserId,
sender_device: &'a DeviceId,
request: &'a Request,
}
#[derive(Clone, Debug)]
@@ -87,63 +71,77 @@ struct WindowRoom {
user_id = %body.sender_user().localpart(),
device_id = %body.sender_device(),
conn_id = ?body.body.conn_id.clone().unwrap_or_default(),
since = ?body.body.pos.clone().or_else(|| body.body.pos_qrs_.clone()).unwrap_or_default(),
since = ?body.body.pos.clone().unwrap_or_default(),
)
)]
pub(crate) async fn sync_events_v5_route(
State(ref services): State<crate::State>,
body: Ruma<Request>,
) -> Result<Response> {
let (sender_user, sender_device) = body.sender();
let request = &body.body;
let since = request
.pos
.as_ref()
.or(request.pos_qrs_.as_ref())
.and_then(|string| string.parse().ok())
.unwrap_or(0);
let (sender_user, sender_device) = body.sender();
let timeout = request
.timeout
.as_ref()
.map(Duration::as_millis)
.map(TryInto::try_into)
.flat_ok()
.map(|timeout: u64| {
timeout
.max(services.config.client_sync_timeout_min)
.min(services.config.client_sync_timeout_max)
})
.unwrap_or(0);
let conn_key = into_connection_key(sender_user, sender_device, request.conn_id.as_deref());
let conn_val = since
.ne(&0)
.then(|| services.sync.find_connection(&conn_key))
.unwrap_or_else(|| Ok(services.sync.init_connection(&conn_key)))
.map_err(|_| err!(Request(UnknownPos("Connection lost; restarting sync stream."))))?;
let conn_val = services
.sync
.load_or_init_connection(&conn_key)
.await;
let conn = conn_val.lock();
let ping_presence = services
.presence
.maybe_ping_presence(sender_user, &request.set_presence)
.maybe_ping_presence(sender_user, Some(sender_device), &request.set_presence)
.inspect_err(inspect_log)
.ok();
let (mut conn, _) = join(conn, ping_presence).await;
if since != 0 && conn.next_batch == 0 {
return Err!(Request(UnknownPos(warn!("Connection lost; restarting sync stream."))));
}
if since == 0 {
*conn = Connection::default();
conn.store(&services.sync, &conn_key);
debug_warn!(?conn_key, "Client cleared cache and reloaded.");
}
let advancing = since == conn.next_batch;
let retarding = since != 0 && since <= conn.globalsince;
if !advancing && !retarding {
return Err!(Request(UnknownPos(warn!(
"Requesting unknown or invalid stream position."
))));
}
debug_assert!(
advancing || retarding,
"Request should either be advancing or replaying the since token."
);
// Update parameters regardless of replay or advance
conn.next_batch = services.globals.wait_pending().await?;
conn.globalsince = since.min(conn.next_batch);
conn.update_cache(request);
conn.globalsince = since;
conn.next_batch = since;
let sync_info = SyncInfo {
services,
sender_user,
sender_device,
request,
};
let timeout = request
.timeout
.as_ref()
.or(request.timeout_qrs_.as_ref())
.map(Duration::as_millis)
.map(TryInto::try_into)
.flat_ok()
.unwrap_or(services.config.client_sync_timeout_default)
.max(services.config.client_sync_timeout_min)
.min(services.config.client_sync_timeout_max);
let stop_at = Instant::now()
.checked_add(Duration::from_millis(timeout))
.expect("configuration must limit maximum timeout");
conn.update_rooms_prologue(retarding.then_some(since));
let mut response = Response {
txn_id: request.txn_id.clone(),
@@ -153,54 +151,66 @@ pub(crate) async fn sync_events_v5_route(
extensions: Default::default(),
};
loop {
let window;
(window, response.lists) = selector(&mut conn, sync_info).boxed().await;
let watch_rooms = window.keys().map(AsRef::as_ref).stream();
let watchers = services
.sync
.watch(sender_user, sender_device, watch_rooms);
let stop_at = Instant::now()
.checked_add(Duration::from_millis(timeout))
.expect("configuration must limit maximum timeout");
conn.next_batch = services.globals.wait_pending().await?;
let sync_info = SyncInfo { services, sender_user, sender_device };
loop {
debug_assert!(
conn.globalsince <= conn.next_batch,
"next_batch should not be greater than since."
"since should not be greater than next_batch."
);
if conn.globalsince < conn.next_batch {
let rooms =
handle_rooms(sync_info, &conn, &window).map_ok(|rooms| response.rooms = rooms);
let window;
let watchers = services.sync.watch(
sender_user,
sender_device,
services.state_cache.rooms_joined(sender_user),
);
let extensions = handle_extensions(sync_info, &conn, &window)
.map_ok(|extensions| response.extensions = extensions);
conn.next_batch = services.globals.wait_pending().await?;
(window, response.lists) = selector::selector(&mut conn, sync_info)
.boxed()
.await;
if conn.globalsince < conn.next_batch {
let rooms = rooms::handle(sync_info, &conn, &window)
.map_ok(|response_rooms| response.rooms = response_rooms);
let extensions = extensions::handle(sync_info, &conn, &window)
.map_ok(|response_extensions| response.extensions = response_extensions);
try_join(rooms, extensions).boxed().await?;
for room_id in window.keys() {
conn.rooms
.entry(room_id.into())
.or_default()
.roomsince = conn.next_batch;
}
conn.update_rooms_epilogue(window.keys().map(AsRef::as_ref));
if !is_empty_response(&response) {
response.pos = conn.next_batch.to_string().into();
trace!(conn.globalsince, conn.next_batch, "response {response:?}");
conn.store(&services.sync, &conn_key);
return Ok(response);
}
}
if timeout_at(stop_at, watchers).await.is_err() || services.server.is_stopping() {
if timeout == 0
|| services.server.is_stopping()
|| timeout_at(stop_at, watchers)
.boxed()
.await
.is_err()
{
response.pos = conn.next_batch.to_string().into();
trace!(conn.globalsince, conn.next_batch, "timeout; empty response");
trace!(conn.globalsince, conn.next_batch, "timeout; empty response {response:?}");
conn.store(&services.sync, &conn_key);
return Ok(response);
}
trace!(
conn.globalsince,
last_batch = ?conn.next_batch,
count = ?services.globals.pending_count(),
stop_at = ?stop_at,
debug!(
?timeout,
last_since = conn.globalsince,
last_batch = conn.next_batch,
pend_count = ?services.globals.pending_count(),
"notified by watcher"
);
@@ -209,167 +219,5 @@ pub(crate) async fn sync_events_v5_route(
}
fn is_empty_response(response: &Response) -> bool {
response.extensions.is_empty()
&& response
.rooms
.iter()
.all(|(_, room)| room.timeline.is_empty() && room.invite_state.is_none())
}
#[tracing::instrument(
name = "rooms",
level = "debug",
skip_all,
fields(
next_batch = conn.next_batch,
window = window.len(),
)
)]
async fn handle_rooms(
sync_info: SyncInfo<'_>,
conn: &Connection,
window: &Window,
) -> Result<BTreeMap<OwnedRoomId, response::Room>> {
window
.iter()
.try_stream()
.broad_and_then(async |(room_id, room)| {
room::handle(sync_info, conn, room)
.map_ok(|room| (room_id, room))
.await
})
.ready_try_filter_map(|(room_id, room)| Ok(room.map(|room| (room_id, room))))
.map_ok(|(room_id, room)| (room_id.to_owned(), room))
.try_collect()
.await
}
#[tracing::instrument(
name = "extensions",
level = "debug",
skip_all,
fields(
next_batch = conn.next_batch,
window = window.len(),
rooms = conn.rooms.len(),
subs = conn.subscriptions.len(),
)
)]
async fn handle_extensions(
sync_info: SyncInfo<'_>,
conn: &Connection,
window: &Window,
) -> Result<response::Extensions> {
let SyncInfo { .. } = sync_info;
let account_data: OptionFuture<_> = conn
.extensions
.account_data
.enabled
.unwrap_or(false)
.then(|| account_data::collect(sync_info, conn, window))
.into();
let receipts: OptionFuture<_> = conn
.extensions
.receipts
.enabled
.unwrap_or(false)
.then(|| receipts::collect(sync_info, conn, window))
.into();
let typing: OptionFuture<_> = conn
.extensions
.typing
.enabled
.unwrap_or(false)
.then(|| typing::collect(sync_info, conn, window))
.into();
let to_device: OptionFuture<_> = conn
.extensions
.to_device
.enabled
.unwrap_or(false)
.then(|| to_device::collect(sync_info, conn))
.into();
let e2ee: OptionFuture<_> = conn
.extensions
.e2ee
.enabled
.unwrap_or(false)
.then(|| e2ee::collect(sync_info, conn))
.into();
let (account_data, receipts, typing, to_device, e2ee) =
join5(account_data, receipts, typing, to_device, e2ee)
.map(apply!(5, |t: Option<_>| t.unwrap_or(Ok(Default::default()))))
.await;
Ok(response::Extensions {
account_data: account_data?,
receipts: receipts?,
typing: typing?,
to_device: to_device?,
e2ee: e2ee?,
})
}
#[tracing::instrument(
name = "selector",
level = "trace",
skip_all,
fields(?implicit, ?explicit),
)]
fn extension_rooms_selector<'a, ListIter, SubsIter>(
SyncInfo { .. }: SyncInfo<'a>,
conn: &'a Connection,
window: &'a Window,
implicit: Option<ListIter>,
explicit: Option<SubsIter>,
) -> impl Iterator<Item = &'a RoomId> + Send + Sync + 'a
where
ListIter: Iterator<Item = &'a ListId> + Clone + Debug + Send + Sync + 'a,
SubsIter: Iterator<Item = &'a ExtensionRoomConfig> + Clone + Debug + Send + Sync + 'a,
{
let has_all_subscribed = explicit
.clone()
.into_iter()
.flatten()
.any(|erc| matches!(erc, ExtensionRoomConfig::AllSubscribed));
let all_subscribed = has_all_subscribed
.then(|| conn.subscriptions.keys())
.into_iter()
.flatten()
.map(AsRef::as_ref);
let rooms_explicit = has_all_subscribed
.is_false()
.then(move || {
explicit
.into_iter()
.flatten()
.filter_map(|erc| extract_variant!(erc, ExtensionRoomConfig::Room))
.map(AsRef::as_ref)
})
.into_iter()
.flatten();
let rooms_selected = window
.iter()
.filter(move |(_, room)| {
implicit.as_ref().is_none_or(|lists| {
lists
.clone()
.any(|list| room.lists.contains(list))
})
})
.map(at!(0))
.map(AsRef::as_ref);
all_subscribed
.chain(rooms_explicit)
.chain(rooms_selected)
response.extensions.is_empty() && response.rooms.is_empty()
}
+150
View File
@@ -0,0 +1,150 @@
mod account_data;
mod e2ee;
mod receipts;
mod to_device;
mod typing;
use std::fmt::Debug;
use futures::{
FutureExt,
future::{OptionFuture, join5},
};
use ruma::{
RoomId,
api::client::sync::sync_events::v5::{ListId, request::ExtensionRoomConfig, response},
};
use tuwunel_core::{Result, apply, at, extract_variant, utils::BoolExt};
use tuwunel_service::sync::Connection;
use super::{SyncInfo, Window, share_encrypted_room};
#[tracing::instrument(
name = "extensions",
level = "debug",
skip_all,
fields(
next_batch = conn.next_batch,
window = window.len(),
rooms = conn.rooms.len(),
subs = conn.subscriptions.len(),
)
)]
pub(super) async fn handle(
sync_info: SyncInfo<'_>,
conn: &Connection,
window: &Window,
) -> Result<response::Extensions> {
let SyncInfo { .. } = sync_info;
let account_data: OptionFuture<_> = conn
.extensions
.account_data
.enabled
.unwrap_or(false)
.then(|| account_data::collect(sync_info, conn, window))
.into();
let receipts: OptionFuture<_> = conn
.extensions
.receipts
.enabled
.unwrap_or(false)
.then(|| receipts::collect(sync_info, conn, window))
.into();
let typing: OptionFuture<_> = conn
.extensions
.typing
.enabled
.unwrap_or(false)
.then(|| typing::collect(sync_info, conn, window))
.into();
let to_device: OptionFuture<_> = conn
.extensions
.to_device
.enabled
.unwrap_or(false)
.then(|| to_device::collect(sync_info, conn))
.into();
let e2ee: OptionFuture<_> = conn
.extensions
.e2ee
.enabled
.unwrap_or(false)
.then(|| e2ee::collect(sync_info, conn))
.into();
let (account_data, receipts, typing, to_device, e2ee) =
join5(account_data, receipts, typing, to_device, e2ee)
.map(apply!(5, |t: Option<_>| t.unwrap_or(Ok(Default::default()))))
.await;
Ok(response::Extensions {
account_data: account_data?,
receipts: receipts?,
typing: typing?,
to_device: to_device?,
e2ee: e2ee?,
})
}
#[tracing::instrument(
name = "selector",
level = "trace",
skip_all,
fields(?implicit, ?explicit),
)]
fn selector<'a, ListIter, SubsIter>(
SyncInfo { .. }: SyncInfo<'a>,
conn: &'a Connection,
window: &'a Window,
implicit: Option<ListIter>,
explicit: Option<SubsIter>,
) -> impl Iterator<Item = &'a RoomId> + Send + Sync + 'a
where
ListIter: Iterator<Item = &'a ListId> + Clone + Debug + Send + Sync + 'a,
SubsIter: Iterator<Item = &'a ExtensionRoomConfig> + Clone + Debug + Send + Sync + 'a,
{
let has_all_subscribed = explicit
.clone()
.into_iter()
.flatten()
.any(|erc| matches!(erc, ExtensionRoomConfig::AllSubscribed));
let all_subscribed = has_all_subscribed
.then(|| conn.subscriptions.keys())
.into_iter()
.flatten()
.map(AsRef::as_ref);
let rooms_explicit = has_all_subscribed
.is_false()
.then(move || {
explicit
.into_iter()
.flatten()
.filter_map(|erc| extract_variant!(erc, ExtensionRoomConfig::Room))
.map(AsRef::as_ref)
})
.into_iter()
.flatten();
let rooms_selected = window
.iter()
.filter(move |(_, room)| {
implicit.as_ref().is_none_or(|lists| {
lists
.clone()
.any(|list| room.lists.contains(list))
})
})
.map(at!(0))
.map(AsRef::as_ref);
all_subscribed
.chain(rooms_explicit)
.chain(rooms_selected)
}
@@ -6,9 +6,9 @@
};
use tuwunel_service::sync::Room;
use super::{Connection, SyncInfo, Window, extension_rooms_selector};
use super::{Connection, SyncInfo, Window, selector};
#[tracing::instrument(level = "trace", skip_all)]
#[tracing::instrument(name = "account_data", level = "trace", skip_all)]
pub(super) async fn collect(
sync_info: SyncInfo<'_>,
conn: &Connection,
@@ -30,7 +30,7 @@ pub(super) async fn collect(
.as_deref()
.map(<[_]>::iter);
let rooms = extension_rooms_selector(sync_info, conn, window, implicit, explicit)
let rooms = selector(sync_info, conn, window, implicit, explicit)
.stream()
.broad_filter_map(async |room_id| {
let &Room { roomsince, .. } = conn.rooms.get(room_id)?;
@@ -26,7 +26,7 @@
use super::{SyncInfo, share_encrypted_room};
#[tracing::instrument(level = "trace", skip_all)]
#[tracing::instrument(name = "e2ee", level = "trace", skip_all)]
pub(super) async fn collect(
sync_info: SyncInfo<'_>,
conn: &Connection,
@@ -94,7 +94,7 @@ pub(super) async fn collect(
})
}
#[tracing::instrument(level = "trace", skip_all, fields(room_id))]
#[tracing::instrument(level = "trace", skip_all, fields(room_id), ret)]
async fn collect_room(
SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
conn: &Connection,
@@ -11,9 +11,9 @@
};
use tuwunel_service::{rooms::read_receipt::pack_receipts, sync::Room};
use super::{Connection, SyncInfo, Window, extension_rooms_selector};
use super::{Connection, SyncInfo, Window, selector};
#[tracing::instrument(level = "trace", skip_all)]
#[tracing::instrument(name = "receipts", level = "trace", skip_all)]
pub(super) async fn collect(
sync_info: SyncInfo<'_>,
conn: &Connection,
@@ -35,7 +35,7 @@ pub(super) async fn collect(
.as_deref()
.map(<[_]>::iter);
let rooms = extension_rooms_selector(sync_info, conn, window, implicit, explicit)
let rooms = selector(sync_info, conn, window, implicit, explicit)
.stream()
.broad_filter_map(|room_id| collect_room(sync_info, conn, window, room_id))
.collect()
@@ -44,7 +44,7 @@ pub(super) async fn collect(
Ok(response::Receipts { rooms })
}
#[tracing::instrument(level = "trace", skip_all, fields(room_id))]
#[tracing::instrument(level = "trace", skip_all, fields(room_id), ret)]
async fn collect_room(
SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
conn: &Connection,
@@ -1,10 +1,10 @@
use futures::StreamExt;
use ruma::api::client::sync::sync_events::v5::response;
use tuwunel_core::{self, Result};
use tuwunel_core::{self, Result, at};
use super::{Connection, SyncInfo};
#[tracing::instrument(level = "trace", skip_all)]
#[tracing::instrument(name = "to_device", level = "trace", skip_all, ret)]
pub(super) async fn collect(
SyncInfo { services, sender_user, sender_device, .. }: SyncInfo<'_>,
conn: &Connection,
@@ -17,6 +17,7 @@ pub(super) async fn collect(
let events: Vec<_> = services
.users
.get_to_device_events(sender_user, sender_device, None, Some(conn.next_batch))
.map(at!(1))
.collect()
.await;
@@ -11,9 +11,9 @@
utils::{IterStream, ReadyExt},
};
use super::{Connection, SyncInfo, Window, extension_rooms_selector};
use super::{Connection, SyncInfo, Window, selector};
#[tracing::instrument(level = "trace", skip_all)]
#[tracing::instrument(name = "typing", level = "trace", skip_all, ret)]
pub(super) async fn collect(
sync_info: SyncInfo<'_>,
conn: &Connection,
@@ -37,7 +37,7 @@ pub(super) async fn collect(
.as_deref()
.map(<[_]>::iter);
extension_rooms_selector(sync_info, conn, window, implicit, explicit)
selector(sync_info, conn, window, implicit, explicit)
.stream()
.filter_map(async |room_id| {
services
+11 -14
View File
@@ -7,7 +7,7 @@
is_equal_to, is_true,
utils::{
BoolExt, FutureBoolExt, IterStream, ReadyExt,
future::{OptionExt, ReadyEqExt},
future::{self, OptionExt, ReadyEqExt},
},
};
@@ -115,19 +115,16 @@ pub(super) async fn filter_room(
})
.into();
match_encrypted
.is_none_or(is_true!())
.and3(
match_invite.is_none_or(is_true!()),
match_direct.is_none_or(is_true!()),
match_direct_member.is_none_or(is_true!()),
)
.and3(
match_space_child.is_none_or(is_true!()),
match_room_type.is_none_or(is_true!()),
match_room_tag.is_none_or(is_true!()),
)
.await
future::and7(
match_invite.is_none_or(is_true!()),
match_encrypted.is_none_or(is_true!()),
match_direct.is_none_or(is_true!()),
match_direct_member.is_none_or(is_true!()),
match_space_child.is_none_or(is_true!()),
match_room_type.is_none_or(is_true!()),
match_room_tag.is_none_or(is_true!()),
)
.await
}
#[tracing::instrument(name = "filter_meta", level = "trace", skip_all)]
@@ -1,14 +1,17 @@
use std::{cmp::Ordering, collections::HashSet};
use std::{
cmp::Ordering,
collections::{BTreeMap, HashSet},
};
use futures::{
FutureExt, StreamExt, TryFutureExt, TryStreamExt,
future::{OptionFuture, join, join3, join4},
};
use ruma::{
JsOption, MxcUri, OwnedMxcUri, RoomId, UInt, UserId,
JsOption, MxcUri, OwnedMxcUri, OwnedRoomId, RoomId, UserId,
api::client::sync::sync_events::{
UnreadNotificationsCount,
v5::{DisplayName, response},
v5::{DisplayName, response, response::Heroes},
},
events::{
StateEventType,
@@ -19,7 +22,7 @@
},
};
use tuwunel_core::{
Result, at, debug_error, err, is_equal_to,
Result, at, err, error, is_equal_to,
matrix::{Event, StateKey, pdu::PduCount},
ref_at,
utils::{
@@ -29,12 +32,41 @@
};
use tuwunel_service::{Services, sync::Room};
use super::{super::load_timeline, Connection, SyncInfo, WindowRoom};
use super::{super::load_timeline, Connection, SyncInfo, Window, WindowRoom};
use crate::client::ignored_filter;
static DEFAULT_BUMP_TYPES: [TimelineEventType; 6] =
[CallInvite, PollStart, Beacon, RoomEncrypted, RoomMessage, Sticker];
#[tracing::instrument(
name = "rooms",
level = "debug",
skip_all,
fields(
next_batch = conn.next_batch,
window = window.len(),
)
)]
pub(super) async fn handle(
sync_info: SyncInfo<'_>,
conn: &Connection,
window: &Window,
) -> Result<BTreeMap<OwnedRoomId, response::Room>> {
window
.iter()
.stream()
.broad_filter_map(async |(room_id, room)| {
handle_room(sync_info, conn, room)
.map_ok(move |room| (room_id.clone(), room))
.inspect_err(|e| error!(?room_id, "sync handler: {e:?}"))
.await
.ok()
})
.collect()
.map(Ok)
.await
}
#[tracing::instrument(
name = "room",
level = "debug",
@@ -42,18 +74,47 @@
fields(room_id, roomsince)
)]
#[allow(clippy::too_many_arguments)]
pub(super) async fn handle(
async fn handle_room(
SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
conn: &Connection,
WindowRoom { lists, membership, room_id, .. }: &WindowRoom,
) -> Result<Option<response::Room>> {
debug_assert!(DEFAULT_BUMP_TYPES.is_sorted(), "DEFAULT_BUMP_TYPES is not sorted");
WindowRoom {
lists, membership, room_id, last_count, ..
}: &WindowRoom,
) -> Result<response::Room> {
debug_assert!(
DEFAULT_BUMP_TYPES.is_sorted(),
"DEFAULT_BUMP_TYPES must be sorted for binary search"
);
let &Room { roomsince } = conn
let &Room { roomsince, .. } = conn
.rooms
.get(room_id)
.ok_or_else(|| err!("Missing connection state for {room_id}"))?;
debug_assert!(
*last_count > roomsince || *last_count == 0 || roomsince == 0,
"Stale room shouldn't be in the window"
);
if *membership == Some(MembershipState::Leave) {
return Ok(response::Room {
initial: roomsince.eq(&0).then_some(true),
lists: lists.clone(),
membership: membership.clone(),
prev_batch: Some(conn.next_batch.to_string().into()),
limited: true,
required_state: vec![
services
.state_accessor
.room_state_get(room_id, &StateEventType::RoomMember, sender_user.as_str())
.map_ok(Event::into_format)
.await?,
],
..Default::default()
});
}
let is_invite = *membership == Some(MembershipState::Invite);
let default_details = (0_usize, HashSet::new());
let (timeline_limit, required_state) = lists
@@ -84,41 +145,55 @@ pub(super) async fn handle(
})
.into();
let Ok(timeline) = timeline.await.transpose() else {
debug_error!(?room_id, "Missing timeline.");
return Ok(None);
};
let (timeline_pdus, limited, _lastcount) = timeline
.await
.flat_ok()
.unwrap_or_else(|| (Vec::new(), true, PduCount::default()));
let (timeline_pdus, limited, _lastcount) =
timeline.unwrap_or_else(|| (Vec::new(), true, PduCount::default()));
if roomsince != 0 && timeline_pdus.is_empty() && !is_invite {
return Ok(None);
}
let required_state = required_state
.into_iter()
.filter(|_| !timeline_pdus.is_empty())
.collect::<Vec<_>>();
let prev_batch = timeline_pdus
.first()
.map(at!(0))
.map(PduCount::into_unsigned)
.or_else(|| roomsince.ne(&0).then_some(roomsince))
.as_ref()
.map(ToString::to_string);
let bump_stamp = timeline_pdus
.iter()
.filter(|(_, pdu)| {
if *pdu.event_type() == TimelineEventType::RoomMember {
return pdu
.state_key()
.is_some_and(is_equal_to!(sender_user.as_str()));
}
DEFAULT_BUMP_TYPES
.binary_search(pdu.event_type())
.is_ok()
})
.fold(Option::<UInt>::None, |mut bump_stamp, (_, pdu)| {
let ts = pdu.origin_server_ts().0;
if bump_stamp.is_none_or(|bump_stamp| bump_stamp < ts) {
bump_stamp.replace(ts);
}
.filter(|(_, pdu)| !pdu.is_redacted())
.map(at!(0))
.map(PduCount::into_signed)
.max()
.map(TryInto::try_into)
.flat_ok();
bump_stamp
});
let num_live: OptionFuture<_> = roomsince
.ne(&0)
.and_is(limited || timeline_pdus.len() >= timeline_limit)
.then(|| {
services
.timeline
.pdus(None, room_id, Some(roomsince.into()))
.count()
.map(TryInto::try_into)
.map(Result::ok)
})
.into();
let lazy = required_state
.iter()
@@ -183,14 +258,6 @@ pub(super) async fn handle(
})
.into();
let timeline = timeline_pdus
.iter()
.stream()
.filter_map(|item| ignored_filter(services, item.clone(), sender_user))
.map(at!(1))
.map(Event::into_format)
.collect();
let room_name = services
.state_accessor
.get_name(room_id)
@@ -205,13 +272,13 @@ pub(super) async fn handle(
.map(Option::flatten);
let highlight_count = services
.user
.pusher
.highlight_count(sender_user, room_id)
.map(TryInto::try_into)
.map(Result::ok);
let notification_count = services
.user
.pusher
.notification_count(sender_user, room_id)
.map(TryInto::try_into)
.map(Result::ok);
@@ -235,15 +302,27 @@ pub(super) async fn handle(
.is_direct(room_id, sender_user)
.map(|is_dm| is_dm.then_some(is_dm));
let last_read_count = services
.pusher
.last_notification_read(sender_user, room_id);
let timeline = timeline_pdus
.iter()
.stream()
.filter_map(|item| ignored_filter(services, item.clone(), sender_user))
.map(at!(1))
.map(Event::into_format)
.collect();
let meta = join3(room_name, room_avatar, is_dm);
let events = join3(timeline, required_state, invite_state);
let events = join4(timeline, num_live, required_state, invite_state);
let member_counts = join(joined_count, invited_count);
let notification_counts = join(highlight_count, notification_count);
let notification_counts = join3(highlight_count, notification_count, last_read_count);
let (
(room_name, room_avatar, is_dm),
(timeline, required_state, invite_state),
(timeline, num_live, required_state, invite_state),
(joined_count, invited_count),
(highlight_count, notification_count),
(highlight_count, notification_count, _last_notification_read),
) = join4(meta, events, member_counts, notification_counts)
.boxed()
.await;
@@ -257,27 +336,25 @@ pub(super) async fn handle(
)
.await?;
let num_live = None; // Count events in timeline greater than global sync counter
Ok(Some(response::Room {
initial: Some(roomsince == 0),
Ok(response::Room {
initial: roomsince.eq(&0).then_some(true),
lists: lists.clone(),
membership: membership.clone(),
name: room_name.or(hero_name),
avatar: JsOption::from_option(room_avatar.or(heroes_avatar)),
is_dm,
heroes,
required_state,
invite_state: invite_state.flatten(),
prev_batch: prev_batch.as_deref().map(Into::into),
num_live: num_live.flatten(),
limited,
timeline,
bump_stamp,
heroes,
num_live,
joined_count,
invited_count,
unread_notifications: UnreadNotificationsCount { highlight_count, notification_count },
}))
})
}
#[tracing::instrument(name = "heroes", level = "trace", skip_all)]
@@ -288,9 +365,9 @@ async fn calculate_heroes(
room_id: &RoomId,
room_name: Option<&DisplayName>,
room_avatar: Option<&MxcUri>,
) -> Result<(Option<Vec<response::Hero>>, Option<DisplayName>, Option<OwnedMxcUri>)> {
) -> Result<(Option<Heroes>, Option<DisplayName>, Option<OwnedMxcUri>)> {
const MAX_HEROES: usize = 5;
let heroes: Vec<_> = services
let heroes: Heroes = services
.state_cache
.room_members(room_id)
.ready_filter(|&member| member != sender_user)
+153 -146
View File
@@ -2,7 +2,7 @@
use futures::{
FutureExt, StreamExt, TryFutureExt,
future::{OptionFuture, join3},
future::{OptionFuture, join5},
};
use ruma::{OwnedRoomId, UInt, events::room::member::MembershipState, uint};
use tuwunel_core::{
@@ -10,8 +10,7 @@
matrix::PduCount,
trace,
utils::{
BoolExt,
future::TryExtExt,
BoolExt, TryFutureExtExt,
math::usize_from_ruma,
stream::{BroadbandExt, IterStream},
},
@@ -19,7 +18,8 @@
use tuwunel_service::sync::Connection;
use super::{
ListIds, ResponseLists, SyncInfo, Window, WindowRoom, filter_room, filter_room_meta,
ListIds, ResponseLists, SyncInfo, Window, WindowRoom,
filter::{filter_room, filter_room_meta},
};
#[tracing::instrument(level = "debug", skip_all)]
@@ -29,16 +29,13 @@ pub(super) async fn selector(
) -> (Window, ResponseLists) {
use MembershipState::*;
let SyncInfo { services, sender_user, request, .. } = sync_info;
let SyncInfo { services, sender_user, .. } = sync_info;
trace!(?request);
let mut rooms = services
.state_cache
.user_memberships(sender_user, Some(&[Join, Invite, Knock]))
.map(|(membership, room_id)| (room_id.to_owned(), Some(membership)))
.broad_filter_map(|(room_id, membership)| {
match_lists_for_room(sync_info, conn, room_id, membership)
})
.broad_filter_map(|(room_id, membership)| matcher(sync_info, conn, room_id, membership))
.collect::<Vec<_>>()
.await;
@@ -48,25 +45,133 @@ pub(super) async fn selector(
.enumerate()
.for_each(|(i, room)| {
room.ranked = i;
conn.rooms
.entry(room.room_id.clone())
.or_default();
});
trace!(?rooms);
let lists = response_lists(rooms.iter());
trace!(?lists);
let window = select_window(sync_info, conn, rooms.iter(), &lists).await;
let window = window(sync_info, conn, rooms.iter(), &lists).await;
trace!(?window);
for room in &rooms {
conn.rooms
.entry(room.room_id.clone())
.or_default();
}
(window, lists)
}
async fn select_window<'a, Rooms>(
#[tracing::instrument(
name = "matcher",
level = "trace",
skip_all,
fields(?room_id, ?membership)
)]
async fn matcher(
sync_info: SyncInfo<'_>,
conn: &Connection,
room_id: OwnedRoomId,
membership: Option<MembershipState>,
) -> Option<WindowRoom> {
let SyncInfo { services, sender_user, .. } = sync_info;
let (matched, lists) = conn
.lists
.iter()
.stream()
.filter_map(async |(id, list)| {
let filter: OptionFuture<_> = list
.filters
.clone()
.map(async |filters| {
filter_room(sync_info, &filters, &room_id, membership.as_ref()).await
})
.into();
filter
.await
.is_none_or(is_true!())
.then(|| id.clone())
})
.collect::<ListIds>()
.map(|lists| (lists.is_empty().is_false(), lists))
.await;
let last_notification: OptionFuture<_> = matched
.then(|| {
services
.pusher
.last_notification_read(sender_user, &room_id)
.unwrap_or_default()
})
.into();
let last_privateread: OptionFuture<_> = matched
.then(|| {
services
.read_receipt
.last_privateread_update(sender_user, &room_id)
})
.into();
let last_receipt: OptionFuture<_> = matched
.then(|| {
services
.read_receipt
.last_receipt_count(&room_id, sender_user.into(), None)
.unwrap_or_default()
})
.into();
let last_account: OptionFuture<_> = matched
.then(|| {
services
.account_data
.last_count(Some(room_id.as_ref()), sender_user, Some(conn.next_batch))
.unwrap_or_default()
})
.into();
let last_timeline: OptionFuture<_> = matched
.then(|| {
services
.timeline
.last_timeline_count(None, &room_id, Some(conn.next_batch.into()))
.map_ok(PduCount::into_unsigned)
.unwrap_or_default()
})
.into();
let (last_timeline, last_notification, last_account, last_receipt, last_privateread) =
join5(last_timeline, last_notification, last_account, last_receipt, last_privateread)
.await;
Some(WindowRoom {
room_id: room_id.clone(),
membership,
lists,
ranked: 0,
last_count: [
last_timeline,
last_notification,
last_account,
last_receipt,
last_privateread,
]
.into_iter()
.map(Option::unwrap_or_default)
.filter(|count| conn.next_batch.ge(count))
.max()
.unwrap_or_default(),
})
}
#[tracing::instrument(
level = "debug",
skip_all,
fields(rooms = rooms.clone().count())
)]
async fn window<'a, Rooms>(
sync_info: SyncInfo<'_>,
conn: &Connection,
rooms: Rooms,
@@ -77,6 +182,8 @@ async fn select_window<'a, Rooms>(
{
static FULL_RANGE: (UInt, UInt) = (UInt::MIN, UInt::MAX);
let SyncInfo { services, sender_user, .. } = sync_info;
let selections = lists
.keys()
.cloned()
@@ -95,135 +202,46 @@ async fn select_window<'a, Rooms>(
.map(move |range| (id.clone(), range))
})
.flat_map(|(id, (start, end))| {
let list = rooms
rooms
.clone()
.filter(move |&room| room.lists.contains(&id));
let cycled = list.clone().all(|room| {
conn.rooms
.get(&room.room_id)
.is_some_and(|room| room.roomsince != 0)
});
list.enumerate()
.skip_while(move |&(i, room)| {
i < start
|| conn
.rooms
.get(&room.room_id)
.is_some_and(|conn_room| {
conn_room.roomsince >= room.last_count
|| (!cycled && conn_room.roomsince != 0)
})
.filter(move |&room| room.lists.contains(&id))
.filter(|&room| {
conn.rooms
.get(&room.room_id)
.is_some_and(|conn_room| {
conn_room.roomsince == 0 || room.last_count > conn_room.roomsince
})
})
.enumerate()
.skip_while(move |&(i, _)| i < start)
.take(end.saturating_add(1).saturating_sub(start))
.map(|(_, room)| (room.room_id.clone(), room.clone()))
});
})
.stream();
conn.subscriptions
let subscriptions = conn
.subscriptions
.iter()
.stream()
.broad_filter_map(async |(room_id, _)| {
filter_room_meta(sync_info, room_id)
.await
.then(|| WindowRoom {
room_id: room_id.clone(),
membership: None,
lists: Default::default(),
ranked: usize::MAX,
last_count: 0,
})
.into_option()?;
Some(WindowRoom {
room_id: room_id.clone(),
lists: Default::default(),
ranked: usize::MAX,
last_count: 0,
membership: services
.state_cache
.user_membership(sender_user, room_id)
.await,
})
})
.map(|room| (room.room_id.clone(), room))
.chain(selections.stream())
.collect()
.await
}
.map(|room| (room.room_id.clone(), room));
#[tracing::instrument(
name = "matcher",
level = "trace",
skip_all,
fields(?room_id, ?membership)
)]
async fn match_lists_for_room(
sync_info: SyncInfo<'_>,
conn: &Connection,
room_id: OwnedRoomId,
membership: Option<MembershipState>,
) -> Option<WindowRoom> {
let SyncInfo { services, sender_user, .. } = sync_info;
let lists = conn
.lists
.iter()
.stream()
.filter_map(async |(id, list)| {
let filter: OptionFuture<_> = list
.filters
.clone()
.map(async |filters| {
filter_room(sync_info, &filters, &room_id, membership.as_ref()).await
})
.into();
filter
.await
.is_none_or(is_true!())
.then(|| id.clone())
})
.collect::<ListIds>()
.await;
let last_timeline_count: OptionFuture<_> = lists
.is_empty()
.is_false()
.then(|| {
services
.timeline
.last_timeline_count(None, &room_id, None)
.map_ok(PduCount::into_unsigned)
.ok()
})
.into();
let last_account_count: OptionFuture<_> = lists
.is_empty()
.is_false()
.then(|| {
services
.account_data
.last_count(Some(room_id.as_ref()), sender_user, conn.next_batch)
.ok()
})
.into();
let last_receipt_count: OptionFuture<_> = lists
.is_empty()
.is_false()
.then(|| {
services
.read_receipt
.last_receipt_count(&room_id, sender_user.into(), conn.globalsince.into())
.map(Result::ok)
})
.into();
let (last_timeline_count, last_account_count, last_receipt_count) =
join3(last_timeline_count, last_account_count, last_receipt_count).await;
Some(WindowRoom {
room_id: room_id.clone(),
membership,
lists,
ranked: 0,
last_count: [last_timeline_count, last_account_count, last_receipt_count]
.into_iter()
.map(Option::flatten)
.map(Option::unwrap_or_default)
.max()
.unwrap_or_default(),
})
subscriptions.chain(selections).collect().await
}
fn response_lists<'a, Rooms>(rooms: Rooms) -> ResponseLists
@@ -243,15 +261,4 @@ fn response_lists<'a, Rooms>(rooms: Rooms) -> ResponseLists
})
}
fn room_sort(a: &WindowRoom, b: &WindowRoom) -> Ordering {
if a.membership != b.membership {
if a.membership == Some(MembershipState::Invite) {
return Ordering::Less;
}
if b.membership == Some(MembershipState::Invite) {
return Ordering::Greater;
}
}
b.last_count.cmp(&a.last_count)
}
fn room_sort(a: &WindowRoom, b: &WindowRoom) -> Ordering { b.last_count.cmp(&a.last_count) }
+5 -1
View File
@@ -66,7 +66,11 @@ pub(crate) async fn create_typing_event_route(
// ping presence
services
.presence
.maybe_ping_presence(&body.user_id, &ruma::presence::PresenceState::Online)
.maybe_ping_presence(
&body.user_id,
body.sender_device.as_deref(),
&ruma::presence::PresenceState::Online,
)
.await?;
Ok(create_typing_event::v3::Response {})
+4 -4
View File
@@ -76,7 +76,7 @@ pub(crate) async fn delete_timezone_key_route(
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await?;
Ok(delete_timezone_key::unstable::Response {})
@@ -104,7 +104,7 @@ pub(crate) async fn set_timezone_key_route(
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await?;
Ok(set_timezone_key::unstable::Response {})
@@ -170,7 +170,7 @@ pub(crate) async fn set_profile_field_route(
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await?;
Ok(set_profile_field::v3::Response {})
@@ -224,7 +224,7 @@ pub(crate) async fn delete_profile_field_route(
// Presence update
services
.presence
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
.await?;
Ok(delete_profile_field::v3::Response {})
+5 -3
View File
@@ -42,17 +42,19 @@ pub(crate) async fn get_supported_versions_route(
unstable_features: BTreeMap::from_iter([
("org.matrix.e2e_cross_signing".to_owned(), true),
("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */
("fi.mau.msc2659.stable".to_owned(), true), /* appservice ping https://github.com/matrix-org/matrix-spec-proposals/pull/2659) */
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
("org.matrix.msc3575".to_owned(), true), /* sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1588877046) */
("org.matrix.msc3814".to_owned(), true), /* dehydrated devices */
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
("org.matrix.msc4180".to_owned(), true), /* stable flag for 3916 (https://github.com/matrix-org/matrix-spec-proposals/pull/4180) */
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
("uk.tcpip.msc4133".to_owned(), true), /* Extending User Profile API with Key:Value Pairs (https://github.com/matrix-org/matrix-spec-proposals/pull/4133) */
("us.cloke.msc4175".to_owned(), true), /* Profile field for user time zone (https://github.com/matrix-org/matrix-spec-proposals/pull/4175) */
("org.matrix.msc4180".to_owned(), true), /* stable flag for 3916 (https://github.com/matrix-org/matrix-spec-proposals/pull/4180) */
("org.matrix.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
]),
};
+1 -1
View File
@@ -25,7 +25,7 @@ pub(crate) async fn turn_server_route(
return Err!(Request(NotFound("Not Found")));
}
let turn_secret = &services.config.turn_secret;
let turn_secret = &services.globals.turn_secret;
let (username, password) = if !turn_secret.is_empty() {
let expiry = SecondsSinceUnixEpoch::from_system_time(
+39 -8
View File
@@ -1,28 +1,59 @@
use axum::{Json, extract::State, response::IntoResponse};
use ruma::api::client::discovery::{
discover_homeserver::{self, HomeserverInfo},
discover_homeserver::{self, HomeserverInfo, RtcFocusInfo},
discover_support::{self, Contact},
};
use tuwunel_core::{Err, Result};
use serde_json::Value as JsonValue;
use tuwunel_core::{Err, Result, err, error::inspect_log};
use crate::Ruma;
/// # `GET /.well-known/matrix/client`
///
/// Returns the .well-known URL if it is configured, otherwise returns 404.
/// Also includes RTC transport configuration for Element Call (MSC4143).
pub(crate) async fn well_known_client(
State(services): State<crate::State>,
_body: Ruma<discover_homeserver::Request>,
) -> Result<discover_homeserver::Response> {
let client_url = match services.server.config.well_known.client.as_ref() {
| Some(url) => url.to_string(),
| None => return Err!(Request(NotFound("Not found."))),
let homeserver = HomeserverInfo {
base_url: match services.server.config.well_known.client.as_ref() {
| Some(url) => url.to_string(),
| None => return Err!(Request(NotFound("Not found."))),
},
};
// Add RTC transport configuration if available (MSC4143 / Element Call)
// Element Call has evolved through several versions with different field
// expectations
let rtc_foci = services
.server
.config
.well_known
.rtc_transports
.iter()
.map(|transport| {
let focus_type = transport
.get("type")
.and_then(JsonValue::as_str)
.ok_or_else(|| err!("`type` is not a valid string"))?;
let transport = transport
.as_object()
.cloned()
.ok_or_else(|| err!("`rtc_transport` is not a valid object"))?;
RtcFocusInfo::new(focus_type, transport).map_err(Into::into)
})
.collect::<Result<_>>()
.map_err(|e| {
err!(Config("global.well_known.rtc_transports", "Malformed value(s): {e:?}"))
})
.inspect_err(inspect_log)?;
Ok(discover_homeserver::Response {
homeserver: HomeserverInfo { base_url: client_url },
identity_server: None,
tile_server: None,
rtc_foci,
..discover_homeserver::Response::new(homeserver)
})
}
+1 -1
View File
@@ -1,4 +1,4 @@
#![type_length_limit = "589824"] //TODO: REDUCE ME
#![type_length_limit = "262144"] //TODO: REDUCE ME
#![allow(clippy::toplevel_ref_arg)]
pub mod client;
+6
View File
@@ -47,6 +47,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::request_3pid_management_token_via_email_route)
.ruma_route(&client::request_3pid_management_token_via_msisdn_route)
.ruma_route(&client::check_registration_token_validity)
.ruma_route(&client::get_notifications_route)
.ruma_route(&client::get_capabilities_route)
.ruma_route(&client::get_pushrules_all_route)
.ruma_route(&client::get_pushrules_global_route)
@@ -145,6 +146,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
get(client::get_state_events_for_empty_key_route)
.put(client::send_state_event_for_empty_key_route),
)
.ruma_route(&client::events_route)
.ruma_route(&client::sync_events_route)
.ruma_route(&client::sync_events_v5_route)
.ruma_route(&client::get_context_route)
@@ -163,6 +165,10 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::update_device_route)
.ruma_route(&client::delete_device_route)
.ruma_route(&client::delete_devices_route)
.ruma_route(&client::put_dehydrated_device_route)
.ruma_route(&client::delete_dehydrated_device_route)
.ruma_route(&client::get_dehydrated_device_route)
.ruma_route(&client::get_dehydrated_events_route)
.ruma_route(&client::get_tags_route)
.ruma_route(&client::update_tag_route)
.ruma_route(&client::delete_tag_route)
+5 -11
View File
@@ -6,7 +6,7 @@
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
OwnedUserId, ServerName, UserId, api::IncomingRequest,
};
use tuwunel_core::{Error, Result, debug, debug_warn, err, trace, utils::string::EMPTY};
use tuwunel_core::{Error, Result, debug_warn, err, trace, utils::string::EMPTY};
use tuwunel_service::{Services, appservice::RegistrationInfo};
use super::{auth, auth::Auth, request, request::Request};
@@ -95,6 +95,7 @@ async fn from_request(
) -> Result<Self, Self::Rejection> {
let mut request = request::from(services, request).await?;
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&request.body).ok();
trace!(?request);
// while very unusual and really shouldn't be recommended, Synapse accepts POST
// requests with a completely empty body. very old clients, libraries, and some
@@ -104,13 +105,13 @@ async fn from_request(
&& request.parts.method == http::Method::POST
&& !request.parts.uri.path().contains("/media/")
{
trace!("json_body from_request: {:?}", json_body.clone());
debug_warn!(
"received a POST request with an empty body, defaulting/assuming to {{}} like \
Synapse does"
);
json_body = Some(CanonicalJsonValue::Object(CanonicalJsonObject::new()));
}
let auth = auth::auth(services, &mut request, json_body.as_ref(), &T::METADATA).await?;
Ok(Self {
body: make_body::<T>(services, &mut request, json_body.as_mut(), &auth)?,
@@ -147,16 +148,9 @@ fn into_http_request(request: &Request, body: Bytes) -> hyper::Request<Bytes> {
.headers_mut()
.expect("mutable http headers") = request.parts.headers.clone();
let http_request = http_request
.body(body)
.expect("http request body");
let headers = http_request.headers();
let method = http_request.method();
let uri = http_request.uri();
debug!("{method:?} {uri:?} {headers:?}");
http_request
.body(body)
.expect("http request body")
}
#[allow(clippy::needless_pass_by_value)]
+1 -1
View File
@@ -19,7 +19,7 @@ pub(super) async fn auth_appservice(
let Ok(user_id) = request
.query
.user_id
.clone()
.as_deref()
.map_or_else(user_id_default, OwnedUserId::parse)
else {
return Err!(Request(InvalidUsername("Username is invalid.")));
+6 -3
View File
@@ -1,6 +1,9 @@
use axum::RequestPartsExt;
use axum_extra::{TypedHeader, headers::Authorization, typed_header::TypedHeaderRejectionReason};
use ruma::{CanonicalJsonObject, CanonicalJsonValue, api::federation::authentication::XMatrix};
use ruma::{
CanonicalJsonName, CanonicalJsonObject, CanonicalJsonValue,
api::federation::authentication::XMatrix,
};
use tuwunel_core::{Err, Result, debug_error, err, warn};
use tuwunel_service::{
Services,
@@ -14,7 +17,7 @@ pub(super) async fn auth_server(
request: &mut Request,
body: Option<&CanonicalJsonValue>,
) -> Result<Auth> {
type Member = (String, CanonicalJsonValue);
type Member = (CanonicalJsonName, CanonicalJsonValue);
type Object = CanonicalJsonObject;
type Value = CanonicalJsonValue;
@@ -66,7 +69,7 @@ pub(super) async fn auth_server(
err!(Request(Forbidden(debug_warn!("Failed to fetch signing keys: {e}"))))
})?;
let keys: PubKeys = [(x_matrix.key.to_string(), key.key)].into();
let keys: PubKeys = [(x_matrix.key.as_str().into(), key.key)].into();
let keys: PubKeyMap = [(origin.as_str().into(), keys)].into();
if let Err(e) = ruma::signatures::verify_json(&keys, &authorization) {
debug_error!("Failed to verify federation request from {origin}: {e}");
+1 -1
View File
@@ -41,7 +41,7 @@ impl<Err, Req, Fut, Fun, $($tx,)*> RumaHandler<($($tx,)* Ruma<Req>,)> for Fun
Fun: Fn($($tx,)* Ruma<Req>,) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<Req::OutgoingResponse, Err>> + Send,
Req: IncomingRequest + Debug + Send + Sync + 'static,
Err: IntoResponse + Send,
Err: IntoResponse + Debug + Send,
<Req as IncomingRequest>::OutgoingResponse: Send,
$( $tx: FromRequestParts<State> + Send + Sync + 'static, )*
{
+11 -5
View File
@@ -4,22 +4,28 @@
use bytes::Bytes;
use http::request::Parts;
use serde::Deserialize;
use tuwunel_core::{Result, err};
use tuwunel_core::{Result, err, smallstr::SmallString, smallvec::SmallVec};
use tuwunel_service::Services;
#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
pub(super) struct QueryParams {
pub(super) access_token: Option<String>,
pub(super) user_id: Option<String>,
pub(super) user_id: Option<UserId>,
}
pub(super) type UserId = SmallString<[u8; 48]>;
#[derive(Debug)]
pub(super) struct Request {
pub(super) path: Path<Vec<String>>,
pub(super) path: Path<PathParams>,
pub(super) query: QueryParams,
pub(super) body: Bytes,
pub(super) parts: Parts,
}
pub(super) type PathParams = SmallVec<[PathParam; 8]>;
pub(super) type PathParam = SmallString<[u8; 32]>;
pub(super) async fn from(
services: &Services,
request: hyper::Request<axum::body::Body>,
@@ -27,7 +33,7 @@ pub(super) async fn from(
let limited = request.with_limited_body();
let (mut parts, body) = limited.into_parts();
let path: Path<Vec<String>> = parts.extract().await?;
let path: Path<PathParams> = parts.extract().await?;
let query = parts.uri.query().unwrap_or_default();
let query = serde_html_form::from_str(query)
.map_err(|e| err!(Request(Unknown("Failed to read query parameters: {e}"))))?;
+39 -3
View File
@@ -1,13 +1,19 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use base64::{Engine as _, engine::general_purpose};
use futures::StreamExt;
use ruma::{
CanonicalJsonValue, OwnedUserId, UserId,
api::{
client::error::ErrorKind,
federation::membership::{RawStrippedState, create_invite},
},
events::room::member::{MembershipState, RoomMemberEventContent},
events::{
GlobalAccountDataEventType,
push_rules::PushRulesEvent,
room::member::{MembershipState, RoomMemberEventContent},
},
push,
serde::JsonObject,
};
use tuwunel_core::{
@@ -100,7 +106,7 @@ pub(crate) async fn create_invite_route(
let event_id = gen_event_id(&signed_event, &body.room_version)?;
// Add event_id back
signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string()));
signed_event.insert("event_id".into(), CanonicalJsonValue::String(event_id.to_string()));
let sender: &UserId = signed_event
.get("sender")
@@ -127,7 +133,7 @@ pub(crate) async fn create_invite_route(
let mut event: JsonObject = serde_json::from_str(body.event.get())
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
event.insert("event_id".to_owned(), "$placeholder".into());
event.insert("event_id".into(), "$placeholder".into());
let pdu: PduEvent = serde_json::from_value(event.into())
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
@@ -159,6 +165,36 @@ pub(crate) async fn create_invite_route(
.await?;
drop(count);
services
.pusher
.get_pushkeys(&invited_user)
.map(ToOwned::to_owned)
.for_each(async |pushkey| {
let Ok(pusher) = services
.pusher
.get_pusher(&invited_user, &pushkey)
.await
else {
return;
};
let ruleset = services
.account_data
.get_global(&invited_user, GlobalAccountDataEventType::PushRules)
.await
.map_or_else(
|_| push::Ruleset::server_default(&invited_user),
|ev: PushRulesEvent| ev.content.global,
);
services
.pusher
.send_push_notice(&invited_user, &pusher, &ruleset, &pdu)
.await
.ok();
})
.await;
for appservice in services.appservice.read().await.values() {
if appservice.is_user_match(&invited_user) {
services
+69 -60
View File
@@ -1,13 +1,17 @@
use axum::extract::State;
use futures::{FutureExt, StreamExt, TryFutureExt};
use ruma::api::{
client::error::ErrorKind,
federation::{
device::get_devices::{self, v1::UserDevice},
keys::{claim_keys, get_keys},
use futures::{FutureExt, StreamExt, TryFutureExt, future::join4};
use ruma::{
UserId,
api::{
client::device::Device,
federation::{
device::get_devices::{self, v1::UserDevice},
keys::{claim_keys, get_keys},
},
},
uint,
};
use tuwunel_core::{Error, Result};
use tuwunel_core::{Err, Result, utils::future::TryExtExt};
use crate::{
Ruma,
@@ -21,54 +25,65 @@ pub(crate) async fn get_devices_route(
State(services): State<crate::State>,
body: Ruma<get_devices::v1::Request>,
) -> Result<get_devices::v1::Response> {
if !services.globals.user_is_local(&body.user_id) {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Tried to access user from other server.",
));
let user_id = &body.user_id;
if !services.globals.user_is_local(user_id) {
return Err!(Request(InvalidParam("Tried to access user from other server.")));
}
let user_id = &body.user_id;
Ok(get_devices::v1::Response {
user_id: user_id.clone(),
stream_id: services
.users
.get_devicelist_version(user_id)
.await
.unwrap_or(0)
.try_into()?,
devices: services
.users
.all_devices_metadata(user_id)
.filter_map(async |metadata| {
let device_id = metadata.device_id.clone();
let device_id_clone = device_id.clone();
let device_id_string = device_id.as_str().to_owned();
let device_display_name = if services.config.allow_device_name_federation {
metadata.display_name.clone()
} else {
Some(device_id_string)
};
let allowed_signatures = |u: &UserId| u.server_name() == body.origin();
services
.users
.get_device_keys(user_id, &device_id_clone)
.map_ok(|keys| UserDevice { device_id, keys, device_display_name })
.map(Result::ok)
.await
})
.collect()
.await,
master_key: services
.users
.get_master_key(None, &body.user_id, &|u| u.server_name() == body.origin())
.await
.ok(),
self_signing_key: services
.users
.get_self_signing_key(None, &body.user_id, &|u| u.server_name() == body.origin())
.await
.ok(),
let master_key = services
.users
.get_master_key(None, user_id, &allowed_signatures)
.ok();
let self_signing_key = services
.users
.get_self_signing_key(None, user_id, &allowed_signatures)
.ok();
let stream_id = services
.users
.get_devicelist_version(user_id)
.map_ok(TryInto::try_into)
.map_ok(Result::ok)
.ok();
let devices = services
.users
.all_devices_metadata(user_id)
.filter_map(async |Device { device_id, display_name, .. }: Device| {
let device_display_name = services
.config
.allow_device_name_federation
.then_some(display_name)
.flatten()
.or_else(|| Some(device_id.as_str().into()));
services
.users
.get_device_keys(user_id, &device_id)
.map_ok(|keys| UserDevice {
device_id: device_id.clone(),
device_display_name,
keys,
})
.map(Result::ok)
.await
})
.collect::<Vec<_>>();
let (stream_id, master_key, self_signing_key, devices) =
join4(stream_id, master_key, self_signing_key, devices)
.boxed()
.await;
Ok(get_devices::v1::Response {
user_id: body.body.user_id,
stream_id: stream_id.flatten().unwrap_or_else(|| uint!(0)),
devices,
self_signing_key,
master_key,
})
}
@@ -84,10 +99,7 @@ pub(crate) async fn get_keys_route(
.iter()
.any(|(u, _)| !services.globals.user_is_local(u))
{
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"User does not belong to this server.",
));
return Err!(Request(InvalidParam("User does not belong to this server.")));
}
let result = get_keys_helper(
@@ -118,10 +130,7 @@ pub(crate) async fn claim_keys_route(
.iter()
.any(|(u, _)| !services.globals.user_is_local(u))
{
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Tried to access user from other server.",
));
return Err!(Request(InvalidParam("Tried to access user from other server.")));
}
let result = claim_keys_helper(&services, &body.one_time_keys).await?;
+60 -12
View File
@@ -590,6 +590,17 @@ pub struct Config {
#[serde(default = "true_fn")]
pub allow_encryption: bool,
/// Controls whether locally-created rooms should be end-to-end encrypted by
/// default. This option is equivalent to the one found in Synapse.
///
/// Options:
/// - "all": All created rooms are encrypted.
/// - "invite": Any room created with `private_chat` or
/// `trusted_private_chat` presets.
/// - Other values default to no effect.
#[serde(default)]
pub encryption_enabled_by_default_for_room_type: Option<String>,
/// Controls whether federation is allowed or not. It is not recommended to
/// disable this after installation due to potential federation breakage but
/// this is technically not a permanent setting.
@@ -1277,26 +1288,28 @@ pub struct Config {
#[serde(default = "default_rocksdb_stats_level")]
pub rocksdb_stats_level: u8,
/// Erases data no longer reachable in the current schema. The developers
/// expect this to be set to true which simplifies the schema and prevents
/// accumulation of old schemas remaining in the codebase forever. If this
/// is set to false, old columns which are not described in the current
/// schema will be ignored rather than erased, leaking their space.
/// Ignores the list of dropped columns set by developers.
///
/// This can be set to false when moving between versions in ways which are
/// not recommended or otherwise forbidden, or for diagnostic and
/// development purposes; requiring preservation across such movements.
/// This should be set to true when knowingly moving between versions in
/// ways which are not recommended or otherwise forbidden, or for
/// diagnostic and development purposes; requiring preservation across such
/// movements.
///
/// default: true
#[serde(default = "true_fn")]
pub rocksdb_drop_missing_columns: bool,
/// The developer's list of dropped columns is meant to safely reduce space
/// by erasing data no longer in use. If this is set to true that storage
/// will not be reclaimed as intended.
///
/// default: false
#[serde(default)]
pub rocksdb_never_drop_columns: bool,
/// This is a password that can be configured that will let you login to the
/// server bot account (currently `@conduit`) for emergency troubleshooting
/// purposes such as recovering/recreating your admin room, or inviting
/// yourself back.
///
/// See https://tuwunel.chat/troubleshooting.html#lost-access-to-admin-room for other ways to get back into your admin room.
/// See https://tuwunel.chat/troubleshooting.html#lost-access-to-admin-room
/// for other ways to get back into your admin room.
///
/// Once this password is unset, all sessions will be logged out for
/// security purposes.
@@ -1310,6 +1323,19 @@ pub struct Config {
#[serde(default = "default_notification_push_path")]
pub notification_push_path: String,
/// For compatibility and special purpose use only. Setting this option to
/// true will not filter messages sent to pushers based on rules or actions.
/// Everything will be sent to the pusher. This option is offered for
/// several reasons, but should not be necessary:
/// - Bypass to workaround bugs or outdated server-side ruleset support.
/// - Allow clients to evaluate pushrules themselves (due to the above).
/// - Hosting or companies which have custom pushers and internal needs.
///
/// Note that setting this option to true will not affect the record of
/// notifications found in the notifications pane.
#[serde(default)]
pub push_everything: bool,
/// Allow local (your server only) presence updates/requests.
///
/// Note that presence on tuwunel is very fast unlike Synapse's. If using
@@ -2134,6 +2160,28 @@ pub struct WellKnownConfig {
///
/// example "@admin:example.com"
pub support_mxid: Option<OwnedUserId>,
/// Element Call / MatrixRTC configuration (MSC4143).
/// Configures the LiveKit SFU server for voice/video calls.
///
/// Requires a LiveKit server with JWT authentication.
/// The `livekit_service_url` should point to your LiveKit JWT endpoint.
///
/// Note: You must also set `client` above to your homeserver URL.
///
/// Example:
/// ```toml
/// [global.well_known]
/// client = "https://matrix.yourdomain.com"
///
/// [[global.well_known.rtc_transports]]
/// type = "livekit"
/// livekit_service_url = "https://livekit.yourdomain.com"
/// ```
///
/// default: []
#[serde(default)]
pub rtc_transports: Vec<serde_json::Value>,
}
#[derive(Clone, Copy, Debug, Deserialize, Default)]
+2 -2
View File
@@ -62,7 +62,7 @@ fn mutate_outgoing_reference_format(value: &mut CanonicalJsonValue) {
if let Some(event_id) = value.as_str().map(ToOwned::to_owned) {
*value = CanonicalJsonValue::Array(vec![
CanonicalJsonValue::String(event_id),
CanonicalJsonValue::Object([(String::new(), "".into())].into()),
CanonicalJsonValue::Object([(Default::default(), "".into())].into()),
]);
}
});
@@ -92,7 +92,7 @@ pub fn from_incoming_federation(
}
if !room_rules.event_format.require_event_id {
pdu_json.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.into()));
pdu_json.insert("event_id".into(), CanonicalJsonValue::String(event_id.into()));
}
PduEvent::from_val(pdu_json)
@@ -10,7 +10,7 @@
};
use crate::{
Err, Result, debug, err, is_equal_to,
Err, Result, err, is_equal_to,
matrix::{Event, StateKey},
};
@@ -45,8 +45,6 @@ pub(super) async fn check_room_member<Fetch, Fut, Pdu>(
Fut: Future<Output = Result<Pdu>> + Send,
Pdu: Event,
{
debug!("starting m.room.member check");
// Since v1, if there is no state_key property, or no membership property in
// content, reject.
let Some(state_key) = room_member_event.state_key() else {
+7 -7
View File
@@ -364,7 +364,7 @@ fn check_pdu_format_pdu_too_big() {
.unwrap();
let long_string = repeat_n('a', 66_000).collect::<String>();
content.insert("big_data".to_owned(), long_string.into());
content.insert("big_data".into(), long_string.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
@@ -384,7 +384,7 @@ fn check_pdu_format_strings_too_big() {
for field in &["event_id", "sender", "room_id", "type", "state_key"] {
let mut pdu = pdu_v1();
let value = repeat_n('a', 300).collect::<String>();
pdu.insert((*field).to_owned(), value.into());
pdu.insert((*field).into(), value.into());
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
}
}
@@ -393,7 +393,7 @@ fn check_pdu_format_strings_too_big() {
fn check_pdu_format_strings_wrong_format() {
for field in &["event_id", "sender", "room_id", "type", "state_key"] {
let mut pdu = pdu_v1();
pdu.insert((*field).to_owned(), true.into());
pdu.insert((*field).into(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
}
}
@@ -405,7 +405,7 @@ fn check_pdu_format_arrays_too_big() {
let value: Vec<_> =
repeat_n(CanonicalJsonValue::from("$eventid".to_owned()), 30).collect();
pdu.insert((*field).to_owned(), value.into());
pdu.insert((*field).into(), value.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
}
@@ -414,7 +414,7 @@ fn check_pdu_format_arrays_too_big() {
fn check_pdu_format_arrays_wrong_format() {
for field in &["prev_events", "auth_events"] {
let mut pdu = pdu_v3();
pdu.insert((*field).to_owned(), true.into());
pdu.insert((*field).into(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
}
@@ -422,7 +422,7 @@ fn check_pdu_format_arrays_wrong_format() {
#[test]
fn check_pdu_format_negative_depth() {
let mut pdu = pdu_v3();
pdu.insert("depth".to_owned(), int!(-1).into())
pdu.insert("depth".into(), int!(-1).into())
.unwrap();
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
@@ -431,7 +431,7 @@ fn check_pdu_format_negative_depth() {
#[test]
fn check_pdu_format_depth_wrong_format() {
let mut pdu = pdu_v3();
pdu.insert("depth".to_owned(), true.into());
pdu.insert("depth".into(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
+8 -7
View File
@@ -25,6 +25,7 @@
use crate::{
Result, debug,
matrix::{Event, TypeStateKey},
trace,
utils::stream::{BroadbandExt, IterStream},
};
@@ -87,7 +88,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
// Split the unconflicted state map and the conflicted state set.
let (unconflicted_state, conflicted_states) = split_conflicted_state(state_maps).await;
debug!(?unconflicted_state, unconflicted = unconflicted_state.len(), "unresolved state");
trace!(?unconflicted_state, unconflicted = unconflicted_state.len(), "unresolved state");
debug!(?conflicted_states, conflicted = conflicted_states.len(), "unresolved states");
if conflicted_states.is_empty() {
@@ -120,7 +121,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
.chain(conflicted_subgraph)
.collect::<AuthSet<_>>()
.inspect(|set| debug!(count = set.len(), "full conflicted set"))
.inspect(|set| debug!(?set, "full conflicted set"))
.inspect(|set| trace!(?set, "full conflicted set"))
.await;
// 1. Select the set X of all power events that appear in the full conflicted
@@ -129,7 +130,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
// a list using the reverse topological power ordering.
let sorted_power_events: Vec<_> = power_sort(rules, &full_conflicted_set, fetch)
.inspect_ok(|list| debug!(count = list.len(), "sorted power events"))
.inspect_ok(|list| debug!(?list, "sorted power events"))
.inspect_ok(|list| trace!(?list, "sorted power events"))
.await?;
let sorted_power_events_set: AuthSet<_> = sorted_power_events.iter().collect();
@@ -154,7 +155,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
let partially_resolved_state =
iterative_auth_check(rules, sorted_power_events, initial_state, fetch)
.inspect_ok(|map| debug!(count = map.len(), "partially resolved power state"))
.inspect_ok(|map| debug!(?map, "partially resolved power state"))
.inspect_ok(|map| trace!(?map, "partially resolved power state"))
.await?;
// This "epochs" power level event
@@ -168,7 +169,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
.collect();
debug!(count = remaining_events.len(), "remaining events");
debug!(list = ?remaining_events, "remaining events");
trace!(list = ?remaining_events, "remaining events");
let have_remaining_events = !remaining_events.is_empty();
let remaining_events = remaining_events
@@ -188,7 +189,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
.unwrap_or(Ok(Vec::new()))?;
debug!(count = sorted_remaining_events.len(), "sorted remaining events");
debug!(list = ?sorted_remaining_events, "sorted remaining events");
trace!(list = ?sorted_remaining_events, "sorted remaining events");
let sorted_remaining_events = sorted_remaining_events
.iter()
@@ -207,7 +208,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
resolved_state.extend(unconflicted_state);
debug!(resolved_state = resolved_state.len(), "resolved state");
debug!(?resolved_state, "resolved state");
trace!(?resolved_state, "resolved state");
Ok(resolved_state)
}
+12
View File
@@ -4,6 +4,12 @@
pub trait BoolExt {
fn and<T>(self, t: Option<T>) -> Option<T>;
#[must_use]
fn and_is(self, b: bool) -> bool;
#[must_use]
fn and_if<F: FnOnce() -> bool>(self, f: F) -> bool;
fn and_then<T, F: FnOnce() -> Option<T>>(self, f: F) -> Option<T>;
#[must_use]
@@ -55,6 +61,12 @@ impl BoolExt for bool {
#[inline]
fn and<T>(self, t: Option<T>) -> Option<T> { self.then_some(t).flatten() }
#[inline]
fn and_if<F: FnOnce() -> Self>(self, f: F) -> Self { self.and_is(f()) }
#[inline]
fn and_is(self, b: Self) -> Self { self && b }
#[inline]
fn and_then<T, F: FnOnce() -> Option<T>>(self, f: F) -> Option<T> { self.then(f).flatten() }
+47 -18
View File
@@ -1,4 +1,5 @@
//! Extended external extensions to futures::FutureExt
#![allow(clippy::many_single_char_names, clippy::impl_trait_in_params)]
use std::marker::Unpin;
@@ -16,8 +17,6 @@ pub trait BoolExt
where
Self: Future<Output = bool> + Send,
{
type Result;
fn or<B>(self, b: B) -> impl Future<Output = bool> + Send
where
B: Future<Output = bool> + Send + Unpin,
@@ -46,15 +45,11 @@ impl<Fut> BoolExt for Fut
where
Fut: Future<Output = bool> + Send,
{
type Result = crate::Result<(), ()>;
fn or<B>(self, b: B) -> impl Future<Output = bool> + Send
where
B: Future<Output = bool> + Send + Unpin,
Self: Sized + Unpin,
{
let test = |test: bool| test.ok_or(Self::Result::Err(()));
select_ok([Left(self.map(test)), Right(b.map(test))]).map(|res| res.is_ok())
}
@@ -63,8 +58,6 @@ fn and<B>(self, b: B) -> impl Future<Output = bool> + Send
B: Future<Output = bool> + Send,
Self: Sized,
{
let test = |test: bool| test.ok_or(Self::Result::Err(()));
try_join(self.map(test), b.map(test)).map(|res| res.is_ok())
}
@@ -74,8 +67,6 @@ fn and2<B, C>(self, b: B, c: C) -> impl Future<Output = bool> + Send
C: Future<Output = bool> + Send,
Self: Sized,
{
let test = |test: bool| test.ok_or(Self::Result::Err(()));
try_join3(self.map(test), b.map(test), c.map(test)).map(|res| res.is_ok())
}
@@ -86,8 +77,6 @@ fn and3<B, C, D>(self, b: B, c: C, d: D) -> impl Future<Output = bool> + Send
D: Future<Output = bool> + Send,
Self: Sized,
{
let test = |test: bool| test.ok_or(Self::Result::Err(()));
try_join4(self.map(test), b.map(test), c.map(test), d.map(test)).map(|res| res.is_ok())
}
}
@@ -97,9 +86,7 @@ pub fn and<I, F>(args: I) -> impl Future<Output = bool> + Send
I: Iterator<Item = F> + Send,
F: Future<Output = bool> + Send,
{
type Result = crate::Result<(), ()>;
let args = args.map(|a| a.map(|a| a.ok_or(Result::Err(()))));
let args = args.map(|a| a.map(test));
try_join_all(args).map(|res| res.is_ok())
}
@@ -109,9 +96,51 @@ pub fn or<I, F>(args: I) -> impl Future<Output = bool> + Send
I: Iterator<Item = F> + Send,
F: Future<Output = bool> + Send + Unpin,
{
type Result = crate::Result<(), ()>;
let args = args.map(|a| a.map(|a| a.ok_or(Result::Err(()))));
let args = args.map(|a| a.map(test));
select_ok(args).map(|res| res.is_ok())
}
pub fn and4(
a: impl Future<Output = bool> + Send,
b: impl Future<Output = bool> + Send,
c: impl Future<Output = bool> + Send,
d: impl Future<Output = bool> + Send,
) -> impl Future<Output = bool> + Send {
a.and3(b, c, d)
}
pub fn and5(
a: impl Future<Output = bool> + Send,
b: impl Future<Output = bool> + Send,
c: impl Future<Output = bool> + Send,
d: impl Future<Output = bool> + Send,
e: impl Future<Output = bool> + Send,
) -> impl Future<Output = bool> + Send {
a.and2(b, c).and2(d, e)
}
pub fn and6(
a: impl Future<Output = bool> + Send,
b: impl Future<Output = bool> + Send,
c: impl Future<Output = bool> + Send,
d: impl Future<Output = bool> + Send,
e: impl Future<Output = bool> + Send,
f: impl Future<Output = bool> + Send,
) -> impl Future<Output = bool> + Send {
a.and3(b, c, d).and2(e, f)
}
pub fn and7(
a: impl Future<Output = bool> + Send,
b: impl Future<Output = bool> + Send,
c: impl Future<Output = bool> + Send,
d: impl Future<Output = bool> + Send,
e: impl Future<Output = bool> + Send,
f: impl Future<Output = bool> + Send,
g: impl Future<Output = bool> + Send,
) -> impl Future<Output = bool> + Send {
a.and3(b, c, d).and3(e, f, g)
}
fn test(test: bool) -> crate::Result<(), ()> { test.ok_or(()) }
+1 -1
View File
@@ -5,7 +5,7 @@
mod ready_eq_ext;
mod try_ext_ext;
pub use bool_ext::{BoolExt, and, or};
pub use bool_ext::{BoolExt, and, and4, and5, and6, and7, or};
pub use ext_ext::ExtExt;
pub use option_ext::OptionExt;
pub use option_stream::OptionStream;
+24
View File
@@ -6,6 +6,14 @@ pub trait OptionExt<T> {
fn is_none_or(self, f: impl FnOnce(&T) -> bool + Send) -> impl Future<Output = bool> + Send;
fn is_some_and(self, f: impl FnOnce(&T) -> bool + Send) -> impl Future<Output = bool> + Send;
fn unwrap_or(self, t: T) -> impl Future<Output = T> + Send;
fn unwrap_or_else(self, f: impl FnOnce() -> T + Send) -> impl Future<Output = T> + Send;
fn unwrap_or_default(self) -> impl Future<Output = T> + Send
where
T: Default;
}
impl<T, Fut> OptionExt<T> for OptionFuture<Fut>
@@ -22,4 +30,20 @@ fn is_none_or(self, f: impl FnOnce(&T) -> bool + Send) -> impl Future<Output = b
fn is_some_and(self, f: impl FnOnce(&T) -> bool + Send) -> impl Future<Output = bool> + Send {
self.map(|o| o.as_ref().is_some_and(f))
}
#[inline]
fn unwrap_or(self, t: T) -> impl Future<Output = T> + Send { self.map(|o| o.unwrap_or(t)) }
#[inline]
fn unwrap_or_else(self, f: impl FnOnce() -> T + Send) -> impl Future<Output = T> + Send {
self.map(|o| o.unwrap_or_else(f))
}
#[inline]
fn unwrap_or_default(self) -> impl Future<Output = T> + Send
where
T: Default,
{
self.map(Option::unwrap_or_default)
}
}
+2
View File
@@ -4,6 +4,7 @@
mod flat_ok;
mod inspect_log;
mod into_is_ok;
mod is_err_or;
mod log_debug_err;
mod log_err;
mod map_expect;
@@ -19,6 +20,7 @@
flat_ok::FlatOk,
inspect_log::{ErrDebugLog, ErrLog},
into_is_ok::IntoIsOk,
is_err_or::IsErrOr,
log_debug_err::LogDebugErr,
log_err::LogErr,
map_expect::MapExpect,
+17
View File
@@ -0,0 +1,17 @@
#![allow(clippy::wrong_self_convention)]
use super::Result;
pub trait IsErrOr<T> {
fn is_err_or<F: FnOnce(T) -> bool>(self, f: F) -> bool;
}
impl<T, E> IsErrOr<T> for Result<T, E> {
#[inline]
fn is_err_or<F>(self, f: F) -> bool
where
F: FnOnce(T) -> bool,
{
if let Ok(t) = self { f(t) } else { true }
}
}
+25 -9
View File
@@ -5,7 +5,7 @@
mod unquoted;
pub use self::{between::Between, split::SplitInfallible, unquote::Unquote, unquoted::Unquoted};
use crate::{Result, utils::exchange};
use crate::{Result, smallstr::SmallString, utils::exchange};
pub const EMPTY: &str = "";
@@ -93,18 +93,34 @@ pub fn camel_to_snake_case<I, O>(output: &mut O, input: I) -> Result
/// ```
#[must_use]
#[allow(clippy::string_slice)]
pub fn common_prefix<'a>(choice: &'a [&str]) -> &'a str {
pub fn common_prefix<T: AsRef<str>>(choice: &[T]) -> &str {
choice.first().map_or(EMPTY, move |best| {
choice.iter().skip(1).fold(*best, |best, choice| {
&best[0..choice
.char_indices()
.zip(best.char_indices())
.take_while(|&(a, b)| a == b)
.count()]
})
choice
.iter()
.skip(1)
.fold(best.as_ref(), |best, choice| {
&best[0..choice
.as_ref()
.char_indices()
.zip(best.char_indices())
.take_while(|&(a, b)| a == b)
.count()]
})
})
}
pub fn to_small_string<const CAP: usize, T>(t: T) -> SmallString<[u8; CAP]>
where
T: std::fmt::Display,
{
use std::fmt::Write;
let mut ret = SmallString::<[u8; CAP]>::new();
write!(&mut ret, "{t}").expect("Failed to Display type in SmallString");
ret
}
/// Parses the bytes into a string.
pub fn string_from_bytes(bytes: &[u8]) -> Result<String> {
let str: &str = str_from_bytes(bytes)?;
+1 -1
View File
@@ -16,7 +16,7 @@ fn common_prefix_empty() {
#[test]
fn common_prefix_none() {
let input = [];
let input: [&str; 0] = [];
let output = super::common_prefix(&input);
assert_eq!(output, "");
}
+1
View File
@@ -57,6 +57,7 @@ async-channel.workspace = true
const-str.workspace = true
ctor.workspace = true
futures.workspace = true
itertools.workspace = true
log.workspace = true
minicbor.workspace = true
minicbor-serde.workspace = true
+1 -1
View File
@@ -274,7 +274,7 @@ fn deserialize_newtype_struct<V>(self, name: &'static str, visitor: V) -> Result
| "$serde_json::private::RawValue" => visitor.visit_map(self),
| "Cbor" => visitor
.visit_newtype_struct(&mut minicbor_serde::Deserializer::new(self.record_trail()))
.map_err(|e| Self::Error::SerdeDe(e.to_string().into())),
.map_err(|e| Self::Error::SerdeDe(format!("{name}: {e}").into())),
| _ => visitor.visit_newtype_struct(self),
}
+1
View File
@@ -43,6 +43,7 @@ fn descriptor_cf_options(
opts.set_max_bytes_for_level_multiplier(1.0);
opts.set_max_bytes_for_level_multiplier_additional(&desc.level_shape);
opts.set_disable_auto_compactions(desc.ignored);
opts.set_compaction_style(desc.compaction);
opts.set_compaction_pri(desc.compaction_pri);
opts.set_universal_compaction_options(&uc_options(&desc));
+9 -1
View File
@@ -10,6 +10,7 @@
#[derive(Debug, Clone, Copy)]
pub(crate) struct Descriptor {
pub(crate) name: &'static str,
pub(crate) ignored: bool,
pub(crate) dropped: bool,
pub(crate) cache_disp: CacheDisp,
pub(crate) key_size_hint: Option<usize>,
@@ -52,6 +53,7 @@ pub(crate) enum CacheDisp {
/// Base descriptor supplying common defaults to all derived descriptors.
static BASE: Descriptor = Descriptor {
name: EMPTY,
ignored: false,
dropped: false,
cache_disp: CacheDisp::Shared,
key_size_hint: None,
@@ -83,8 +85,14 @@ pub(crate) enum CacheDisp {
auto_readahead_max: 1024 * 1024 * 2,
};
/// Placeholder descriptor for existing columns which have no description.
/// Automatically generated on db open; should not appear in any schema.
pub(crate) static IGNORED: Descriptor = Descriptor { ignored: true, ..BASE };
/// Tombstone descriptor for columns which have been or will be deleted.
pub(crate) static DROPPED: Descriptor = Descriptor { dropped: true, ..BASE };
/// Descriptors of this kind are explicitly set to delete data. Care should be
/// taken when using this as it inhibits downgrading and other migrations.
pub(crate) static DROPPED: Descriptor = Descriptor { dropped: true, ..IGNORED };
/// Descriptor for large datasets with random updates across the keyspace.
pub(crate) static RANDOM: Descriptor = Descriptor {
+92 -33
View File
@@ -4,16 +4,13 @@
sync::{Arc, atomic::AtomicU32},
};
use itertools::Itertools;
use rocksdb::{ColumnFamilyDescriptor, Options};
use tuwunel_core::{Result, debug, implement, info, warn};
use tuwunel_core::{Result, debug, debug_warn, implement, info, trace, warn};
use super::{
Db, Engine,
cf_opts::cf_options,
context,
db_opts::db_options,
descriptor::{self, Descriptor},
repair::repair,
Db, Engine, cf_opts::cf_options, context, db_opts::db_options, descriptor,
descriptor::Descriptor, repair::repair,
};
use crate::{Context, or_else};
@@ -31,7 +28,7 @@ pub(crate) async fn open(ctx: Arc<Context>, desc: &[Descriptor]) -> Result<Arc<S
&ctx.row_cache.lock().expect("row cache locked"),
)?;
let cfds = Self::configure_cfds(&ctx, &db_opts, desc)?;
let (cfds, dropped) = Self::configure_cfds(&ctx, &db_opts, desc)?;
let num_cfds = cfds.len();
debug!("Configured {num_cfds} column descriptors...");
@@ -50,6 +47,13 @@ pub(crate) async fn open(ctx: Arc<Context>, desc: &[Descriptor]) -> Result<Arc<S
}
.or_else(or_else)?;
if !config.rocksdb_read_only && !config.rocksdb_secondary {
for name in &dropped {
debug!("Deleting dropped column {name:?} ...");
db.drop_cf(name).or_else(or_else)?;
}
}
info!(
columns = num_cfds,
sequence = %db.latest_sequence_number(),
@@ -74,59 +78,114 @@ fn configure_cfds(
ctx: &Arc<Context>,
db_opts: &Options,
desc: &[Descriptor],
) -> Result<Vec<ColumnFamilyDescriptor>> {
) -> Result<(Vec<ColumnFamilyDescriptor>, Vec<String>)> {
let server = &ctx.server;
let config = &server.config;
let path = &config.database_path;
let existing = Self::discover_cfs(path, db_opts);
let creating = desc
.iter()
.filter(|desc| !existing.contains(desc.name));
// Found columns which are not described.
let missing = existing
.iter()
.map(String::as_str)
.filter(|&name| name != "default")
.filter(|&name| !desc.iter().any(|desc| desc.name == name));
// Described columns which are not found.
let creating = desc
.iter()
.filter(|desc| !desc.dropped)
.filter(|desc| !existing.contains(desc.name));
// Found columns which are described as dropped.
let dropping = desc
.iter()
.filter(|desc| desc.dropped)
.filter(|desc| existing.contains(desc.name))
.filter(|_| !config.rocksdb_never_drop_columns);
// Described dropped columns which are no longer found.
let dropped = desc
.iter()
.filter(|desc| desc.dropped)
.filter(|desc| !existing.contains(desc.name));
debug!(
existing = existing.len(),
described = desc.len(),
missing = missing.clone().count(),
dropped = dropped.clone().count(),
creating = creating.clone().count(),
dropping = dropping.clone().count(),
"Discovered database columns"
);
missing.clone().for_each(|name| {
debug!("Found unrecognized column {name:?} in existing database.");
debug_warn!("Found undescribed column {name:?} in existing database.");
});
creating.map(|desc| desc.name).for_each(|name| {
debug!("Creating new column {name:?} not previously found in existing database.");
});
let missing_descriptors = missing
dropped
.clone()
.filter(|_| config.rocksdb_drop_missing_columns)
.map(|_| descriptor::DROPPED);
.map(|desc| desc.name)
.for_each(|name| {
debug!("Previously dropped column {name:?} no longer found in database.");
});
let cfopts: Vec<_> = desc
creating
.clone()
.map(|desc| desc.name)
.for_each(|name| {
debug!("Creating new column {name:?} not previously found in existing database.");
});
dropping
.clone()
.map(|desc| desc.name)
.for_each(|name| {
warn!(
"Column {name:?} has been scheduled for deletion. Storage may not appear \
reclaimed until further restart or compaction."
);
});
let not_dropped = |desc: &&Descriptor| {
!dropped
.clone()
.any(|dropped| desc.name == dropped.name)
};
let dropping = dropping
.map(|desc| desc.name)
.map(ToOwned::to_owned)
.collect();
let cfnames = desc
.iter()
.copied()
.chain(missing_descriptors)
.map(|ref desc| cf_options(ctx, db_opts.clone(), desc))
.collect::<Result<_>>()?;
.filter(not_dropped)
.map(|desc| desc.name)
.chain(missing.clone());
let cfds: Vec<_> = desc
.iter()
.map(|desc| desc.name)
.map(ToOwned::to_owned)
.chain(missing.cloned())
.zip(cfopts.into_iter())
.map(|(name, opts)| ColumnFamilyDescriptor::new(name, opts))
.collect();
.filter(not_dropped)
.copied()
.chain(missing.map(|_| descriptor::IGNORED))
.zip(cfnames)
.inspect(|&(desc, name)| {
assert!(
desc.ignored || desc.name == name,
"{name:?} does not match descriptor {:?}",
desc.name
);
})
.inspect(|&(_, name)| debug!(name, "Described column"))
.map(|(desc, name)| (desc, name.to_owned()))
.map(|(desc, name)| Ok((name, cf_options(ctx, db_opts.clone(), &desc)?)))
.map_ok(|(name, opts)| ColumnFamilyDescriptor::new(name, opts))
.collect::<Result<_>>()?;
Ok(cfds)
trace!(?dropping);
Ok((cfds, dropping))
}
#[implement(Engine)]
+2
View File
@@ -20,4 +20,6 @@ pub fn remove<K>(&self, key: &K)
if !self.engine.corked() {
self.engine.flush().expect("database flush error");
}
self.notify(key.as_ref());
}
+19 -5
View File
@@ -8,11 +8,12 @@
use futures::pin_mut;
use serde::Serialize;
use tokio::sync::watch::{Sender, channel};
use tuwunel_core::implement;
use tuwunel_core::{debug, implement, smallvec::SmallVec};
use crate::keyval::{KeyBuf, serialize_key};
type Watchers = Mutex<BTreeMap<KeyBuf, Sender<()>>>;
type KeyVec = SmallVec<[KeyBuf; 1]>;
#[derive(Default)]
pub(super) struct Watch {
@@ -57,6 +58,14 @@ pub fn watch_raw_prefix<'a, K>(&self, prefix: &'a K) -> impl Future<Output = ()>
}
#[implement(super::Map)]
#[tracing::instrument(
level = "trace",
skip_all,
fields(
map = self.name(),
key = str::from_utf8(key.as_ref()).unwrap_or("<binary>"),
)
)]
pub(crate) fn notify<K>(&self, key: &K)
where
K: AsRef<[u8]> + Ord + ?Sized,
@@ -65,15 +74,20 @@ pub(crate) fn notify<K>(&self, key: &K)
let mut watchers = self.watch.watchers.lock().expect("locked");
watchers
let num_notified = watchers
.range(range)
.rev()
.take_while(|(k, _)| key.as_ref().starts_with(k))
.filter_map(|(k, tx)| tx.send(()).is_err().then_some(k))
.cloned()
.collect::<Vec<_>>()
.collect::<KeyVec>()
.into_iter()
.for_each(|k| {
watchers.remove(&k);
.fold(0_usize, |num_notified, key| {
watchers.remove(&key);
num_notified.saturating_add(1)
});
if num_notified > 0 {
debug!(watchers = watchers.len(), num_notified, "notified");
}
}
+27 -4
View File
@@ -16,6 +16,7 @@ pub(super) fn open(engine: &Arc<Engine>) -> Result<Maps> { open_list(engine, MAP
#[tracing::instrument(name = "maps", level = "debug", skip_all)]
pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
maps.iter()
.filter(|desc| !desc.dropped)
.map(|desc| Ok((desc.name, Map::open(engine, desc.name)?)))
.collect()
}
@@ -165,6 +166,10 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
name: "roomid_joinedcount",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "roomid_maxremotepowerlevel",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "roomid_pduleaves",
..descriptor::RANDOM_SMALL
@@ -183,6 +188,10 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
name: "roomserverids",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "roomsynctoken_shortstatehash",
..descriptor::DROPPED
},
Descriptor {
name: "roomuserdataid_accountdata",
..descriptor::RANDOM_SMALL
@@ -200,6 +209,10 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
name: "roomuserid_lastprivatereadupdate",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "roomuserid_lastnotificationread",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "roomuserid_leftcount",
val_size_hint: Some(8),
@@ -337,6 +350,11 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
name: "userdeviceid_metadata",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userdeviceconnid_conn",
block_size: 1024 * 16,
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userdeviceid_refresh",
..descriptor::RANDOM_SMALL
@@ -365,6 +383,10 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
name: "userid_blurhash",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_dehydrateddevice",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_devicelistversion",
..descriptor::RANDOM_SMALL
@@ -401,6 +423,11 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
name: "userid_usersigningkeyid",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "useridcount_notification",
limit_size: 1024 * 1024 * 256,
..descriptor::RANDOM_SMALL_CACHE
},
Descriptor {
name: "useridprofilekey_value",
..descriptor::RANDOM_SMALL
@@ -429,8 +456,4 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
name: "userroomid_notificationcount",
..descriptor::RANDOM
},
Descriptor {
name: "roomid_maxremotepowerlevel",
..descriptor::RANDOM_SMALL
},
];
+1 -1
View File
@@ -1,4 +1,4 @@
#![type_length_limit = "8192"]
#![type_length_limit = "65536"]
extern crate rust_rocksdb as rocksdb;
+48 -1
View File
@@ -9,7 +9,7 @@
};
use crate::{
Ignore, Interfix, de, ser,
Cbor, Ignore, Interfix, de, ser,
ser::{Json, serialize_to_vec},
};
@@ -204,6 +204,53 @@ fn ser_json_raw_json() {
assert_eq!(&s, r#"{"event_fields":["content.body"]}"#);
}
#[test]
fn ser_cbor() {
use tuwunel_core::ruma::api::client::filter::FilterDefinition;
let filter = FilterDefinition {
event_fields: Some(vec!["content.body".to_owned()]),
..Default::default()
};
let serialized = serialize_to_vec(Cbor(&filter)).expect("failed to serialize cbor");
let deserialized: FilterDefinition = de::from_slice::<Cbor<_>>(&serialized)
.expect("failed to deserialize cbor")
.0;
assert_eq!(filter.event_fields, deserialized.event_fields);
}
#[test]
#[cfg(disable)]
fn ser_cbor_ruma_raw() {
use serde_json::value::RawValue;
use tuwunel_core::ruma::api::client::filter::FilterDefinition;
struct Foo {
a: String,
b: Box<RawValue>,
}
let filter = FilterDefinition {
event_fields: Some(vec!["content.body".to_owned()]),
..Default::default()
};
let foo = Foo {
a: "test".into(),
b: serde_json::value::to_raw_value(&filter).expect("failed to serialize to raw value"),
};
let serialized = serialize_to_vec(Cbor(&foo)).expect("failed to serialize cbor");
let deserialized: Foo = de::from_slice::<Cbor<_>>(&serialized)
.expect("failed to deserialize cbor")
.0;
assert_eq!(foo.a, deserialized.a);
assert_eq!(foo.a.get(), deserialized.b.get());
}
#[test]
fn de_tuple() {
let user_id: &UserId = "@user:example.com".try_into().unwrap();
+2 -1
View File
@@ -169,10 +169,11 @@ pub async fn last_count<'a>(
&'a self,
room_id: Option<&'a RoomId>,
user_id: &'a UserId,
upper: u64,
upper: Option<u64>,
) -> Result<u64> {
type Key<'a> = (Option<&'a RoomId>, &'a UserId, u64, Ignore);
let upper = upper.unwrap_or(u64::MAX);
let key = (room_id, user_id, upper, Interfix);
self.db
.roomuserdataid_accountdata
+103
View File
@@ -0,0 +1,103 @@
use ruma::{UserId, events::TimelineEventType};
use tuwunel_core::{
Result, error, implement,
matrix::{
event::Event,
pdu::{Pdu, RawPduId},
},
utils::ReadyExt,
};
use super::{NamespaceRegex, RegistrationInfo};
/// Called by timeline::append() after accepting new PDU.
#[implement(super::Service)]
#[tracing::instrument(name = "append", level = "debug", skip_all)]
pub(crate) async fn append_pdu(&self, pdu_id: RawPduId, pdu: &Pdu) -> Result {
for appservice in self.read().await.values() {
self.append_pdu_to(appservice, pdu_id, pdu)
.await
.inspect_err(|e| {
error!(
event_id = %pdu.event_id(),
appservice = ?appservice.registration.id,
"Failed to send PDU to appservice: {e}"
);
})
.ok();
}
Ok(())
}
#[implement(super::Service)]
#[tracing::instrument(
name = "append_to",
level = "debug",
skip_all,
fields(id = %appservice.registration.id),
)]
async fn append_pdu_to(
&self,
appservice: &RegistrationInfo,
pdu_id: RawPduId,
pdu: &Pdu,
) -> Result {
if self
.services
.state_cache
.appservice_in_room(pdu.room_id(), appservice)
.await
{
self.services
.sending
.send_pdu_appservice(appservice.registration.id.clone(), pdu_id)?;
return Ok(());
}
// If the RoomMember event has a non-empty state_key, it is targeted at someone.
// If it is our appservice user, we send this PDU to it.
if *pdu.kind() == TimelineEventType::RoomMember {
if let Some(state_key_uid) = &pdu
.state_key
.as_ref()
.and_then(|state_key| UserId::parse(state_key.as_str()).ok())
{
let appservice_uid = appservice.registration.sender_localpart.as_str();
if state_key_uid == &appservice_uid {
self.services
.sending
.send_pdu_appservice(appservice.registration.id.clone(), pdu_id)?;
return Ok(());
}
}
}
let matching_users = |users: &NamespaceRegex| {
appservice.users.is_match(pdu.sender().as_str())
|| *pdu.kind() == TimelineEventType::RoomMember
&& pdu
.state_key
.as_ref()
.is_some_and(|state_key| users.is_match(state_key))
};
let matching_aliases = |aliases: NamespaceRegex| {
self.services
.alias
.local_aliases_for_room(pdu.room_id())
.ready_any(move |room_alias| aliases.is_match(room_alias.as_str()))
};
if matching_aliases(appservice.aliases.clone()).await
|| appservice.rooms.is_match(pdu.room_id().as_str())
|| matching_users(&appservice.users)
{
self.services
.sending
.send_pdu_appservice(appservice.registration.id.clone(), pdu_id)?;
}
Ok(())
}
+3
View File
@@ -1,5 +1,7 @@
mod append;
mod namespace_regex;
mod registration_info;
pub(crate) mod request;
use std::{
collections::{BTreeMap, HashSet},
@@ -14,6 +16,7 @@
use tuwunel_core::{Err, Result, debug, err, utils::stream::IterStream};
use tuwunel_database::Map;
pub(crate) use self::request::send_request;
pub use self::{namespace_regex::NamespaceRegex, registration_info::RegistrationInfo};
pub struct Service {

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