Compare commits

...
119 Commits
Author SHA1 Message Date
Jason Volk fb4dfafe2d Bump 1.4.2
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-12 04:13:34 +00:00
Jason Volk 8dbea51968 Slightly decrease the default OTK limit.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-12 04:13:34 +00:00
Jason Volk 7b5905bbca Remove inapposite readme sections on versioning.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-12 04:13:34 +00:00
Jason Volk 42d143c013 Allow matching for passthru resolver on all resolution paths. (#158)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-12 04:13:27 +00:00
Jason Volk 10fb1cd192 Purge room synctokens during deletion.
Purge last notification read counts.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-11 08:56:38 +00:00
Jason Volk 1322ba1b00 Send unmaintained hardened_malloc-rs to valhalla.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-11 08:56:38 +00:00
Jason Volk d73b4332bf Fix deletion of local leave state to allow sync for clients.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-11 06:20:41 +00:00
Jason Volk 11eeca7e01 Prune one time keys based on reasonable configured limit.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-11 04:27:33 +00:00
Jason Volk 19dc5fafd4 Fix missing device_one_time_keys_count in empty timeout responses.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 21:29:04 +00:00
Jason Volk 54b347b855 Abstract and dedup the general UIAA pattern into api::router.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 10:35:49 +00:00
Jason Volk 8ed61aecb0 Split api::router::auth server/appservice units.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 05:41:44 +00:00
Jason Volk bf8aa57d03 Add config option for dns passthru for appservices. (#158)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 01:17:40 +00:00
Jason Volk a9f1926654 Use dns_resolver2 per reqwest client builder deprecation notice.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 00:59:50 +00:00
Jason Volk 248d778290 Add alternative resolver path with passthru cache-characteristics. (resolves #158)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-10 00:01:26 +00:00
Jason Volk 2e13e87e43 docs: Update the Helm Chart link for Kubernetes.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-09 22:47:33 +00:00
Jason Volk ba19d407d0 Add config to control m.federate in room create events. (fixes #151)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-09 04:46:19 +00:00
Jason Volk de7c5dcbc8 Make iss and aud args optional to debug create-jwt command.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-09 03:03:53 +00:00
dasha_uwuandJason Volk adadafa88f Fix alias resolution logic; element-web v1.11.111 compat
element-hq/element-web#30641
element-hq/element-web#30611

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-09 02:28:19 +00:00
Jason Volk deda746222 Support JWT authentication on deactivate route.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-09 02:28:19 +00:00
Jason Volk 29c2c8a333 Branch based on config to note sync for push suppression.
Minor if-let condition and scope reduction; formatting.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 22:25:00 +00:00
tototomate123andJason Volk 296018f0cc mark notification suppression as experimental 2025-09-08 22:25:00 +00:00
tototomate123andJason Volk 6295ca135a feature marked as experimental and executed rustfmt 2025-09-08 22:25:00 +00:00
tototomate123andJason Volk b5a9884194 feat(presence,push) optionally suppress push notifications for active users 2025-09-08 22:25:00 +00:00
Jason Volk 1be7fd9247 Fix cfg condition for debug logging (996a5488be)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 22:25:00 +00:00
Jason Volk be1264965a Refactor sliding-sync.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 00:07:59 +00:00
Jason Volk f6d2ce2f22 Greenlight hydra backports.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 00:07:59 +00:00
Jason Volk 3cbe1e25cd Cleanup Resolver construction/configuration.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 00:07:59 +00:00
Jason Volk 897322964c Fix dns_min_ttl config doc.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 00:07:59 +00:00
Jason Volk 996a5488be ci: Add build for release-logging, optimized build with debug logging.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-08 00:07:59 +00:00
Jason Volk 0ed4bd1e07 Only ping presence for public read receipts.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-07 21:39:20 +00:00
Jason Volk da79de5381 Add cfg to preclude log elision without requiring debug_assertions.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-07 21:39:20 +00:00
Jason Volk 1bcf3ae19a Soften the Docker troubleshooting note.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-07 20:50:14 +00:00
Jason Volk 91e500efe3 Further tweak troubleshooting formatting.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 22:11:42 +00:00
Jason Volk 35b1ebaf3b Bump dependencies.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 07:45:04 +00:00
Jason Volk ea5deb7e85 Generate CoC HTML inside book directory. (closes #155)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 07:45:04 +00:00
Jason Volk 542cc51047 ci: Add docs test step.
docker: Add docs test target.

docker: Add mising color args to rustfmt.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 06:49:45 +00:00
Jason Volk 3225db0ddd Add external repair tool step to db repair troubleshooting.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk a164da8b58 Fix and extend the apply! util macro.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 7221d466ce Ignore canonical_alias with removed alias for directory results.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk f6c88e3a16 Add query for alias existing; tweak tracing spans.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 3deebeab78 Fix sliding-sync room_avatar conditions.
Eliminate jsOption from non-sliding-sync avatar related codepaths.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 1d2818de58 Round upward for events_after in /context limit.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 86781522b6 Allow state visibility for once-joined users when history_visibility=shared.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 4b49aaad53 Fix spec compliance limiting /joined_rooms to current members only.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 3a78ba2b16 Support v1/v2 conditions for join/leave, creation and other operations. (#12)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 685946faed Cleanup validate/verify suite room version related; optimize event_id insert. (#12)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 13c9385ef7 Add v1/v2 support to gen_event_id() scheme. (#12)
Add v1/v2 and improve reference and content hashing suite. (#12)

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk 6a583a359e Support v1/v2 prev_events/auth_events outgoing federation format. (#12)
Support v1/v2 prev_events/auth_events when handling outlier pdu. (#12)

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Jason Volk eec6e45358 Improve error messages for missing auth event and invalid join validations.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-05 05:45:41 +00:00
Alexandru ScvorțovandJason Volk 804257eb52 Fix broken doctest in src/core/utils/debug.rs 2025-09-04 22:21:14 +00:00
Jason Volk a1677f05d3 Bump 1.4.1
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-02 09:43:50 +00:00
Jason Volk 1f77200299 Mark MSC4297 compliance as flake.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-02 06:29:40 +00:00
Jason Volk 888b93df91 Fix /joined_members results. (regression a3666fe672) (fixes #147)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-02 05:34:37 +00:00
Jason Volk a127724eaa Bump 1.4.0
Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-01 11:34:08 +00:00
Jason Volk 00102c4276 ci: Skip unnecessary amd64 -v2 and -v3 builds in test-profile mode.
docker: Use env override for git_checkout.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-09-01 02:33:00 +00:00
dasha_uwu b59af0530c Deprioritize joins through configured servers 2025-08-31 12:30:27 +05:00
dasha_uwu 44af849ed0 Dedup base join servers 2025-08-31 11:30:10 +05:00
Jason Volk 26e7a8e962 Bump 1.4.0 Preview
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-30 20:27:39 +00:00
Jason Volk aa413ae601 Implement room purge after last local user leaves. (resolves #83)
Consume a state_lock for room delete call.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-30 11:45:32 +00:00
Jason Volk 7c19d1e1ff Fix unnecessary backfill requests after reaching m.room.create.
Cleanup condition.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-30 11:45:32 +00:00
Jason Volk 616f20f273 Fix unused lifetims.
Fix unused pub(crate).

Allow implicit hasher.

Fix unused self.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-30 11:13:14 +00:00
Jason Volk 91efcf8a8e Bump and unpatch tracing-subscriber.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-30 10:51:55 +00:00
Jason Volk 8708e98478 ci: Tweak docker cache sizes.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-30 10:16:43 +00:00
Jason Volk 4bdc260015 Update the troubleshooting for easier rocksdb recovery/repair.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-30 10:16:43 +00:00
Jason Volk 7bc47e58d0 Add state_cache iteration for local pending invitees to room.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-30 10:04:24 +00:00
Jason Volk 6172b9c8ec Replace legacy federation proto-event formatter.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-30 10:04:24 +00:00
k0tlin 9dc16eeab7 Adjust generic docs
Match readme instruction with doc
2025-08-30 00:45:41 +03:00
k0tlin 402f7a2157 Update example caddy config 2025-08-30 00:35:38 +03:00
Jason Volk 9c6e0e3350 Stabilize room version 12. (#126)
Bless complement.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 07:49:36 +00:00
Jason Volk 12bdfe3b66 Optimize backfill_if_required conditions.
Optimize backfill_pdu.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 06:49:04 +00:00
dasha_uwuandJason Volk 54ce221e88 Add active request assert on tls listener shutdown 2025-08-29 06:49:04 +00:00
dasha_uwuandJason Volk 33a16f4831 Make /sync respond early on server shutdown 2025-08-29 06:49:04 +00:00
Jason Volk d8e6d95f4b Fix device verification crutch (regression 628597c318); cleanup. (fixes #134)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 06:49:04 +00:00
Jason Volk 9c290bc513 Fix latest key backup determination.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 06:49:04 +00:00
Jason Volk a4520424bd Adjust several log levels; cleanup unformatted log call.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 06:49:04 +00:00
Jason Volk 280d7733fc Update capability for MSC4133 profile fields.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 06:49:04 +00:00
Jason Volk 79e3c898ac Fix /send_join response event format. Optimize.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 06:49:04 +00:00
Jason Volk 757d38e891 Optimize expected auth types checking.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 06:49:04 +00:00
Jason Volk 4b66911886 Eliminate redundant duplicate auth and matching room check.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 06:49:04 +00:00
Jade EllisandJason Volk 9db3e35f41 Fix incorrect command descriptions. [ci skip] 2025-08-29 02:17:46 +00:00
Jason Volk 7977512d5d Fix additional cases for room_id.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 02:17:46 +00:00
Jason Volk e354be6830 Simplify Pdu from_id_val constructions.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 02:17:46 +00:00
Jason Volk f58081838e Log error for send_join auth_check failure.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-29 02:17:46 +00:00
dasha_uwuandJason Volk 526a138021 Force promotion (closes #136) 2025-08-29 02:17:46 +00:00
dasha_uwuandJason Volk 7fa6894334 Remove excessive doc on admin command 2025-08-29 02:17:46 +00:00
dasha_uwuandJason Volk 33d0ad1be2 Remove dead link in readme 2025-08-29 02:17:46 +00:00
Jason Volkanddasha_uwu fbc3892904 ci: Disable lychee for now due to excessive build time issue during install.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-28 23:28:20 +05:00
Jason Volkanddasha_uwu ce6212b161 Add room_version argument and reorg convert_outgoing_federation_event.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-28 23:28:20 +05:00
Jason Volkanddasha_uwu 0aea56f02a Optimize OnceServices; simplify init.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-28 23:28:20 +05:00
dasha_uwu d05d3f710f Room deletion (fixes #43) 2025-08-28 23:28:20 +05:00
Jason Volkanddasha_uwu 7a3496869b Remove associated pushers on device delete. (fixes #120)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-28 23:28:20 +05:00
dasha_uwu 146b72265d Skip serialization for appservice id 2025-08-28 23:28:20 +05:00
dasha_uwu 120a43cceb Fix joining restricted rooms with invite 2025-08-28 23:28:20 +05:00
dasha_uwu c40671bfb0 Remove api crate dependency from admin crate 2025-08-28 23:28:20 +05:00
dasha_uwu 8e9c6661b2 Membership refactor
Introduce deactivate, membership services

Move membership and user deactivation functions from api crate into those services
2025-08-28 23:28:20 +05:00
dasha_uwu 6810604629 banned_room_check: pull out common part 2025-08-28 23:28:20 +05:00
dasha_uwu 3dd984eff9 clippy: add must_use attribute 2025-08-28 23:28:20 +05:00
dasha_uwu bcd4f80149 Move server user presence handling to presence service 2025-08-28 23:28:20 +05:00
dasha_uwu f51a7b12b5 Make Service.interrupt async 2025-08-28 23:28:20 +05:00
dasha_uwu 3a1649bcaf Add config option for creating admin room non-federated (closes #118) 2025-08-28 23:28:20 +05:00
dasha_uwu d4421422b2 Remove explicit unstable thirdparty/protocols route
Already handled by the other ruma route
2025-08-28 23:28:20 +05:00
dasha_uwu 4884c55be7 Split disable_room, ban_room functions 2025-08-28 23:28:13 +05:00
Jason Volkanddasha_uwu a4f589f475 Remove unnecessary recursion_limit.
Reduce type_length_limits.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-28 23:20:44 +05:00
dasha_uwu b5890b9664 Services refactor
Replace structs of Dep<Service> with OnceServices, so each service has a Services reference

Remove service name => Service map

Flatten Services.rooms

Make reqwest Clients lazy initialized (client service)
2025-08-28 23:15:59 +05:00
Jason Volk 26b3a84b88 Fix updated serde type expected by database deserialize_any.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-27 00:41:13 +00:00
Jason Volk 3430d4ac86 Improved solution to current_shortstatehash > next_batch. (59b62b1)
`current_shortstatehash` is now calculated from timeline which is already
upper-bound by the `next_batch` snapshot. Previously `current_shortstatehash`
was truly current and had to be disallowed from exceeding `next_batch` by
ignoring the room during sync until a future pass when these values finally
met that condition.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-27 00:41:12 +00:00
Jason Volk 3e2f2fbffa Bump Ruma post-hydra.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-27 00:41:12 +00:00
Jason Volk 74deb14ca0 Bump Complement post-hydra.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-27 00:41:12 +00:00
Jason Volk acd930b9ca Replace clippy::string_to_string with clippy::implicit_clone
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-26 08:49:26 +00:00
Jason Volk e2db2029c5 Add note directing users to docker deployment docs.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-26 02:40:25 +00:00
Jason Volk 4bb83a1972 Fix version 12 outgoing federation format.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-26 02:40:25 +00:00
Jason Volk 751aa27859 Bump dependencies for yanked slab.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-14 02:25:54 +00:00
Jason Volk 628597c318 State-reset and security mitigations.
Upgrade Ruma to present.

The following are intentionally benign for activation in a later commit:

- Hydra backports not default.
- Room version 12 not default.
- Room version 12 not listed as stable.

Do not enable them manually or you can brick your database.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-11 18:45:28 +00:00
Jason Volk 2c6dd78502 Reduce log level on router::args returned error.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-11 11:32:53 +00:00
Jason Volk cdb0e31b21 Allow clippy::default_trait_access.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-11 11:32:53 +00:00
dasha_uwuandJason Volk 6a6f77a100 Simplify feature condition.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-11 10:24:40 +00:00
Jason Volk a3666fe672 Use member profile rather than global profile for joined_members (fixes #121)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-10 23:52:03 +00:00
Jason Volk 0c8df52d18 Bump dependencies.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-07 04:52:46 +00:00
Jason Volk d7e5c0ccf3 Optimize spaces pageload; pipeline outer loop.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-08-07 03:16:32 +00:00
278 changed files with 22083 additions and 11703 deletions
+2
View File
@@ -0,0 +1,2 @@
[env]
RUMA_UNSTABLE_EXHAUSTIVE_TYPES = "true"
+2
View File
@@ -106,6 +106,7 @@ jobs:
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
# disabled due to excessive build time issue installing cargo lychee
lychee:
if: >
!failure() && !cancelled()
@@ -113,6 +114,7 @@ jobs:
&& 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])
&& false
name: Lychee
uses: ./.github/workflows/bake.yml
+17 -5
View File
@@ -58,8 +58,8 @@ jobs:
- name: Initialize Builder
env:
runner: ${{matrix.runner}}
reserved_space: '{"het": "384GB", "aws": "48GB", "gcp": "160GB"}'
max_used_space: '{"het": "512GB", "aws": "64GB", "gcp": "192GB"}'
reserved_space: '{"het": "128GB", "aws": "48GB", "gcp": "160GB"}'
max_used_space: '{"het": "256GB", "aws": "64GB", "gcp": "192GB"}'
run: |
set +e
docker buildx inspect "${GITHUB_ACTOR}"
@@ -129,17 +129,21 @@ jobs:
machines: ${{needs.init.outputs.machines}}
excludes: >
[
{"cargo_profile": "test", "feat_set": "logging"},
{"cargo_profile": "test", "rust_toolchain": "stable", "feat_set": "none"},
{"cargo_profile": "test", "rust_target": "aarch64-unknown-linux-gnu"},
{"cargo_profile": "release", "rust_toolchain": "nightly", "feat_set": "none"},
{"cargo_profile": "release", "rust_toolchain": "nightly", "feat_set": "default"},
{"cargo_profile": "release", "rust_toolchain": "nightly", "feat_set": "logging"},
{"cargo_profile": "release-debuginfo"},
{"cargo_profile": "release-native", "rust_toolchain": "stable"},
{"cargo_profile": "release-native", "feat_set": "none"},
{"cargo_profile": "release-native", "feat_set": "default"},
{"cargo_profile": "release-native", "feat_set": "logging"},
{"cargo_profile": "release-native", "rust_toolchain": "stable"},
{"cargo_profile": "release-native", "rust_target": "aarch64-unknown-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "none"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "default"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "logging"},
{"rust_target": "aarch64-unknown-linux-gnu", "rust_toolchain": "nightly"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v1-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v2-linux-gnu"},
@@ -174,15 +178,17 @@ 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"},
{"cargo_profile": "release-native", "rust_toolchain": "stable"},
{"cargo_profile": "release-native", "feat_set": "default"},
{"cargo_profile": "release-native", "feat_set": "none"},
{"cargo_profile": "release-native", "feat_set": "default"},
{"cargo_profile": "release-native", "rust_toolchain": "stable"},
{"cargo_profile": "release-native", "rust_target": "aarch64-unknown-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "none"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "default"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "logging"},
{"rust_target": "aarch64-unknown-linux-gnu", "rust_toolchain": "nightly"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v1-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v2-linux-gnu"},
@@ -228,6 +234,7 @@ jobs:
{"cargo_profile": "test"},
{"cargo_profile": "release-native"},
{"cargo_profile": "release-debuginfo", "feat_set": "default"},
{"cargo_profile": "release-debuginfo", "feat_set": "logging"},
{"cargo_profile": "release-debuginfo", "rust_toolchain": "nightly"},
{"cargo_profile": "release-debuginfo", "bake_target": "oci"},
{"cargo_profile": "release-debuginfo", "rust_target": "aarch64-unknown-linux-gnu"},
@@ -236,6 +243,7 @@ jobs:
{"cargo_profile": "release-debuginfo", "sys_target": "x86_64-v4-linux-gnu"},
{"cargo_profile": "release", "rust_toolchain": "nightly"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "default"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "logging"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v1-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v2-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v3-linux-gnu"},
@@ -245,10 +253,13 @@ jobs:
{"sys_target": "x86_64-v1-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v2-linux-gnu", "feat_set": "logging"},
{"sys_target": "x86_64-v3-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "logging"},
{"sys_target": "x86_64-v4-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "logging"},
]
publish:
@@ -273,6 +284,7 @@ jobs:
excludes: >
[
{"feat_set": "none"},
{"feat_set": "logging"},
{"cargo_profile": "test"},
{"cargo_profile": "release-debuginfo"},
{"cargo_profile": "release-native"},
+31 -1
View File
@@ -45,6 +45,28 @@ on:
type: string
jobs:
docs:
if: >
contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
&& 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])
name: Docs
uses: ./.github/workflows/bake.yml
with:
bake_targets: '["docs"]'
cargo_profiles: '["test"]'
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}}
excludes: ${{inputs.excludes}}
includes: ${{inputs.includes}}
unit:
if: >
contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
@@ -89,13 +111,16 @@ jobs:
excludes: >
[
{"cargo_profile": "test", "feat_set": "default"},
{"cargo_profile": "test", "feat_set": "logging"},
{"cargo_profile": "test", "feat_set": "none", "bake_target": "smoke-valgrind"},
{"cargo_profile": "test", "rust_toolchain": "stable"},
{"cargo_profile": "test", "rust_target": "aarch64-unknown-linux-gnu"},
{"cargo_profile": "test", "sys_target": "x86_64-v2-linux-gnu", "bake_target": "smoke-valgrind"},
{"cargo_profile": "test", "sys_target": "x86_64-v2-linux-gnu"},
{"cargo_profile": "test", "sys_target": "x86_64-v3-linux-gnu"},
{"cargo_profile": "release", "rust_toolchain": "nightly"},
{"cargo_profile": "release", "rust_toolchain": "stable", "feat_set": "none"},
{"cargo_profile": "release", "bake_target": "smoke-valgrind"},
{"cargo_profile": "release-debuginfo", "feat_set": "logging"},
{"cargo_profile": "release-debuginfo", "rust_toolchain": "nightly"},
{"cargo_profile": "release-debuginfo", "rust_toolchain": "stable", "feat_set": "none"},
{"cargo_profile": "release-debuginfo", "bake_target": "smoke"},
@@ -103,6 +128,7 @@ jobs:
{"cargo_profile": "release-native", "rust_toolchain": "stable"},
{"cargo_profile": "release-native", "feat_set": "none"},
{"cargo_profile": "release-native", "feat_set": "default"},
{"cargo_profile": "release-native", "feat_set": "logging"},
{"cargo_profile": "release-native", "bake_target": "smoke-valgrind"},
{"cargo_profile": "release-native", "rust_target": "aarch64-unknown-linux-gnu"},
{"cargo_profile": "release-native", "sys_target": "x86_64-v2-linux-gnu"},
@@ -110,6 +136,7 @@ jobs:
{"cargo_profile": "release-native", "sys_target": "x86_64-v4-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "none"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "default"},
{"rust_target": "aarch64-unknown-linux-gnu", "feat_set": "logging"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v1-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v2-linux-gnu"},
{"rust_target": "aarch64-unknown-linux-gnu", "sys_target": "x86_64-v3-linux-gnu"},
@@ -120,13 +147,16 @@ jobs:
{"sys_target": "x86_64-v2-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v2-linux-gnu", "feat_set": "none"},
{"sys_target": "x86_64-v2-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v2-linux-gnu", "feat_set": "logging"},
{"sys_target": "x86_64-v3-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "none"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v3-linux-gnu", "feat_set": "logging"},
{"sys_target": "x86_64-v3-linux-gnu", "bake_target": "smoke-valgrind"},
{"sys_target": "x86_64-v4-linux-gnu", "machine": "ARM64"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "none"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "default"},
{"sys_target": "x86_64-v4-linux-gnu", "feat_set": "logging"},
{"sys_target": "x86_64-v4-linux-gnu", "bake_target": "smoke-valgrind"},
]
Generated
+646 -544
View File
File diff suppressed because it is too large Load Diff
+96 -113
View File
@@ -29,7 +29,7 @@ license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/matrix-construct/tuwunel"
rust-version = "1.88.0"
version = "1.3.0"
version = "1.4.2"
[workspace.metadata.crane]
name = "tuwunel"
@@ -39,7 +39,7 @@ name = "tuwunel"
#
[workspace.dependencies.argon2]
version = "0.5.3"
version = "0.5"
default-features = false
features = [
"alloc",
@@ -47,17 +47,17 @@ features = [
]
[workspace.dependencies.arrayvec]
version = "0.7.6"
version = "0.7"
features = ["serde"]
[workspace.dependencies.async-channel]
version = "2.3.1"
[workspace.dependencies.async-trait]
version = "0.1.88"
version = "0.1"
[workspace.dependencies.axum]
version = "0.8.4"
version = "0.8"
default-features = false
features = [
"form",
@@ -73,7 +73,7 @@ features = [
version = "0.7"
[workspace.dependencies.axum-extra]
version = "0.10.1"
version = "0.10"
default-features = false
features = [
"typed-header",
@@ -81,7 +81,7 @@ features = [
]
[workspace.dependencies.axum-server]
version = "0.7.2"
version = "0.7"
default-features = false
[workspace.dependencies.axum-server-dual-protocol]
@@ -89,11 +89,11 @@ version = "0.7"
default-features = false
[workspace.dependencies.base64]
version = "0.22.1"
version = "0.22"
default-features = false
[workspace.dependencies.blurhash]
version = "0.2.3"
version = "0.2"
default-features = false
features = [
"fast-linear-to-srgb",
@@ -101,13 +101,13 @@ features = [
]
[workspace.dependencies.bytes]
version = "1.10.1"
version = "1.10"
[workspace.dependencies.bytesize]
version = "2.0.1"
version = "2.0"
[workspace.dependencies.cargo_toml]
version = "0.22.1"
version = "0.22"
default-features = false
features = ["features"]
@@ -115,12 +115,12 @@ features = ["features"]
version = "0.1"
[workspace.dependencies.chrono]
version = "0.4.41"
version = "0.4"
features = ["alloc", "std"]
default-features = false
[workspace.dependencies.clap]
version = "4.5.39"
version = "4.5"
default-features = false
features = [
"derive",
@@ -140,40 +140,31 @@ features = [
version = "0.8.3"
[workspace.dependencies.const-str]
version = "0.6.2"
version = "0.6"
[workspace.dependencies.ctor]
version = "0.4.2"
version = "0.4"
[workspace.dependencies.cyborgtime]
version = "2.1.1"
version = "2.1"
[workspace.dependencies.either]
version = "1.15.0"
version = "1.15"
default-features = false
features = ["serde"]
[workspace.dependencies.figment]
version = "0.10.19"
version = "0.10"
default-features = false
features = ["env", "toml"]
[workspace.dependencies.futures]
version = "0.3.31"
version = "0.3"
default-features = false
features = ["std", "async-await"]
[workspace.dependencies.hardened_malloc-rs]
version = "0.1.2"
default-features = false
features = [
"static",
"gcc",
"light",
]
[workspace.dependencies.hickory-resolver]
version = "0.25.2"
version = "0.25"
default-features = false
features = [
"serde",
@@ -182,17 +173,17 @@ features = [
]
[workspace.dependencies.hmac]
version = "0.12.1"
version = "0.12"
default-features = false
[workspace.dependencies.http]
version = "1.3.1"
version = "1.3"
[workspace.dependencies.http-body-util]
version = "0.1.3"
version = "0.1"
[workspace.dependencies.hyper]
version = "1.6.0"
version = "1.6"
default-features = false
features = [
"server",
@@ -210,7 +201,7 @@ features = [
]
[workspace.dependencies.image]
version = "0.25.6"
version = "0.25"
default-features = false
features = [
"jpeg",
@@ -219,14 +210,20 @@ features = [
"webp",
]
[workspace.dependencies.insta]
version = "1.43"
features = [
"json",
]
[workspace.dependencies.ipaddress]
version = "0.1.3"
version = "0.1"
[workspace.dependencies.itertools]
version = "0.14.0"
version = "0.14"
[workspace.dependencies.jsonwebtoken]
version = "9.3.1"
version = "9.3"
default-features = false
features = ["use_pem"]
@@ -237,39 +234,39 @@ default-features = false
features = ["sync", "tls-rustls"]
[workspace.dependencies.libc]
version = "0.2.172"
version = "0.2"
[workspace.dependencies.libloading]
version = "0.8.8"
version = "0.8"
[workspace.dependencies.log]
version = "0.4.27"
version = "0.4"
default-features = false
[workspace.dependencies.loole]
version = "0.4.1"
version = "0.4"
[workspace.dependencies.lru-cache]
version = "0.1.2"
version = "0.1"
[workspace.dependencies.maplit]
version = "1.0.2"
version = "1.0"
[workspace.dependencies.minicbor]
version = "1.0.0"
version = "2.0"
features = ["std"]
[workspace.dependencies.minicbor-serde]
version = "0.5.0"
version = "0.6"
features = ["std"]
[workspace.dependencies.nix]
version = "0.30.1"
version = "0.30"
default-features = false
features = ["resource"]
[workspace.dependencies.num-traits]
version = "0.2.19"
version = "0.2"
[workspace.dependencies.opentelemetry]
version = "0.30.0"
@@ -280,23 +277,23 @@ version = "0.30.0"
#features = ["rt-tokio"]
[workspace.dependencies.opentelemetry_sdk]
version = "0.30.0"
version = "0.30"
features = ["rt-tokio"]
[workspace.dependencies.proc-macro2]
version = "1.0.95"
version = "1.0"
[workspace.dependencies.quote]
version = "1.0.40"
version = "1.0"
[workspace.dependencies.rand]
version = "0.8.5"
version = "0.8"
[workspace.dependencies.regex]
version = "1.11.1"
version = "1.11"
[workspace.dependencies.reqwest]
version = "0.12.19"
version = "0.12"
default-features = false
features = [
"rustls-tls-native-roots",
@@ -306,25 +303,24 @@ features = [
]
[workspace.dependencies.ring]
version = "0.17.14"
version = "0.17"
default-features = false
[workspace.dependencies.ruma]
git = "https://github.com/matrix-construct/ruma"
rev = "0155c2b33233bec9dece79d5134a9574b347f4c1"
rev = "5682b88cf1bcaf0f47805d614b476b242ef075d4"
features = [
"compat",
"rand",
"__compat",
"appservice-api-c",
"client-api",
"client-api-s",
"compat-upload-signatures",
"federation-api",
"identifiers-validation",
"markdown",
"push-gateway-api-c",
"unstable-exhaustive-types",
"rand",
"ring-compat",
"compat-upload-signatures",
"identifiers-validation",
"unstable-unspecified",
"unstable-msc2448",
"unstable-msc2666",
"unstable-msc2867",
@@ -332,24 +328,23 @@ features = [
"unstable-msc3026",
"unstable-msc3061",
"unstable-msc3245",
"unstable-msc3266",
"unstable-msc3381", # polls
"unstable-msc3489", # beacon / live location
"unstable-msc3575",
"unstable-msc3930", # polls push rules
"unstable-msc4075",
"unstable-msc4095",
"unstable-msc4121",
"unstable-msc4125",
"unstable-msc4133",
"unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4210", # remove legacy mentions
"unstable-msc4311",
"unstable-extensible-events",
"unstable-pdu",
"unstable-hydra",
]
[workspace.dependencies.rustls]
version = "0.23.27"
version = "0.23"
default-features = false
features = ["aws_lc_rs"]
@@ -370,14 +365,14 @@ features = [
]
[workspace.dependencies.sanitize-filename]
version = "0.6.0"
version = "0.6"
[workspace.dependencies.sd-notify]
version = "0.4.5"
version = "0.4"
default-features = false
[workspace.dependencies.sentry]
version = "0.38.1"
version = "0.42"
default-features = false
features = [
"backtrace",
@@ -393,48 +388,52 @@ features = [
]
[workspace.dependencies.sentry-tower]
version = "0.38.1"
version = "0.42"
[workspace.dependencies.sentry-tracing]
version = "0.38.1"
version = "0.42"
[workspace.dependencies.serde]
version = "1.0.219"
version = "1.0"
default-features = false
features = ["rc"]
[workspace.dependencies.serde_html_form]
version = "0.2.6"
version = "0.2"
[workspace.dependencies.serde_json]
version = "1.0.140"
version = "1.0"
default-features = false
features = ["raw_value"]
[workspace.dependencies.serde_regex]
version = "1.1.0"
version = "1.1"
[workspace.dependencies.serde_yaml]
version = "0.9.34"
version = "0.9"
[workspace.dependencies.sha1]
version = "0.10.6"
version = "0.10"
default-features = false
[workspace.dependencies.sha2]
version = "0.10.9"
version = "0.10"
default-features = false
[workspace.dependencies.similar]
version = "2.7"
[workspace.dependencies.smallstr]
version = "0.3"
features = [
"ffi",
"serde",
"std",
"union",
]
[workspace.dependencies.smallvec]
version = "1.15.1"
version = "1.15"
features = [
"const_generics",
"const_new",
@@ -444,7 +443,7 @@ features = [
]
[workspace.dependencies.syn]
version = "2.0.101"
version = "2.0"
default-features = false
features = [
"extra-traits",
@@ -455,11 +454,11 @@ features = [
]
[workspace.dependencies.termimad]
version = "0.33.0"
version = "0.33"
default-features = false
[workspace.dependencies.thiserror]
version = "2.0.12"
version = "2.0"
default-features = false
[workspace.dependencies.tikv-jemallocator]
@@ -487,7 +486,7 @@ features = [
]
[workspace.dependencies.tokio]
version = "1.46.0"
version = "1.47"
default-features = false
features = [
"fs",
@@ -502,20 +501,20 @@ features = [
]
[workspace.dependencies.tokio-metrics]
version = "0.4.2"
version = "0.4"
[workspace.dependencies.toml]
version = "0.8.23"
version = "0.9"
default-features = false
features = ["parse"]
[workspace.dependencies.tower]
version = "0.5.2"
version = "0.5"
default-features = false
features = ["util"]
[workspace.dependencies.tower-http]
version = "0.6.6"
version = "0.6"
default-features = false
features = [
"add-extension",
@@ -529,21 +528,21 @@ features = [
]
[workspace.dependencies.tracing]
version = "0.1.41"
version = "0.1"
default-features = false
[workspace.dependencies.tracing-core]
version = "0.1.33"
version = "0.1"
default-features = false
[workspace.dependencies.tracing-flame]
version = "0.2.0"
version = "0.2"
[workspace.dependencies.tracing-opentelemetry]
version = "0.31.0"
version = "0.31"
[workspace.dependencies.tracing-subscriber]
version = "0.3.19"
version = "0.3"
default-features = false
features = [
"ansi",
@@ -555,35 +554,18 @@ features = [
]
[workspace.dependencies.url]
version = "2.5.4"
version = "2.5"
default-features = false
features = ["serde"]
[workspace.dependencies.webpage]
version = "2.0.1"
version = "2.0"
default-features = false
#
# Patches
#
# backport of [https://github.com/tokio-rs/tracing/pull/2956] to the 0.1.x
# branch of tracing. we can switch back to upstream if #2956 is merged and
# backported in the upstream repo.
# https://github.com/girlbossceo/tracing/commit/b348dca742af641c47bc390261f60711c2af573c
[patch.crates-io.tracing-subscriber]
git = "https://github.com/matrix-construct/tracing"
rev = "1e64095a8051a1adf0d1faa307f9f030889ec2aa"
[patch.crates-io.tracing]
git = "https://github.com/matrix-construct/tracing"
rev = "1e64095a8051a1adf0d1faa307f9f030889ec2aa"
[patch.crates-io.tracing-core]
git = "https://github.com/matrix-construct/tracing"
rev = "1e64095a8051a1adf0d1faa307f9f030889ec2aa"
[patch.crates-io.tracing-log]
git = "https://github.com/matrix-construct/tracing"
rev = "1e64095a8051a1adf0d1faa307f9f030889ec2aa"
# adds a tab completion callback:
# https://github.com/girlbossceo/rustyline-async/commit/de26100b0db03e419a3d8e1dd26895d170d1fe50
# adds event for CTRL+\:
@@ -915,6 +897,7 @@ pedantic = { level = "warn", priority = -1 }
## some sadness
too_long_first_doc_paragraph = { level = "allow", priority = 1 }
default_trait_access = { level = "allow", priority = 1 }
doc_markdown = { level = "allow", priority = 1 }
enum_glob_use = { level = "allow", priority = 1 }
if_not_else = { level = "allow", priority = 1 }
@@ -972,7 +955,7 @@ semicolon_outside_block = "warn"
str_to_string = "warn"
string_lit_chars_any = "warn"
string_slice = "warn"
string_to_string = "warn"
implicit_clone = "warn"
suspicious_xor_used_as_pow = "warn"
tests_outside_test_module = "warn"
try_err = "warn"
+8 -19
View File
@@ -21,7 +21,7 @@ ## High Performance Matrix Homeserver in Rust!
[bot](https://matrix.org/ecosystem/integrations/). It is written entirely in Rust to be a scalable,
lightweight, low-cost, community-driven alternative covering all but the most niche uses.
This project is the official successor to [conduwuit](https://github.com/girlbossceo/conduwuit), which
This project is the official successor to conduwuit, which
was a featureful and high-performance fork of [Conduit](https://gitlab.com/famedly/conduit), all
community-lead homeservers implementing the compatible
[Matrix Specification](https://spec.matrix.org/latest/).
@@ -57,12 +57,12 @@ ### Getting Started
`/etc/caddy/Caddyfile` configuration with [Element](https://github.com/element-hq/element-web/releases)
unzipped to `/var/www/element`:
```
https://tuwunel.me:8448 {
reverse_proxy http://127.0.0.1:8008
tuwunel.me, tuwunel.me:8448 {
reverse_proxy localhost:8008
}
https://tuwunel.me:443 {
root * /var/www/element/
file_server
web.tuwunel.me {
root * /var/www/element/
file_server
}
```
`caddy reload --config /etc/caddy/Caddyfile`
@@ -75,7 +75,7 @@ ### Getting Started
🤗 Did you find this and other documentation helpful? We would love to hear feedback about setting
up Tuwunel.
### Migrating to Tuwunel
@@ -110,22 +110,11 @@ ### Upgrading & Downgrading Tuwunel
safe but often prevented by a guard. An error will indicate the downgrade is not possible and a
newer version which does not error must be sought.
#### Versioning
Tuwunel uses a semantic version tag in the format of `v<major>.<minor>.<patch>`. The `patch` value will
always correspond to the number of commits from the last `minor` change with significance to developers.
The `minor` version is changed for fixes or features significant to users. The `major` value corresponds
to significant feature evolutions, and does not indicate any "breaking change" nor connote stability.
> [!IMPORTANT]
> **Users should attempt to keep up to date with the latest minor version.**
#### Branches
The main branch is always _reasonably safe_ to run. We understand the propensity for users to simply clone
the main branch to get up and running, and we're obliged to ensure it's always viable. Nevertheless, only
tagged releases are true releases. If you don't care to update often, find the latest `minor` version
change rather than `patch`. We don't recommend simply following `major` version changes at this time.
tagged releases are true releases.
#### Container Tracking
+28 -30
View File
@@ -1,59 +1,57 @@
# Tuwunel 1.3.0
# Tuwunel 1.4.2
August 4, 2025
Preparations for [Project Hydra](https://matrix.org/blog/2025/07/security-predisclosure/) have taken place. Users should be prepared to upgrade to `v1.4.0-rc` next week. Due to the comprehensive low-level changes which have taken place, and the inability to develop them in public with your feedback, the release will undergo an extended `-rc` period. Due to the time difference and scheduling conflicts our release may be published between 12 to 36 hours after the suggested time; though well before the written disclosure of the issues.
Some foundational work for `v1.4.0` was backported into this release after the announcement was made mid-July to further extend Hydra. An unexpected performance improvement drastically reduced CPU usage. As a result, integration tests began to flake, uncovering latent concurrency bugs which were addressed. These fixes primarily impact the legacy and sliding-sync systems, and further fixes improve performance and compliance, though mostly in the legacy system as sliding-sync lacks adequate test coverage.
This release fixes many bugs and improves performance but at the cost of planned features further rescheduled to either `v1.4.1` or `v1.5.0`.
September 12, 2025
Users running maubot, neochat, or any client or bridge not excluded below should update to this patch as soon as possible to reduce unnecessary resource consumption. (see: Bug Fixes)
### New Features
- Refresh tokens (MSC2918) have been implemented. Supporting clients can now timeout their access tokens with a soft-logout after a configured `access_token_ttl`. This feature was commissioned and made public by an enterprise sponsor.
- Requested by @alaviss an alternative DNS resolver has been implemented for use with appservices and other configured targets intended for local networks. This passthru performs minimal caching and cannot be used for federation. Enable with `dns_passthru_appservices = true` or specifying hosts in `dns_passthru_domains` (#158)
- Typing indicators have been added to sliding-sync thanks to @tmayoff. This feature (and the whole of sliding-sync) is still experimental and the indicator may not always appear or disappear as intended, nevertheless the effort will be enhanced by foundational fixes improving sliding-sync requested soon by the project's sponsor.
- Contributed by @tototomate123 a nifty experimental feature can disable push notifications when you're active on one device from being sent to others. This can be enabled with `suppress_push_when_active`. Please thank them when your pocket stops vibrating while chatting on your desktop! (#150)
- Thanks to a report by @DetermineAbsurd the `m.federate` field can be defaulted to false when creating a room using the new `federate_created_rooms` config option. (#151)
- At the request of @grinapo verbose logging builds are now bundled with this release. These builds are found with the feature-set `-logging-` which is otherwise similar to `-all-`. This contains more messages at all levels optimized away in other release modes; it comes at some performance penalty.
- JWT tokens can now be used for authentication on any endpoint which supports UIA. For example: an external forgot-password service can send a token to the `client/account/password` endpoint to reset a user's password. This feature was commissioned and made public by an enterprise sponsor.
### Enhancements
- @dasha_uwu maintains their streak as a serial contributor by patching the `!admin query raw` command with a base64 option allowing for low-level debugging of database records.
- Sliding-sync has been significantly refactored. Performance has massively increased with many bugs and compliance issues also fixed. Please be aware we are tracking an issue related to read-marker behavior in Element X. The 🟢 dot does not unconditionally clear at every touch. Whether this is a feature or a bug, or both, is being investigated for v1.5.
- @obioma has improved the documentation explaining how to use multiple configuration files with precedence.
- Hydra backports are now enabled by default. The change should be completely transparent. If you do notice any increased load try to increase the `cache_capacity_modifier` above default.
- Upon recommendation of @grin a basic request ID has been added to the tracing logs to uniquely distinguish each request while it's interleaved among others.
- Room deletions now also purge synctokens which can be significant to the overall storage consumed by a room. Users who have already deleted rooms please be assured an update planned for v1.5 will deal with cleansing synctokens in general.
- Requested by @fruzitent this and future releases are tagged by version as multi-arch docker images to be properly archived in the registry rather than simply overwriting `:latest`.
- Event processing performance has been improved by fetching and processing `prev_events` and `auth_events` concurrently. This reduces the impact of recursing large graphs while the room's mutex is locked.
- An experimental command `!admin debug resync-database` has been added for developers curious about #35
- Room version 1 and 2 support took a step forward, possibly working for some rooms but is not yet considered adequately supported and the ticket remains open. (#12)
- Thanks to @AreYouLoco for contributing an updated Kubernetes [Helm Chart](https://github.com/AreYouLoco/tuwunel-helm); link added to docs.
### Bug Fixes
- Thanks to @tmayoff room avatars are properly calculated and no longer the same for all spaces (https://github.com/matrix-construct/tuwunel/pull/102).
- **Special thanks to @frebib for investigating a bug which triggers the uploading of unnecessary encryption one-time-keys.** Running over ten maubot instances it became obvious after observing increased resources and laggy bot response. This update removes any excess keys for a device. Thanks to @duckbuster for confirming neochat is affected. Clients confirmed unaffected include: Element, Element X, Nheko. Fractal, Cinny, matrix-rust-sdk and matrix-js-sdk clients and bots are probably unaffected. Mautrix-based bridges are probably affected. Users of unaffected clients should still upgrade.
- Courtesy of @coolGi69 our bump to Rust 1.88 was properly updated for Nix. Apologies to the Nix community for getting this wrong the first time.
- Thanks @dasha_uwu for refactoring alias resolution logic with fixes to remain compatible with the upcoming element-web release. This was an incredibly valuable contribution which will spare all of us from impending grief; the kind of ahead-of-the-game initiative I don't think a project like this could exist without. (adadafa88f3)
- Invite rejections have been fixed, this was due to a misinterpretation of the spec in legacy sync.
- Room deletions now preserve a small number of records to properly synchronize with local clients and remote servers after the room vanishes. Prior behavior is maintained with a `--force` flag added to the command.
- Knock rooms might have been buggy from the ambiguous overuse of the word "count" in the codebase. Database records which expected a summation of the users in a room instead received the sequence number of the server, both are called "count."
- Thanks @scvalex for once again cleaning up our mess after Nix found the github CI was not running doctests. Thank you for contributing the patch 🙏 (#152).
- Room knocks failed to wakeup the sync systems; some cases of account_data changes also failed to wakeup the sync systems. These have been addressed.
- Thanks @Tronde for reporting a broken link to the CoC in the mdbook documentation. (#155)
- The main sequence number fundamental to the entire server's operation (the "count" or counter) has been refactored after having exceeded architectural limitations. It has been replaced by a two-phase counter ensuring read-after-write consistency, and quasi-transactions grouping multiple writes.
- Specification compliance required the `/joined_rooms` endpoint be restricted to current members rather than including past members. (4b49aaad53a)
- Sequence issues have been addressed in both legacy and sliding sync. These systems operate using a "snapshot" approach which intentionally ignores new data received by the server after the sync request has started; the snapshot approach replaced the complex of mutexes used by Conduit. The server was not originally designed for this approach and some information "from the future" continued to leak into the snapshot's window; these leaks have been sealed.
- Specification compliance required state events be made visible to prior members of a room where `history_visibility=shared`. (86781522b68)
- Protocol compliance issues in legacy sync have been addressed. Additional compliance tests for device list updates now pass. The `state` and `timeline` on incremental sync provide expected results in more (if not all) cases.
- The `limit` parameter to the `/context` endpoint is now divided with de facto compatibility (matrix-org/matrix-spec#2202)
- Errors requiring M_BAD_ALIAS instead of M_UNKNOWN when sending `m.room.canonical_alias` are now conforming.
- The room avatar in sliding sync is now computed with greater compliance to the specification (3deebeab78f). This builds off earlier work done by @tmayoff in (a340e6786db).
- The canonical alias for a room is considered invalid if the primary alias is missing or removed (7221d466ce8). This is a T&S concern and we encourage reports for any other contexts where this condition should be applied.
- Presence is no longer updated by the private read-receipt or read-marker paths, only public receipts.
### Deprecations
- Unauthenticated media fallbacks are no longer requested by default. This can still be enabled with `request_legacy_media` if desired.
- Legacy Sliding-Sync has been removed in favor of Simplified Sliding-Sync. Clients which exclusively using Sliding-Sync have already migrated around the start of this year, so this removal should have no impact now.
- Hardened Malloc support had to be removed after the build broke. We will gladly add support back upon request or contribution.
+1 -1
View File
@@ -167,7 +167,7 @@ RUN \
EOF
WORKDIR /usr/src
ADD https://github.com/matrix-construct/complement.git#81f3b61e10ec102a36921890c81b79564ebf6f40 complement
ADD https://github.com/matrix-construct/complement.git#403840348f6bcc9cc8ed1671dc2f638c2b1ce4ac complement
WORKDIR /usr/src/complement
ENV COMPLEMENT_BASE_IMAGE="complement-testee"
+5
View File
@@ -1,5 +1,10 @@
# Docker Builder
> [!NOTE]
> This directory is for building docker images. For [deploying](https://github.com/matrix-construct/tuwunel/tree/main/docs/deploying)
> [docker](https://github.com/matrix-construct/tuwunel/blob/main/docs/deploying/docker.md) visit
> the appropriate documentation instead.
All Docker images for the project are built here. All images are
[Docker Bake](https://docs.docker.com/build/bake/) targets. All targets are leaves and
branches of a unified tree leading to a single root. It is a combinatorial matrix
+30 -7
View File
@@ -29,8 +29,12 @@ variable "git_ref_name" {
cargo_feat_sets = {
none = ""
# Default features
default = "brotli_compression,element_hacks,gzip_compression,io_uring,jemalloc,jemalloc_conf,media_thumbnail,release_max_log_level,systemd,url_preview,zstd_compression"
all = "blurhashing,brotli_compression,bzip2_compression,tuwunel_mods,console,default,direct_tls,element_hacks,gzip_compression,hardened_malloc,io_uring,jemalloc,jemalloc_conf,jemalloc_prof,jemalloc_stats,ldap,lz4_compression,media_thumbnail,perf_measurements,release_max_log_level,sentry_telemetry,systemd,tokio_console,url_preview,zstd_compression"
# All features sans release_max_log_level
logging = "blurhashing,brotli_compression,bzip2_compression,console,direct_tls,element_hacks,gzip_compression,io_uring,jemalloc,jemalloc_conf,jemalloc_prof,jemalloc_stats,ldap,lz4_compression,media_thumbnail,perf_measurements,sentry_telemetry,systemd,tokio_console,tuwunel_mods,url_preview,zstd_compression"
# All features
all = "blurhashing,brotli_compression,bzip2_compression,console,direct_tls,element_hacks,gzip_compression,io_uring,jemalloc,jemalloc_conf,jemalloc_prof,jemalloc_stats,ldap,lz4_compression,media_thumbnail,perf_measurements,release_max_log_level,sentry_telemetry,systemd,tokio_console,tuwunel_mods,url_preview,zstd_compression"
}
variable "cargo_features_always" {
default = "direct_tls"
@@ -233,6 +237,7 @@ group "lints" {
group "tests" {
targets = [
"docs",
"unit",
"smoke",
"complement",
@@ -809,6 +814,25 @@ target "unit" {
}
}
target "docs" {
name = elem("docs", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("docs", [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]),
]
contexts = {
input = elem("target:build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
cargo_cmd = "test"
cargo_args = "--doc --no-fail-fast"
}
}
#
# Workspace builds
#
@@ -835,10 +859,10 @@ target "book" {
EOF
}
target "docs" {
name = elem("docs", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
target "build-docs" {
name = elem("build-docs", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
tags = [
elem_tag("docs", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
elem_tag("build-docs", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
]
matrix = cargo_rust_feat_sys
inherits = [
@@ -1035,7 +1059,7 @@ target "fmt" {
input = elem("target:ingredients", [rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
}
args = {
fmt_args = "-- --color always"
fmt_args = "-- --color=always"
}
}
@@ -1504,7 +1528,7 @@ cargo_installs = [
"cargo-deb",
#"cargo-arch",
"cargo-generate-rpm",
"lychee",
#"lychee",
"mdbook",
"typos-cli",
]
@@ -1613,7 +1637,6 @@ target "kitchen" {
args = {
packages = join(" ", [
contains(split(",", cargo_feat_sets[feat_set]), "bzip2_compression")? "libbz2-dev": "",
contains(split(",", cargo_feat_sets[feat_set]), "hardened_malloc")? "g++": "",
contains(split(",", cargo_feat_sets[feat_set]), "io_uring")? "liburing-dev": "",
contains(split(",", cargo_feat_sets[feat_set]), "jemalloc")? "libjemalloc-dev": "",
contains(split(",", cargo_feat_sets[feat_set]), "lz4_compression")? "liblz4-dev": "",
+1 -1
View File
@@ -61,7 +61,7 @@ toolchain_toml="$docker_dir/../rust-toolchain.toml"
rust_msrv=$(grep "channel = " "$toolchain_toml" | cut -d'=' -f2 | sed 's/\s"\|"$//g')
rocksdb_opt_level=3
rocksdb_portable=1
git_checkout="HEAD"
git_checkout="${git_checkout:-HEAD}"
use_chef="true"
set +a
+3
View File
@@ -31,6 +31,9 @@ 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"
set -a
cargo_profile="${cargo_profile:-$default_cargo_profile}"
+1
View File
@@ -0,0 +1 @@
../CODE_OF_CONDUCT.md
+1 -1
View File
@@ -20,4 +20,4 @@ # Summary
- [Contributing](contributing.md)
- [Testing](development/testing.md)
- [Hot Reloading ("Live" Development)](development/hot_reload.md)
- [Tuwunel Community Code of Conduct](../CODE_OF_CONDUCT.md)
- [Tuwunel Community Code of Conduct](CODE_OF_CONDUCT.md)
+1 -1
View File
@@ -153,7 +153,7 @@ ### Caddy
```caddyfile
your.server.name, your.server.name:8448 {
# TCP reverse_proxy
reverse_proxy 127.0.0.1:6167
reverse_proxy localhost:8008
# UNIX socket
#reverse_proxy unix//run/tuwunel/tuwunel.sock
}
+2 -1
View File
@@ -2,7 +2,8 @@ # Tuwunel for Kubernetes
Tuwunel doesn't support horizontal scalability or distributed loading
natively, however a community maintained Helm Chart is available here to run
Tuwunel on Kubernetes: <https://gitlab.cronce.io/charts/conduwuit>
Tuwunel on Kubernetes: <https://github.com/AreYouLoco/tuwunel-helm> and the
legacy conduwuit version: <https://gitlab.cronce.io/charts/conduwuit>.
Should changes need to be made, please reach out to the maintainer in our
Matrix room as this is not maintained/controlled by the Tuwunel maintainers.
+100 -46
View File
@@ -1,17 +1,11 @@
# Troubleshooting Tuwunel
> ## Docker users ⚠️
>
> Docker is extremely UX unfriendly. Because of this, a ton of issues or support
> is actually Docker support, not tuwunel support. We also cannot document the
> ever-growing list of Docker issues here.
>
> [!IMPORTANT]
> If you intend on asking for support and you are using Docker, **PLEASE**
> triple validate your issues are **NOT** because you have a misconfiguration in
> your Docker setup.
>
> If there are things like Compose file issues or Dockerhub image issues, those
> can still be mentioned as long as they're something we can fix.
> your Docker setup. We must remain focused on supporting Tuwunel issues and
> cannot budget our time for generic Docker support. Compose file issues or
> Dockerhub image issues are okay if they are something we can fix.
## Tuwunel and Matrix issues
@@ -93,48 +87,107 @@ ## RocksDB / database issues
#### Database corruption
If your database is corrupted *and* is failing to start (e.g. checksum
mismatch), it may be recoverable but careful steps must be taken, and there is
no guarantee it may be recoverable.
There are many causes and varieties of database corruption. There are several
methods for mitigation, each with outcomes ranging from a recovered state down
to a savage state. This guide has been simplified into a set of universal steps
which everyone can follow from the top until they have recovered or reach the
end. The details and implications will be explained within each step.
The first thing that can be done is launching Tuwunel with the
`rocksdb_repair` config option set to true. This will tell RocksDB to attempt to
repair itself at launch. If this does not work, disable the option and continue
reading.
> [!TIP]
> All command-line `-O` options can be expressed as environment variables or in
> the config file based on your deployment's requirements. Note that
> `--maintenance` is equivalent to configuring `startup_netburst = false` and
> `listening = false`.
RocksDB has the following recovery modes:
> [!IMPORTANT]
> Always create a backup of the database before running any operation. This is
> critical for steps 3 and above.
- `TolerateCorruptedTailRecords`
- `AbsoluteConsistency`
- `PointInTime`
- `SkipAnyCorruptedRecord`
**0. Start the server with the following options:**
By default, Tuwunel uses `TolerateCorruptedTailRecords` as generally these may
be due to bad federation and we can re-fetch the correct data over federation.
The RocksDB default is `PointInTime` which will attempt to restore a "snapshot"
of the data when it was last known to be good. This data can be either a few
seconds old, or multiple minutes prior. `PointInTime` may not be suitable for
default usage due to clients and servers possibly not being able to handle
sudden "backwards time travels", and `AbsoluteConsistency` may be too strict.
`tuwunel --maintenance -O rocksdb_recovery_mode=0`
`AbsoluteConsistency` will fail to start the database if any sign of corruption
is detected. `SkipAnyCorruptedRecord` will skip all forms of corruption unless
it forbids the database from opening (e.g. too severe). Usage of
`SkipAnyCorruptedRecord` voids any support as this may cause more damage and/or
leave your database in a permanently inconsistent state, but it may do something
if `PointInTime` does not work as a last ditch effort.
This is actually a "control" and not a method of recovery. If the server starts
you either do not have corruption or have deep corruption indicated by very
specific errors from rocksdb citing corruption during runtime. If you are
certain there is deep corruption skip to step 4, otherwise you are finished
without any modifications.
With this in mind:
**1. Start the server in Tolerate-Corrupted-Tail-Records mode:**
- First start Tuwunel with the `PointInTime` recovery method. See the [example
config](configuration/examples.md) for how to do this using
`rocksdb_recovery_mode`
- If your database successfully opens, clients are recommended to clear their
client cache to account for the rollback
- Leave your Tuwunel running in `PointInTime` for at least 30-60 minutes so as
much possible corruption is restored
- If all goes will, you should be able to restore back to using
`TolerateCorruptedTailRecords` and you have successfully recovered your database
`tuwunel --maintenance -O rocksdb_recovery_mode=1`
The most common corruption scenario is from a loss of power to the hardware
(not an application crash, though it is still possible). This is remediated
by dropping the most recently written record. It is highly unlikely there will
be any impact on the application from this loss. In the best-case the same data
is often re-requested over the federation or replaced by a client. In the
worst-case clients may need to clear-cache & reload to guarantee correctness.
If the server starts you are finished.
**2. Start the server in Point-In-Time mode:**
`tuwunel --maintenance -O rocksdb_recovery_mode=2`
Similar to the corruption scenario above but for more severe cases. The most
recent records are discarded back to the point where there is no corruption.
It is highly unlikely there will be any impact on the application from this
loss, but it is more likely than above that clients may need to clear-cache
& reload to correctly resynchronize with the server.
**3. Start the server in Skip-Any-Corrupted-Record mode:**
> [!WARNING]
> Salvage mode potentially impacting the application's ability to function.
> We cannot provide support for users who have entered this mode.
`tuwunel --maintenance -O rocksdb_recovery_mode=3`
Similar to the prior corruption scenarios but for the most severe cases.
The database will be inconsistent. It is theoretically possible for the
server to continue functioning without notable issue in the best case, but
it is completely uncertain what the effect of this operation will be. If
the server starts you should immediately export your messages, encryption
keys, etc, in a salvage effort and prepare to reinstall.
**4. Start the server in repair mode.**
> [!WARNING]
> Salvage mode potentially impacting the application's ability to function.
> We cannot provide support for users who have entered this mode.
> [!CAUTION]
> Always create a backup of the database before entering this mode. The repair
> is not configurable and not interactive. It may automatically remove more
> data than anticipated, preventing further salvage efforts.
`tuwunel --maintenance -O rocksdb_repair=true`
For corruption affecting the bulk database tables not covered by any journal.
This will leave the database in an inconsistent and unpredictable state. It
is theoretically possible to continue operating the server depending on which
records were dropped, such as some historical records which are no longer
essential. Nevertheless the impact of this operation is impossible to assess
and a successful recovery should be used to salvage data prior to reinstall.
Once finished, restart the server without `rocksdb_repair`. If no errors
persist, restart the server again without maintenance mode.
**5. Utilize an external repair tool.**
> [!WARNING]
> Salvage mode potentially impacting the application's ability to function.
> We cannot provide support for users who have entered this mode.
```
git clone https://github.com/facebook/rocksdb
cd rocksdb
make -j$(nproc) ldb
./ldb repair --db=/var/lib/tuwunel/ 2>./repair-log.txt
```
For situations when the repair mode in step 4 failed or produced unexpected
results.
## Debugging
@@ -147,7 +200,8 @@ #### Debug/Trace log level
Tuwunel builds without debug or trace log levels at compile time by default
for substantial performance gains in CPU usage and improved compile times. If
you need to access debug/trace log levels, you will need to build without the
`release_max_log_level` feature or use our provided static debug binaries.
`release_max_log_level` feature or use our provided release-logging binaries
and images.
#### Changing log level dynamically
-54
View File
@@ -204,8 +204,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -218,8 +216,6 @@
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
@@ -231,8 +227,6 @@
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
@@ -246,8 +240,6 @@
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
@@ -260,12 +252,9 @@
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
hmalloc = scopeHost.main.override { features = ["hardened_malloc"]; };
oci-image = scopeHost.oci-image;
oci-image-all-features = scopeHost.oci-image.override {
@@ -278,8 +267,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -294,18 +281,11 @@
disable_features = [
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
};
oci-image-hmalloc = scopeHost.oci-image.override {
main = scopeHost.main.override {
features = ["hardened_malloc"];
};
};
book = scopeHost.book;
@@ -359,8 +339,6 @@
disable_features = [
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -379,8 +357,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -400,8 +376,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -420,22 +394,12 @@
disable_features = [
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
};
}
# An output for a statically-linked binary with hardened_malloc
{
name = "${binaryName}-hmalloc";
value = scopeCrossStatic.main.override {
features = ["hardened_malloc"];
};
}
# An output for an OCI image based on that binary
{
name = "oci-image-${crossSystem}";
@@ -478,8 +442,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -501,8 +463,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -523,8 +483,6 @@
disable_features = [
# dont include experimental features
"experimental"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
@@ -532,16 +490,6 @@
};
}
# An output for an OCI image based on that binary with hardened_malloc
{
name = "oci-image-${crossSystem}-hmalloc";
value = scopeCrossStatic.oci-image.override {
main = scopeCrossStatic.main.override {
features = ["hardened_malloc"];
};
};
}
# An output for a complement OCI image for the specified platform
{
name = "complement-${crossSystem}";
@@ -571,8 +519,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
];
-2
View File
@@ -23,8 +23,6 @@
# be expected on non-debug builds.
"jemalloc_prof"
"jemalloc_stats"
# this is non-functional on nix for some reason
"hardened_malloc"
# tuwunel_mods is a development-only hot reload feature
"tuwunel_mods"
]
-12
View File
@@ -18,56 +18,46 @@ crate-type = [
[features]
brotli_compression = [
"tuwunel-api/brotli_compression",
"tuwunel-core/brotli_compression",
"tuwunel-service/brotli_compression",
]
bzip2_compression = [
"tuwunel-api/bzip2_compression",
"tuwunel-database/bzip2_compression",
"tuwunel-service/bzip2_compression",
]
gzip_compression = [
"tuwunel-api/gzip_compression",
"tuwunel-core/gzip_compression",
"tuwunel-service/gzip_compression",
]
io_uring = [
"tuwunel-api/io_uring",
"tuwunel-database/io_uring",
"tuwunel-service/io_uring",
]
jemalloc = [
"tuwunel-api/jemalloc",
"tuwunel-core/jemalloc",
"tuwunel-database/jemalloc",
"tuwunel-service/jemalloc",
]
jemalloc_conf = [
"tuwunel-api/jemalloc_conf",
"tuwunel-core/jemalloc_conf",
"tuwunel-database/jemalloc_conf",
"tuwunel-service/jemalloc_conf",
]
jemalloc_prof = [
"tuwunel-api/jemalloc_prof",
"tuwunel-core/jemalloc_prof",
"tuwunel-database/jemalloc_prof",
"tuwunel-service/jemalloc_prof",
]
jemalloc_stats = [
"tuwunel-api/jemalloc_stats",
"tuwunel-core/jemalloc_stats",
"tuwunel-database/jemalloc_stats",
"tuwunel-service/jemalloc_stats",
]
lz4_compression = [
"tuwunel-api/lz4_compression",
"tuwunel-database/lz4_compression",
"tuwunel-service/lz4_compression",
]
release_max_log_level = [
"tuwunel-api/release_max_log_level",
"tuwunel-core/release_max_log_level",
"tuwunel-database/release_max_log_level",
"tuwunel-service/release_max_log_level",
@@ -77,7 +67,6 @@ release_max_log_level = [
"log/release_max_level_info",
]
zstd_compression = [
"tuwunel-api/zstd_compression",
"tuwunel-core/zstd_compression",
"tuwunel-database/zstd_compression",
"tuwunel-service/zstd_compression",
@@ -86,7 +75,6 @@ zstd_compression = [
[dependencies]
base64.workspace = true
clap.workspace = true
tuwunel-api.workspace = true
tuwunel-core.workspace = true
tuwunel-database.workspace = true
tuwunel-macros.workspace = true
+32 -53
View File
@@ -45,7 +45,6 @@ pub(super) async fn echo(&self, message: Vec<String>) -> Result {
pub(super) async fn get_auth_chain(&self, event_id: OwnedEventId) -> Result {
let Ok(event) = self
.services
.rooms
.timeline
.get_pdu_json(&event_id)
.await
@@ -64,7 +63,6 @@ pub(super) async fn get_auth_chain(&self, event_id: OwnedEventId) -> Result {
let start = Instant::now();
let count = self
.services
.rooms
.auth_chain
.event_ids_iter(room_id, once(event_id.as_ref()))
.ready_filter_map(Result::ok)
@@ -87,9 +85,12 @@ pub(super) async fn parse_pdu(&self) -> Result {
}
let string = self.body[1..self.body.len().saturating_sub(1)].join("\n");
let rules = RoomVersionId::V6
.rules()
.expect("rules for V6 rooms");
match serde_json::from_str(&string) {
| Err(e) => return Err!("Invalid json in command body: {e}"),
| Ok(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
| Ok(value) => match ruma::signatures::reference_hash(&value, &rules) {
| Err(e) => return Err!("Could not parse PDU JSON: {e:?}"),
| Ok(hash) => {
let event_id = OwnedEventId::parse(format!("${hash}"));
@@ -108,7 +109,6 @@ pub(super) async fn get_pdu(&self, event_id: OwnedEventId) -> Result {
let mut outlier = false;
let mut pdu_json = self
.services
.rooms
.timeline
.get_non_outlier_pdu_json(&event_id)
.await;
@@ -117,7 +117,6 @@ pub(super) async fn get_pdu(&self, event_id: OwnedEventId) -> Result {
outlier = true;
pdu_json = self
.services
.rooms
.timeline
.get_pdu_json(&event_id)
.await;
@@ -152,7 +151,6 @@ pub(super) async fn get_short_pdu(
let pdu_json = self
.services
.rooms
.timeline
.get_pdu_json_from_id(&pdu_id)
.await;
@@ -252,7 +250,6 @@ pub(super) async fn get_remote_pdu(
.sending
.send_federation_request(&server, ruma::api::federation::event::get_event::v1::Request {
event_id: event_id.clone(),
include_unredacted_content: None,
})
.await
{
@@ -273,32 +270,28 @@ pub(super) async fn get_remote_pdu(
})?;
trace!("Attempting to parse PDU: {:?}", &response.pdu);
let _parsed_pdu = {
let (room_id, ..) = {
let parsed_result = self
.services
.rooms
.event_handler
.parse_incoming_pdu(&response.pdu)
.boxed()
.await;
let (event_id, value, room_id) = match parsed_result {
match parsed_result {
| Ok(t) => t,
| Err(e) => {
warn!("Failed to parse PDU: {e}");
info!("Full PDU: {:?}", &response.pdu);
return Err!("Failed to parse PDU remote server {server} sent us: {e}");
},
};
vec![(event_id, value, room_id)]
}
};
info!("Attempting to handle event ID {event_id} as backfilled PDU");
self.services
.rooms
.timeline
.backfill_pdu(&server, response.pdu)
.backfill_pdu(&room_id, &server, response.pdu)
.await?;
let text = serde_json::to_string_pretty(&json)?;
@@ -311,10 +304,9 @@ pub(super) async fn get_remote_pdu(
#[admin_command]
pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result {
let room_id = self.services.rooms.alias.resolve(&room).await?;
let room_id = self.services.alias.resolve(&room).await?;
let room_state: Vec<Raw<AnyStateEvent>> = self
.services
.rooms
.state_accessor
.room_state_full_pdus(&room_id)
.map_ok(Event::into_format)
@@ -492,7 +484,6 @@ pub(super) async fn verify_pdu(&self, event_id: OwnedEventId) -> Result {
let mut event = self
.services
.rooms
.timeline
.get_pdu_json(&event_id)
.await?;
@@ -517,7 +508,6 @@ pub(super) async fn verify_pdu(&self, event_id: OwnedEventId) -> Result {
pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
if !self
.services
.rooms
.state_cache
.server_in_room(&self.services.server.name, &room_id)
.await
@@ -527,7 +517,6 @@ pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
let first_pdu = self
.services
.rooms
.timeline
.first_pdu_in_room(&room_id)
.await
@@ -542,7 +531,6 @@ pub(super) async fn first_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
if !self
.services
.rooms
.state_cache
.server_in_room(&self.services.server.name, &room_id)
.await
@@ -552,7 +540,6 @@ pub(super) async fn latest_pdu_in_room(&self, room_id: OwnedRoomId) -> Result {
let latest_pdu = self
.services
.rooms
.timeline
.latest_pdu_in_room(&room_id)
.await
@@ -571,7 +558,6 @@ pub(super) async fn force_set_room_state_from_server(
) -> Result {
if !self
.services
.rooms
.state_cache
.server_in_room(&self.services.server.name, &room_id)
.await
@@ -581,7 +567,6 @@ pub(super) async fn force_set_room_state_from_server(
let first_pdu = self
.services
.rooms
.timeline
.latest_pdu_in_room(&room_id)
.await
@@ -589,7 +574,6 @@ pub(super) async fn force_set_room_state_from_server(
let room_version = self
.services
.rooms
.state
.get_room_version(&room_id)
.await?;
@@ -608,7 +592,6 @@ pub(super) async fn force_set_room_state_from_server(
for pdu in remote_state_response.pdus.clone() {
match self
.services
.rooms
.event_handler
.parse_incoming_pdu(&pdu)
.await
@@ -627,24 +610,33 @@ pub(super) async fn force_set_room_state_from_server(
.server_keys
.validate_and_add_event_id(pdu, &room_version)
}) {
let Ok((event_id, value)) = result.await else {
let Ok((event_id, mut value)) = result.await else {
continue;
};
let pdu = PduEvent::from_id_val(&event_id, value.clone()).map_err(|e| {
let invalid_pdu_err = |e| {
debug_error!("Invalid PDU in fetching remote room state PDUs response: {value:#?}");
err!(BadServerResponse(debug_error!("Invalid PDU in send_join response: {e:?}")))
})?;
};
let pdu = if value["type"] == "m.room.create" {
PduEvent::from_rid_val(&room_id, &event_id, value.clone()).map_err(invalid_pdu_err)?
} else {
PduEvent::from_id_val(&event_id, value.clone()).map_err(invalid_pdu_err)?
};
if !value.contains_key("room_id") {
let room_id = CanonicalJsonValue::String(room_id.as_str().into());
value.insert("room_id".into(), room_id);
}
self.services
.rooms
.timeline
.add_pdu_outlier(&event_id, &value);
if let Some(state_key) = &pdu.state_key {
let shortstatekey = self
.services
.rooms
.short
.get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key)
.await;
@@ -667,14 +659,12 @@ pub(super) async fn force_set_room_state_from_server(
};
self.services
.rooms
.timeline
.add_pdu_outlier(&event_id, &value);
}
let new_room_state = self
.services
.rooms
.event_handler
.resolve_state(&room_id, &room_version, state)
.await?;
@@ -686,21 +676,13 @@ pub(super) async fn force_set_room_state_from_server(
removed,
} = self
.services
.rooms
.state_compressor
.save_state(room_id.clone().as_ref(), new_room_state)
.await?;
let state_lock = self
.services
.rooms
.state
.mutex
.lock(&*room_id)
.await;
let state_lock = self.services.state.mutex.lock(&*room_id).await;
self.services
.rooms
.state
.force_state(room_id.clone().as_ref(), short_state_hash, added, removed, &state_lock)
.await?;
@@ -710,7 +692,6 @@ pub(super) async fn force_set_room_state_from_server(
the room's m.room.member state"
);
self.services
.rooms
.state_cache
.update_joined_count(&room_id)
.await;
@@ -993,10 +974,10 @@ pub(super) async fn create_jwt(
#[derive(Serialize)]
struct Claim {
sub: String,
iss: String,
aud: String,
exp: usize,
nbf: usize,
iss: Option<String>,
aud: Option<String>,
exp: Option<usize>,
nbf: Option<usize>,
}
let config = &self.services.config.jwt;
@@ -1013,21 +994,19 @@ struct Claim {
let claim = Claim {
sub: user,
iss: issuer.unwrap_or_default(),
iss: issuer,
aud: audience.unwrap_or_default(),
aud: audience,
exp: exp_from_now
.and_then(|val| now_secs().checked_add(val))
.map(TryInto::try_into)
.and_then(Result::ok)
.unwrap_or(usize::MAX),
.and_then(Result::ok),
nbf: nbf_from_now
.and_then(|val| now_secs().checked_add(val))
.map(TryInto::try_into)
.and_then(Result::ok)
.unwrap_or(0),
.and_then(Result::ok),
};
encode(&header, &claim, &key)
+2 -11
View File
@@ -72,15 +72,6 @@ pub(super) enum DebugCommand {
},
/// - Gets all the room state events for the specified room.
///
/// This is functionally equivalent to `GET
/// /_matrix/client/v3/rooms/{roomid}/state`, except the admin command does
/// *not* check if the sender user is allowed to see state events. This is
/// done because it's implied that server admins here have database access
/// and can see/get room info themselves anyways if they were malicious
/// admins.
///
/// Of course the check is still done on the actual client API.
GetRoomState {
/// Room ID
room_id: OwnedRoomOrAliasId,
@@ -125,13 +116,13 @@ pub(super) enum DebugCommand {
reset: bool,
},
/// - Verify json signatures
/// - Sign JSON blob
///
/// This command needs a JSON blob provided in a Markdown code block below
/// the command.
SignJson,
/// - Verify json signatures
/// - Verify JSON signatures
///
/// This command needs a JSON blob provided in a Markdown code block below
/// the command.
+2 -9
View File
@@ -6,19 +6,13 @@
#[admin_command]
pub(super) async fn disable_room(&self, room_id: OwnedRoomId) -> Result {
self.services
.rooms
.metadata
.disable_room(&room_id, true);
self.services.metadata.disable_room(&room_id);
self.write_str("Room disabled.").await
}
#[admin_command]
pub(super) async fn enable_room(&self, room_id: OwnedRoomId) -> Result {
self.services
.rooms
.metadata
.disable_room(&room_id, false);
self.services.metadata.enable_room(&room_id);
self.write_str("Room enabled.").await
}
@@ -82,7 +76,6 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: OwnedUserId) -> Result
let mut rooms: Vec<(OwnedRoomId, u64, String)> = self
.services
.rooms
.state_cache
.rooms_joined(&user_id)
.then(|room_id| get_room_info(self.services, room_id))
-1
View File
@@ -37,7 +37,6 @@ pub(super) async fn delete(
// parsing the PDU for any MXC URLs begins here
match self
.services
.rooms
.timeline
.get_pdu_json(&event_id)
.await
-1
View File
@@ -1,4 +1,3 @@
#![recursion_limit = "192"]
#![allow(clippy::wildcard_imports)]
#![allow(clippy::enum_glob_use)]
#![allow(clippy::too_many_arguments)]
+1 -7
View File
@@ -30,11 +30,7 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Context<'_>)
match subcommand {
| RoomAliasCommand::ResolveLocalAlias { alias } => {
let timer = tokio::time::Instant::now();
let results = services
.rooms
.alias
.resolve_local_alias(&alias)
.await;
let results = services.alias.resolve_local_alias(&alias).await;
let query_time = timer.elapsed();
write!(context, "Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")
@@ -42,7 +38,6 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Context<'_>)
| RoomAliasCommand::LocalAliasesForRoom { room_id } => {
let timer = tokio::time::Instant::now();
let aliases: Vec<_> = services
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
@@ -55,7 +50,6 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Context<'_>)
| RoomAliasCommand::AllLocalAliases => {
let timer = tokio::time::Instant::now();
let aliases = services
.rooms
.alias
.all_local_aliases()
.map(|(room_id, alias)| (room_id.to_owned(), alias.to_owned()))
-16
View File
@@ -83,7 +83,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::ServerInRoom { server, room_id } => {
let timer = tokio::time::Instant::now();
let result = services
.rooms
.state_cache
.server_in_room(&server, &room_id)
.await;
@@ -98,7 +97,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::RoomServers { room_id } => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.rooms
.state_cache
.room_servers(&room_id)
.map(ToOwned::to_owned)
@@ -115,7 +113,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::ServerRooms { server } => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.rooms
.state_cache
.server_rooms(&server)
.map(ToOwned::to_owned)
@@ -132,7 +129,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::RoomMembers { room_id } => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
@@ -149,7 +145,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::LocalUsersInRoom { room_id } => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.rooms
.state_cache
.local_users_in_room(&room_id)
.map(ToOwned::to_owned)
@@ -166,7 +161,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::ActiveLocalUsersInRoom { room_id } => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.rooms
.state_cache
.active_local_users_in_room(&room_id)
.map(ToOwned::to_owned)
@@ -183,7 +177,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::RoomJoinedCount { room_id } => {
let timer = tokio::time::Instant::now();
let results = services
.rooms
.state_cache
.room_joined_count(&room_id)
.await;
@@ -198,7 +191,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::RoomInvitedCount { room_id } => {
let timer = tokio::time::Instant::now();
let results = services
.rooms
.state_cache
.room_invited_count(&room_id)
.await;
@@ -213,7 +205,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::RoomUserOnceJoined { room_id } => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.rooms
.state_cache
.room_useroncejoined(&room_id)
.map(ToOwned::to_owned)
@@ -230,7 +221,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::RoomMembersInvited { room_id } => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.rooms
.state_cache
.room_members_invited(&room_id)
.map(ToOwned::to_owned)
@@ -247,7 +237,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::GetInviteCount { room_id, user_id } => {
let timer = tokio::time::Instant::now();
let results = services
.rooms
.state_cache
.get_invite_count(&room_id, &user_id)
.await;
@@ -262,7 +251,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::GetLeftCount { room_id, user_id } => {
let timer = tokio::time::Instant::now();
let results = services
.rooms
.state_cache
.get_left_count(&room_id, &user_id)
.await;
@@ -277,7 +265,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::RoomsJoined { user_id } => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.rooms
.state_cache
.rooms_joined(&user_id)
.map(ToOwned::to_owned)
@@ -294,7 +281,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::RoomsInvited { user_id } => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.rooms
.state_cache
.rooms_invited(&user_id)
.collect()
@@ -310,7 +296,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::RoomsLeft { user_id } => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services
.rooms
.state_cache
.rooms_left(&user_id)
.collect()
@@ -326,7 +311,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
| RoomStateCacheCommand::InviteState { user_id, room_id } => {
let timer = tokio::time::Instant::now();
let results = services
.rooms
.state_cache
.invite_state(&user_id, &room_id)
.await;
+3 -15
View File
@@ -25,18 +25,12 @@ pub(crate) enum RoomTimelineCommand {
#[admin_command]
pub(super) async fn last(&self, room_id: OwnedRoomOrAliasId) -> Result {
let room_id = self
.services
.rooms
.alias
.resolve(&room_id)
.await?;
let room_id = self.services.alias.resolve(&room_id).await?;
let result = self
.services
.rooms
.timeline
.last_timeline_count(None, &room_id)
.last_timeline_count(None, &room_id, None)
.await?;
self.write_str(&format!("{result:#?}")).await
@@ -49,18 +43,12 @@ pub(super) async fn pdus(
from: Option<String>,
limit: Option<usize>,
) -> Result {
let room_id = self
.services
.rooms
.alias
.resolve(&room_id)
.await?;
let room_id = self.services.alias.resolve(&room_id).await?;
let from: Option<PduCount> = from.as_deref().map(str::parse).transpose()?;
let result: Vec<_> = self
.services
.rooms
.timeline
.pdus_rev(None, &room_id, from)
.try_take(limit.unwrap_or(3))
+1 -8
View File
@@ -21,7 +21,6 @@ pub(crate) enum ShortCommand {
pub(super) async fn short_event_id(&self, event_id: OwnedEventId) -> Result {
let shortid = self
.services
.rooms
.short
.get_shorteventid(&event_id)
.await?;
@@ -31,16 +30,10 @@ pub(super) async fn short_event_id(&self, event_id: OwnedEventId) -> Result {
#[admin_command]
pub(super) async fn short_room_id(&self, room_id: OwnedRoomOrAliasId) -> Result {
let room_id = self
.services
.rooms
.alias
.resolve(&room_id)
.await?;
let room_id = self.services.alias.resolve(&room_id).await?;
let shortid = self
.services
.rooms
.short
.get_shortroomid(&room_id)
.await?;
-1
View File
@@ -134,7 +134,6 @@ async fn get_shared_rooms(&self, user_a: OwnedUserId, user_b: OwnedUserId) -> Re
let timer = tokio::time::Instant::now();
let result: Vec<_> = self
.services
.rooms
.state_cache
.get_shared_rooms(&user_a, &user_b)
.map(ToOwned::to_owned)
+8 -16
View File
@@ -63,17 +63,15 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) ->
match (
force,
services
.rooms
.alias
.resolve_local_alias(&room_alias)
.await,
) {
| (true, Ok(id)) => {
match services.rooms.alias.set_alias(
&room_alias,
&room_id,
server_user,
) {
match services
.alias
.set_alias(&room_alias, &room_id, server_user)
{
| Err(err) => Err!("Failed to remove alias: {err}"),
| Ok(()) =>
context
@@ -88,11 +86,10 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) ->
overwrite"
),
| (_, Err(_)) => {
match services.rooms.alias.set_alias(
&room_alias,
&room_id,
server_user,
) {
match services
.alias
.set_alias(&room_alias, &room_id, server_user)
{
| Err(err) => Err!("Failed to remove alias: {err}"),
| Ok(()) => context.write_str("Successfully set alias").await,
}
@@ -101,14 +98,12 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) ->
},
| RoomAliasCommand::Remove { .. } => {
match services
.rooms
.alias
.resolve_local_alias(&room_alias)
.await
{
| Err(_) => Err!("Alias isn't in use."),
| Ok(id) => match services
.rooms
.alias
.remove_alias(&room_alias, server_user)
.await
@@ -123,7 +118,6 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) ->
},
| RoomAliasCommand::Which { .. } => {
match services
.rooms
.alias
.resolve_local_alias(&room_alias)
.await
@@ -141,7 +135,6 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) ->
| RoomAliasCommand::List { room_id } =>
if let Some(room_id) = room_id {
let aliases: Vec<OwnedRoomAliasId> = services
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(Into::into)
@@ -160,7 +153,6 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) ->
context.write_str(&plain).await
} else {
let aliases = services
.rooms
.alias
.all_local_aliases()
.map(|(room_id, localpart)| (room_id.into(), localpart.into()))
+22 -21
View File
@@ -16,27 +16,14 @@ pub(super) async fn list_rooms(
let page = page.unwrap_or(1);
let mut rooms = self
.services
.rooms
.metadata
.iter_ids()
.filter_map(async |room_id| {
(!exclude_disabled
|| !self
.services
.rooms
.metadata
.is_disabled(room_id)
.await)
(!exclude_disabled || !self.services.metadata.is_disabled(room_id).await)
.then_some(room_id)
})
.filter_map(async |room_id| {
(!exclude_banned
|| !self
.services
.rooms
.metadata
.is_banned(room_id)
.await)
(!exclude_banned || !self.services.metadata.is_banned(room_id).await)
.then_some(room_id)
})
.then(|room_id| get_room_info(self.services, room_id))
@@ -74,12 +61,26 @@ pub(super) async fn list_rooms(
#[admin_command]
pub(super) async fn exists(&self, room_id: OwnedRoomId) -> Result {
let result = self
.services
.rooms
.metadata
.exists(&room_id)
.await;
let result = self.services.metadata.exists(&room_id).await;
self.write_str(&format!("{result}")).await
}
#[admin_command]
pub(super) async fn delete_room(&self, room_id: OwnedRoomId, force: bool) -> Result {
if self.services.admin.is_admin_room(&room_id).await {
return Err!("Cannot delete admin room");
}
let state_lock = self.services.state.mutex.lock(&room_id).await;
self.services
.delete
.delete_room(&room_id, force, state_lock)
.await?;
self.write_str("Successfully deleted the room from our database.")
.await?;
Ok(())
}
+2 -3
View File
@@ -29,18 +29,17 @@ pub(super) async fn process(command: RoomDirectoryCommand, context: &Context<'_>
let services = context.services;
match command {
| RoomDirectoryCommand::Publish { room_id } => {
services.rooms.directory.set_public(&room_id);
services.directory.set_public(&room_id);
context.write_str("Room published").await
},
| RoomDirectoryCommand::Unpublish { room_id } => {
services.rooms.directory.set_not_public(&room_id);
services.directory.set_not_public(&room_id);
context.write_str("Room unpublished").await
},
| RoomDirectoryCommand::List { page } => {
// TODO: i know there's a way to do this with clap, but i can't seem to find it
let page = page.unwrap_or(1);
let mut rooms: Vec<_> = services
.rooms
.directory
.public_rooms()
.then(|room_id| get_room_info(services, room_id))
-3
View File
@@ -30,7 +30,6 @@ pub(crate) enum RoomInfoCommand {
async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> Result {
let room_name = self
.services
.rooms
.state_accessor
.get_name(&room_id)
.await
@@ -38,7 +37,6 @@ async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> R
let member_info: Vec<_> = self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_filter(|user_id| {
@@ -75,7 +73,6 @@ async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> R
async fn view_room_topic(&self, room_id: OwnedRoomId) -> Result {
let Ok(room_topic) = self
.services
.rooms
.state_accessor
.get_room_topic(&room_id)
.await
+8
View File
@@ -56,4 +56,12 @@ pub(super) enum RoomCommand {
Exists {
room_id: OwnedRoomId,
},
/// - Delete room
DeleteRoom {
room_id: OwnedRoomId,
#[arg(short, long)]
force: bool,
},
}
+26 -58
View File
@@ -1,7 +1,6 @@
use clap::Subcommand;
use futures::{FutureExt, StreamExt};
use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId};
use tuwunel_api::client::leave_room;
use tuwunel_core::{
Err, Result, debug,
utils::{IterStream, ReadyExt},
@@ -70,10 +69,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
};
debug!("Room specified is a room ID, banning room ID");
self.services
.rooms
.metadata
.ban_room(room_id, true);
self.services.metadata.ban_room(room_id);
room_id.to_owned()
} else if room.is_room_alias_id() {
@@ -95,7 +91,6 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
let room_id = match self
.services
.rooms
.alias
.resolve_local_alias(room_alias)
.await
@@ -109,7 +104,6 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
@@ -131,10 +125,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
},
};
self.services
.rooms
.metadata
.ban_room(&room_id, true);
self.services.metadata.ban_room(&room_id);
room_id
} else {
@@ -148,7 +139,6 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
debug!("Making all users leave the room {room_id} and forgetting it");
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
@@ -161,27 +151,31 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
evicting admins too)",
);
if let Err(e) = leave_room(self.services, user_id, &room_id, None)
let state_lock = self.services.state.mutex.lock(&room_id).await;
if let Err(e) = self
.services
.membership
.leave(user_id, &room_id, None, false, &state_lock)
.boxed()
.await
{
warn!("Failed to leave room: {e}");
}
drop(state_lock);
self.services
.rooms
.state_cache
.forget(&room_id, user_id);
}
self.services
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.for_each(async |local_alias| {
self.services
.rooms
.alias
.remove_alias(&local_alias, &self.services.globals.server_user)
.await
@@ -190,15 +184,9 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
.await;
// unpublish from room directory
self.services
.rooms
.directory
.set_not_public(&room_id);
self.services.directory.set_not_public(&room_id);
self.services
.rooms
.metadata
.disable_room(&room_id, true);
self.services.metadata.disable_room(&room_id);
self.write_str(
"Room banned, removed all our local users, and disabled incoming federation with room.",
@@ -258,7 +246,6 @@ async fn ban_list_of_rooms(&self) -> Result {
| Ok(room_alias) => {
let room_id = match self
.services
.rooms
.alias
.resolve_local_alias(room_alias)
.await
@@ -272,7 +259,6 @@ async fn ban_list_of_rooms(&self) -> Result {
match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
@@ -320,10 +306,7 @@ async fn ban_list_of_rooms(&self) -> Result {
}
for room_id in room_ids {
self.services
.rooms
.metadata
.ban_room(&room_id, true);
self.services.metadata.ban_room(&room_id);
debug!("Banned {room_id} successfully");
room_ban_count = room_ban_count.saturating_add(1);
@@ -331,7 +314,6 @@ async fn ban_list_of_rooms(&self) -> Result {
debug!("Making all users leave the room {room_id} and forgetting it");
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
@@ -344,28 +326,32 @@ async fn ban_list_of_rooms(&self) -> Result {
evicting admins too)",
);
if let Err(e) = leave_room(self.services, user_id, &room_id, None)
let state_lock = self.services.state.mutex.lock(&room_id).await;
if let Err(e) = self
.services
.membership
.leave(user_id, &room_id, None, false, &state_lock)
.boxed()
.await
{
warn!("Failed to leave room: {e}");
}
drop(state_lock);
self.services
.rooms
.state_cache
.forget(&room_id, user_id);
}
// remove any local aliases, ignore errors
self.services
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.for_each(async |local_alias| {
self.services
.rooms
.alias
.remove_alias(&local_alias, &self.services.globals.server_user)
.await
@@ -374,15 +360,9 @@ async fn ban_list_of_rooms(&self) -> Result {
.await;
// unpublish from room directory, ignore errors
self.services
.rooms
.directory
.set_not_public(&room_id);
self.services.directory.set_not_public(&room_id);
self.services
.rooms
.metadata
.disable_room(&room_id, true);
self.services.metadata.disable_room(&room_id);
}
self.write_str(&format!(
@@ -407,10 +387,7 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
};
debug!("Room specified is a room ID, unbanning room ID");
self.services
.rooms
.metadata
.ban_room(room_id, false);
self.services.metadata.unban_room(room_id);
room_id.to_owned()
} else if room.is_room_alias_id() {
@@ -432,7 +409,6 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
let room_id = match self
.services
.rooms
.alias
.resolve_local_alias(room_alias)
.await
@@ -446,7 +422,6 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
@@ -466,10 +441,7 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
},
};
self.services
.rooms
.metadata
.ban_room(&room_id, false);
self.services.metadata.unban_room(&room_id);
room_id
} else {
@@ -480,10 +452,7 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
);
};
self.services
.rooms
.metadata
.disable_room(&room_id, false);
self.services.metadata.enable_room(&room_id);
self.write_str("Room unbanned and federation re-enabled.")
.await
}
@@ -492,7 +461,6 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
async fn list_banned_rooms(&self, no_details: bool) -> Result {
let room_ids: Vec<OwnedRoomId> = self
.services
.rooms
.metadata
.list_banned_rooms()
.map(Into::into)
+194 -152
View File
@@ -2,30 +2,27 @@
use futures::{FutureExt, StreamExt};
use ruma::{
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
Int, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
events::{
RoomAccountDataEventType, StateEventType,
room::{
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent, UserPowerLevel},
redaction::RoomRedactionEventContent,
},
tag::{TagEvent, TagEventContent, TagInfo},
},
};
use tuwunel_api::client::{
full_user_deactivate, join_room_by_id_helper, leave_all_rooms, leave_room, update_avatar_url,
update_displayname,
};
use tuwunel_core::{
Err, Result, debug, debug_warn, error, info, is_equal_to,
matrix::{Event, pdu::PduBuilder},
utils::{self, ReadyExt},
warn,
};
use tuwunel_service::Services;
use crate::{
admin_command, get_room_info,
utils::{parse_active_local_user_id, parse_local_user_id},
utils::{parse_active_local_user_id, parse_local_user_id, parse_user_id},
};
const AUTO_GEN_PASSWORD_LENGTH: usize = 25;
@@ -122,7 +119,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
.is_empty()
{
for room in &self.services.server.config.auto_join_rooms {
let Ok(room_id) = self.services.rooms.alias.resolve(room).await else {
let Ok(room_id) = self.services.alias.resolve(room).await else {
error!(
%user_id,
"Failed to resolve room alias to room ID when attempting to auto join {room}, skipping"
@@ -132,7 +129,6 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
if !self
.services
.rooms
.state_cache
.server_in_room(self.services.globals.server_name(), &room_id)
.await
@@ -143,20 +139,24 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
continue;
}
let state_lock = self.services.state.mutex.lock(&room_id).await;
if let Some(room_server_name) = room.server_name() {
match join_room_by_id_helper(
self.services,
&user_id,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
&[
self.services.globals.server_name().to_owned(),
room_server_name.to_owned(),
],
None,
&None,
)
.await
match self
.services
.membership
.join(
&user_id,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
&[
self.services.globals.server_name().to_owned(),
room_server_name.to_owned(),
],
&None,
&state_lock,
)
.await
{
| Ok(_response) => {
info!("Automatically joined room {room} for user {user_id}");
@@ -175,6 +175,8 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
.await;
},
}
drop(state_lock);
}
}
}
@@ -186,7 +188,6 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
if let Ok(admin_room) = self.services.admin.get_admin_room().await {
if self
.services
.rooms
.state_cache
.room_joined_count(&admin_room)
.await
@@ -195,6 +196,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
self.services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
warn!("Granting {user_id} admin privileges as the first user");
}
@@ -216,34 +218,7 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
return Err!("Not allowed to deactivate the server service account.",);
}
self.services
.users
.deactivate_account(&user_id)
.await?;
if !no_leave_rooms {
self.services
.admin
.send_text(&format!("Making {user_id} leave all rooms after deactivation..."))
.await;
let all_joined_rooms: Vec<OwnedRoomId> = self
.services
.rooms
.state_cache
.rooms_joined(&user_id)
.map(Into::into)
.collect()
.await;
full_user_deactivate(self.services, &user_id, &all_joined_rooms)
.boxed()
.await?;
update_displayname(self.services, &user_id, None, &all_joined_rooms).await;
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms).await;
leave_all_rooms(self.services, &user_id).await;
}
deactivate_user(self.services, &user_id, no_leave_rooms).await?;
self.write_str(&format!("User {user_id} has been deactivated"))
.await
@@ -336,41 +311,16 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
let mut deactivation_count: usize = 0;
for user_id in user_ids {
match self
.services
.users
.deactivate_account(&user_id)
.await
{
match deactivate_user(self.services, &user_id, no_leave_rooms).await {
| Ok(()) => {
deactivation_count = deactivation_count.saturating_add(1);
},
| Err(e) => {
self.services
.admin
.send_text(&format!("Failed deactivating user: {e}"))
.await;
},
| Ok(()) => {
deactivation_count = deactivation_count.saturating_add(1);
if !no_leave_rooms {
info!("Forcing user {user_id} to leave all rooms apart of deactivate-all");
let all_joined_rooms: Vec<OwnedRoomId> = self
.services
.rooms
.state_cache
.rooms_joined(&user_id)
.map(Into::into)
.collect()
.await;
full_user_deactivate(self.services, &user_id, &all_joined_rooms)
.boxed()
.await?;
update_displayname(self.services, &user_id, None, &all_joined_rooms).await;
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms)
.await;
leave_all_rooms(self.services, &user_id).await;
}
},
}
}
@@ -387,6 +337,20 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
.await
}
async fn deactivate_user(services: &Services, user_id: &UserId, no_leave_rooms: bool) -> Result {
if !no_leave_rooms {
services
.deactivate
.full_deactivate(user_id)
.boxed()
.await?;
} else {
services.users.deactivate_account(user_id).await?;
}
Ok(())
}
#[admin_command]
pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result {
// Validate user id
@@ -394,7 +358,6 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result {
let mut rooms: Vec<(OwnedRoomId, u64, String)> = self
.services
.rooms
.state_cache
.rooms_joined(&user_id)
.then(|room_id| get_room_info(self.services, room_id))
@@ -444,14 +407,12 @@ pub(super) async fn force_join_list_of_local_users(
let (room_id, servers) = self
.services
.rooms
.alias
.resolve_with_servers(&room_id, None)
.await?;
if !self
.services
.rooms
.state_cache
.server_in_room(self.services.globals.server_name(), &room_id)
.await
@@ -461,7 +422,6 @@ pub(super) async fn force_join_list_of_local_users(
let server_admins: Vec<_> = self
.services
.rooms
.state_cache
.active_local_users_in_room(&admin_room)
.map(ToOwned::to_owned)
@@ -470,7 +430,6 @@ pub(super) async fn force_join_list_of_local_users(
if !self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
@@ -518,17 +477,21 @@ pub(super) async fn force_join_list_of_local_users(
let mut failed_joins: usize = 0;
let mut successful_joins: usize = 0;
let state_lock = self.services.state.mutex.lock(&room_id).await;
for user_id in user_ids {
match join_room_by_id_helper(
self.services,
&user_id,
&room_id,
Some(String::from(BULK_JOIN_REASON)),
&servers,
None,
&None,
)
.await
match self
.services
.membership
.join(
&user_id,
&room_id,
Some(String::from(BULK_JOIN_REASON)),
&servers,
&None,
&state_lock,
)
.await
{
| Ok(_res) => {
successful_joins = successful_joins.saturating_add(1);
@@ -540,6 +503,8 @@ pub(super) async fn force_join_list_of_local_users(
}
}
drop(state_lock);
self.write_str(&format!(
"{successful_joins} local users have been joined to {room_id}. {failed_joins} joins \
failed.",
@@ -566,14 +531,12 @@ pub(super) async fn force_join_all_local_users(
let (room_id, servers) = self
.services
.rooms
.alias
.resolve_with_servers(&room_id, None)
.await?;
if !self
.services
.rooms
.state_cache
.server_in_room(self.services.globals.server_name(), &room_id)
.await
@@ -583,7 +546,6 @@ pub(super) async fn force_join_all_local_users(
let server_admins: Vec<_> = self
.services
.rooms
.state_cache
.active_local_users_in_room(&admin_room)
.map(ToOwned::to_owned)
@@ -592,7 +554,6 @@ pub(super) async fn force_join_all_local_users(
if !self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
@@ -604,6 +565,8 @@ pub(super) async fn force_join_all_local_users(
let mut failed_joins: usize = 0;
let mut successful_joins: usize = 0;
let state_lock = self.services.state.mutex.lock(&room_id).await;
for user_id in &self
.services
.users
@@ -612,16 +575,18 @@ pub(super) async fn force_join_all_local_users(
.collect::<Vec<_>>()
.await
{
match join_room_by_id_helper(
self.services,
user_id,
&room_id,
Some(String::from(BULK_JOIN_REASON)),
&servers,
None,
&None,
)
.await
match self
.services
.membership
.join(
user_id,
&room_id,
Some(String::from(BULK_JOIN_REASON)),
&servers,
&None,
&state_lock,
)
.await
{
| Ok(_res) => {
successful_joins = successful_joins.saturating_add(1);
@@ -633,6 +598,8 @@ pub(super) async fn force_join_all_local_users(
}
}
drop(state_lock);
self.write_str(&format!(
"{successful_joins} local users have been joined to {room_id}. {failed_joins} joins \
failed.",
@@ -649,7 +616,6 @@ pub(super) async fn force_join_room(
let user_id = parse_local_user_id(self.services, &user_id)?;
let (room_id, servers) = self
.services
.rooms
.alias
.resolve_with_servers(&room_id, None)
.await?;
@@ -658,9 +624,16 @@ pub(super) async fn force_join_room(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers, None, &None)
let state_lock = self.services.state.mutex.lock(&room_id).await;
self.services
.membership
.join(&user_id, &room_id, None, &servers, &None, &state_lock)
.await?;
drop(state_lock);
self.write_str(&format!("{user_id} has been joined to {room_id}.",))
.await
}
@@ -672,12 +645,7 @@ pub(super) async fn force_leave_room(
room_id: OwnedRoomOrAliasId,
) -> Result {
let user_id = parse_local_user_id(self.services, &user_id)?;
let room_id = self
.services
.rooms
.alias
.resolve(&room_id)
.await?;
let room_id = self.services.alias.resolve(&room_id).await?;
assert!(
self.services.globals.user_is_local(&user_id),
@@ -686,7 +654,6 @@ pub(super) async fn force_leave_room(
if !self
.services
.rooms
.state_cache
.is_joined(&user_id, &room_id)
.await
@@ -694,10 +661,16 @@ pub(super) async fn force_leave_room(
return Err!("{user_id} is not joined in the room");
}
leave_room(self.services, &user_id, &room_id, None)
let state_lock = self.services.state.mutex.lock(&room_id).await;
self.services
.membership
.leave(&user_id, &room_id, None, false, &state_lock)
.boxed()
.await?;
drop(state_lock);
self.write_str(&format!("{user_id} has left {room_id}.",))
.await
}
@@ -705,57 +678,49 @@ pub(super) async fn force_leave_room(
#[admin_command]
pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAliasId) -> Result {
let user_id = parse_local_user_id(self.services, &user_id)?;
let room_id = self
.services
.rooms
.alias
.resolve(&room_id)
.await?;
let room_id = self.services.alias.resolve(&room_id).await?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
let state_lock = self
.services
.rooms
.state
.mutex
.lock(&room_id)
.await;
let state_lock = self.services.state.mutex.lock(&room_id).await;
let room_power_levels: Option<RoomPowerLevelsEventContent> = self
let room_power_levels: Option<RoomPowerLevels> = self
.services
.rooms
.state_accessor
.room_state_get_content(&room_id, &StateEventType::RoomPowerLevels, "")
.get_power_levels(&room_id)
.await
.ok();
let user_can_demote_self = room_power_levels
let user_can_change_self = room_power_levels
.as_ref()
.is_some_and(|power_levels_content| {
RoomPowerLevels::from(power_levels_content.clone())
.user_can_change_user_power_level(&user_id, &user_id)
}) || self
.services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
.is_some_and(|power_levels| {
power_levels.user_can_change_user_power_level(&user_id, &user_id)
});
let user_can_demote_self = user_can_change_self
|| self
.services
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
if !user_can_demote_self {
return Err!("User is not allowed to modify their own power levels in the room.",);
return Err!("User is not allowed to modify their own power levels in the room.");
}
let mut power_levels_content = room_power_levels.unwrap_or_default();
let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels
.map(TryInto::try_into)
.transpose()?
.unwrap_or_default();
power_levels_content.users.remove(&user_id);
let event_id = self
.services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &power_levels_content),
@@ -772,6 +737,85 @@ pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAli
.await
}
#[admin_command]
pub(super) async fn force_promote(
&self,
target_id: String,
room_id: OwnedRoomOrAliasId,
) -> Result {
let target_id = parse_user_id(self.services, &target_id)?;
let room_id = self.services.alias.resolve(&room_id).await?;
let state_lock = self.services.state.mutex.lock(&room_id).await;
let room_power_levels = self
.services
.state_accessor
.get_power_levels(&room_id)
.await?;
let privileged_member = self
.services
.state_cache
.room_members(&room_id)
.ready_filter(|member_id| {
self.services.globals.user_is_local(member_id)
&& room_power_levels.user_can_change_user_power_level(member_id, &target_id)
})
.map(ToOwned::to_owned)
.ready_fold_default(|selected_user, member_id| match selected_user {
| None => Some(member_id),
| Some(selected_user) => Some(
if room_power_levels.for_user(&selected_user)
> room_power_levels.for_user(&member_id)
{
selected_user
} else {
member_id
},
),
})
.await;
let Some(privileged_member) = privileged_member else {
return Err!("No privileged user exists in room, cannot promote.");
};
info!("Selected privileged member {privileged_member}");
let power_level: Int = match room_power_levels.for_user(&privileged_member) {
| UserPowerLevel::Infinite => Int::MAX,
| UserPowerLevel::Int(x) => x,
};
let mut power_levels_content: RoomPowerLevelsEventContent = room_power_levels.try_into()?;
power_levels_content
.users
.insert(target_id.clone(), power_level);
let event_id = self
.services
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &power_levels_content),
&privileged_member,
&room_id,
&state_lock,
)
.await?;
drop(state_lock);
self.write_str(&format!(
"User {privileged_member} promoted {target_id} to {power_level} power level in \
{room_id} - {event_id}"
))
.await?;
Ok(())
}
#[admin_command]
pub(super) async fn make_user_admin(&self, user_id: String) -> Result {
let user_id = parse_local_user_id(self.services, &user_id)?;
@@ -783,6 +827,7 @@ pub(super) async fn make_user_admin(&self, user_id: String) -> Result {
self.services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
self.write_str(&format!("{user_id} has been granted admin privileges.",))
@@ -889,7 +934,6 @@ pub(super) async fn get_room_tags(&self, user_id: String, room_id: OwnedRoomId)
pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
let Ok(event) = self
.services
.rooms
.timeline
.get_non_outlier_pdu(&event_id)
.await
@@ -917,14 +961,12 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
let redaction_event_id = {
let state_lock = self
.services
.rooms
.state
.mutex
.lock(event.room_id())
.await;
self.services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
+6
View File
@@ -88,6 +88,12 @@ pub(super) enum UserCommand {
room_id: OwnedRoomOrAliasId,
},
/// - Force promote
ForcePromote {
user_id: String,
room_id: OwnedRoomOrAliasId,
},
/// - Grant server-admin privileges to a user.
MakeUserAdmin {
user_id: String,
-2
View File
@@ -17,13 +17,11 @@ pub(crate) async fn get_room_info(
(
room_id.into(),
services
.rooms
.state_cache
.room_joined_count(room_id)
.await
.unwrap_or(0),
services
.rooms
.state_accessor
.get_name(room_id)
.await
+11 -236
View File
@@ -1,32 +1,13 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::{FutureExt, StreamExt};
use ruma::{
OwnedRoomId, UserId,
api::client::{
account::{
ThirdPartyIdRemovalStatus, change_password, deactivate, get_3pids,
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
whoami,
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
events::{
StateEventType,
room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
use ruma::api::client::account::{
ThirdPartyIdRemovalStatus, change_password, deactivate, get_3pids,
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, whoami,
};
use tuwunel_core::{
Err, Error, Result, err, info,
matrix::{Event, pdu::PduBuilder},
utils,
utils::{ReadyExt, stream::BroadbandExt},
warn,
};
use tuwunel_service::Services;
use tuwunel_core::{Err, Result, info, utils::ReadyExt};
use super::SESSION_ID_LENGTH;
use crate::Ruma;
use crate::{Ruma, router::auth_uiaa};
/// # `POST /_matrix/client/r0/account/password`
///
@@ -51,48 +32,7 @@ pub(crate) async fn change_password_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<change_password::v3::Request>,
) -> Result<change_password::v3::Response> {
// Authentication for this endpoint was made optional, but we need
// authentication currently
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, body.sender_device(), &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
let ref sender_user = auth_uiaa(&services, &body).await?;
services
.users
@@ -104,32 +44,9 @@ pub(crate) async fn change_password_route(
services
.users
.all_device_ids(sender_user)
.ready_filter(|id| *id != body.sender_device())
.ready_filter(|&id| Some(id) != body.sender_device.as_deref())
.for_each(|id| services.users.remove_device(sender_user, id))
.await;
// Remove all pushers except the ones associated with this session
services
.pusher
.get_pushkeys(sender_user)
.map(ToOwned::to_owned)
.broad_filter_map(async |pushkey| {
services
.pusher
.get_pusher_device(&pushkey)
.await
.ok()
.filter(|pusher_device| pusher_device != body.sender_device())
.is_some()
.then_some(pushkey)
})
.for_each(async |pushkey| {
services
.pusher
.delete_pusher(sender_user, &pushkey)
.await;
})
.await;
}
info!("User {sender_user} changed their password.");
@@ -180,66 +97,15 @@ pub(crate) async fn deactivate_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<deactivate::v3::Request>,
) -> Result<deactivate::v3::Response> {
// Authentication for this endpoint was made optional, but we need
// authentication currently
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let ref sender_user = auth_uiaa(&services, &body).await?;
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, body.sender_device(), &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
// Remove profile pictures and display name
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(sender_user)
.map(Into::into)
.collect()
.await;
super::update_displayname(&services, sender_user, None, &all_joined_rooms).await;
super::update_avatar_url(&services, sender_user, None, None, &all_joined_rooms).await;
full_user_deactivate(&services, sender_user, &all_joined_rooms)
services
.deactivate
.full_deactivate(sender_user)
.boxed()
.await?;
info!("User {sender_user} deactivated their account.");
if services.server.config.admin_room_notices {
services
.admin
@@ -293,94 +159,3 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
}
/// Runs through all the deactivation steps:
///
/// - Mark as deactivated
/// - Removing display name
/// - Removing avatar URL and blurhash
/// - Removing all profile data
/// - Leaving all rooms (and forgets all of them)
pub async fn full_user_deactivate(
services: &Services,
user_id: &UserId,
all_joined_rooms: &[OwnedRoomId],
) -> Result {
services
.users
.deactivate_account(user_id)
.await
.ok();
super::update_displayname(services, user_id, None, all_joined_rooms).await;
super::update_avatar_url(services, user_id, None, None, all_joined_rooms).await;
services
.users
.all_profile_keys(user_id)
.ready_for_each(|(profile_key, _)| {
services
.users
.set_profile_key(user_id, &profile_key, None);
})
.await;
for room_id in all_joined_rooms {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let room_power_levels = services
.rooms
.state_accessor
.room_state_get_content::<RoomPowerLevelsEventContent>(
room_id,
&StateEventType::RoomPowerLevels,
"",
)
.await
.ok();
let user_can_demote_self =
room_power_levels
.as_ref()
.is_some_and(|power_levels_content| {
RoomPowerLevels::from(power_levels_content.clone())
.user_can_change_user_power_level(user_id, user_id)
}) || services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
if user_can_demote_self {
let mut power_levels_content = room_power_levels.unwrap_or_default();
power_levels_content.users.remove(user_id);
// ignore errors so deactivation doesn't fail
match services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &power_levels_content),
user_id,
room_id,
&state_lock,
)
.await
{
| Err(e) => {
warn!(%room_id, %user_id, "Failed to demote user's own power level: {e}");
},
| _ => {
info!("Demoted {user_id} in {room_id} as part of account deactivation");
},
}
}
}
super::leave_all_rooms(services, user_id)
.boxed()
.await;
Ok(())
}
-7
View File
@@ -19,7 +19,6 @@ pub(crate) async fn create_alias_route(
) -> Result<create_alias::v3::Response> {
let sender_user = body.sender_user();
services
.rooms
.alias
.appservice_checks(&body.room_alias, &body.appservice_info)
.await?;
@@ -35,7 +34,6 @@ pub(crate) async fn create_alias_route(
}
if services
.rooms
.alias
.resolve_local_alias(&body.room_alias)
.await
@@ -45,7 +43,6 @@ pub(crate) async fn create_alias_route(
}
services
.rooms
.alias
.set_alias(&body.room_alias, &body.room_id, sender_user)?;
@@ -63,13 +60,11 @@ pub(crate) async fn delete_alias_route(
) -> Result<delete_alias::v3::Response> {
let sender_user = body.sender_user();
services
.rooms
.alias
.appservice_checks(&body.room_alias, &body.appservice_info)
.await?;
services
.rooms
.alias
.remove_alias(&body.room_alias, sender_user)
.await?;
@@ -89,7 +84,6 @@ pub(crate) async fn get_alias_route(
let room_alias = body.body.room_alias;
let Ok((room_id, servers)) = services
.rooms
.alias
.resolve_alias(&room_alias, None)
.await
@@ -111,7 +105,6 @@ async fn room_available_servers(
) -> Vec<OwnedServerName> {
// find active servers in room state cache to suggest
let mut servers: Vec<OwnedServerName> = services
.rooms
.state_cache
.room_servers(room_id)
.map(ToOwned::to_owned)
+7 -5
View File
@@ -3,9 +3,12 @@
use axum::extract::State;
use ruma::{
RoomVersionId,
api::client::discovery::get_capabilities::{
self, Capabilities, GetLoginTokenCapability, RoomVersionStability,
RoomVersionsCapability, ThirdPartyIdChangesCapability,
api::client::discovery::{
get_capabilities,
get_capabilities::v3::{
Capabilities, GetLoginTokenCapability, ProfileFieldsCapability, RoomVersionStability,
RoomVersionsCapability, ThirdPartyIdChangesCapability,
},
},
};
use serde_json::json;
@@ -41,8 +44,7 @@ pub(crate) async fn get_capabilities_route(
enabled: services.server.config.login_via_existing_session,
};
// MSC4133 capability
capabilities.set("uk.tcpip.msc4133.profile_fields", json!({"enabled": true}))?;
capabilities.profile_fields = ProfileFieldsCapability::new(true).into();
capabilities.set(
"org.matrix.msc4267.forget_forced_upon_leave",
+9 -22
View File
@@ -38,7 +38,7 @@ pub(crate) async fn get_context_route(
let event_id = &body.event_id;
let filter = &body.filter;
if !services.rooms.metadata.exists(room_id).await {
if !services.metadata.exists(room_id).await {
return Err!(Request(Forbidden("Room does not exist to this server")));
}
@@ -50,19 +50,16 @@ pub(crate) async fn get_context_route(
.min(LIMIT_MAX);
let base_id = services
.rooms
.timeline
.get_pdu_id(event_id)
.map_err(|_| err!(Request(NotFound("Event not found."))));
let base_pdu = services
.rooms
.timeline
.get_pdu(event_id)
.map_err(|_| err!(Request(NotFound("Base event not found."))));
let visible = services
.rooms
.state_accessor
.user_can_see_event(sender_user, room_id, event_id)
.map(Ok);
@@ -74,7 +71,11 @@ pub(crate) async fn get_context_route(
}
if !visible {
debug_warn!(req_evt = ?event_id, ?base_id, ?room_id, "Event requested by {sender_user} but is not allowed to see it, returning 404");
debug_warn!(
req_evt = ?event_id, ?base_id, ?room_id,
"Event requested by {sender_user} but is not allowed to see it."
);
return Err!(Request(NotFound("Event not found.")));
}
@@ -83,7 +84,6 @@ pub(crate) async fn get_context_route(
let base_event = ignored_filter(&services, (base_count, base_pdu), sender_user);
let events_before = services
.rooms
.timeline
.pdus_rev(Some(sender_user), room_id, Some(base_count))
.ignore_err()
@@ -94,14 +94,13 @@ pub(crate) async fn get_context_route(
.collect();
let events_after = services
.rooms
.timeline
.pdus(Some(sender_user), room_id, Some(base_count))
.ignore_err()
.ready_filter_map(|item| event_filter(item, filter))
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
.take(limit / 2)
.take(limit.div_ceil(2))
.collect();
let (base_event, events_before, events_after): (_, Vec<_>, Vec<_>) =
@@ -135,18 +134,11 @@ pub(crate) async fn get_context_route(
.map_or_else(|| body.event_id.as_ref(), |pdu| pdu.event_id.as_ref());
let state_ids = services
.rooms
.state_accessor
.pdu_shortstatehash(state_at)
.or_else(|_| {
services
.rooms
.state
.get_room_shortstatehash(room_id)
})
.or_else(|_| services.state.get_room_shortstatehash(room_id))
.map_ok(|shortstatehash| {
services
.rooms
.state_accessor
.state_full_ids(shortstatehash)
.map(Ok)
@@ -163,7 +155,6 @@ pub(crate) async fn get_context_route(
let shorteventids = state_ids.iter().map(ref_at!(1)).stream();
let lazy_loading_witnessed = lazy_loading_witnessed.unwrap_or_default();
let state: Vec<_> = services
.rooms
.short
.multi_get_statekey_from_short(shortstatekeys)
.zip(shorteventids)
@@ -182,11 +173,7 @@ pub(crate) async fn get_context_route(
Some(event_id)
})
.broad_filter_map(|event_id: &OwnedEventId| {
services
.rooms
.timeline
.get_pdu(event_id.as_ref())
.ok()
services.timeline.get_pdu(event_id.as_ref()).ok()
})
.map(Event::into_format)
.collect()
+8 -79
View File
@@ -3,16 +3,13 @@
use futures::StreamExt;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedDeviceId,
api::client::{
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
error::ErrorKind,
uiaa::{AuthFlow, AuthType, UiaaInfo},
api::client::device::{
self, delete_device, delete_devices, get_device, get_devices, update_device,
},
};
use tuwunel_core::{Err, Error, Result, debug, err, utils};
use tuwunel_core::{Err, Result, debug, err, utils};
use super::SESSION_ID_LENGTH;
use crate::{Ruma, client::DEVICE_ID_LENGTH};
use crate::{Ruma, client::DEVICE_ID_LENGTH, router::auth_uiaa};
/// # `GET /_matrix/client/r0/devices`
///
@@ -126,10 +123,10 @@ pub(crate) async fn delete_device_route(
State(services): State<crate::State>,
body: Ruma<delete_device::v3::Request>,
) -> Result<delete_device::v3::Response> {
let (sender_user, sender_device) = body.sender();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
let sender_user = body.sender_user();
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
@@ -142,41 +139,7 @@ pub(crate) async fn delete_device_route(
return Ok(delete_device::v3::Response {});
}
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err!(Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
return Err!(Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("Not json.")));
},
},
}
let ref sender_user = auth_uiaa(&services, &body).await?;
services
.users
@@ -203,10 +166,10 @@ pub(crate) async fn delete_devices_route(
State(services): State<crate::State>,
body: Ruma<delete_devices::v3::Request>,
) -> Result<delete_devices::v3::Response> {
let (sender_user, sender_device) = body.sender();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
let sender_user = body.sender_user();
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
@@ -221,41 +184,7 @@ pub(crate) async fn delete_devices_route(
return Ok(delete_devices::v3::Response {});
}
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
};
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
},
},
}
let ref sender_user = auth_uiaa(&services, &body).await?;
for device_id in &body.devices {
services
+50 -73
View File
@@ -16,18 +16,15 @@
},
federation,
},
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
directory::{Filter, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
events::{
StateEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
},
uint,
};
use tuwunel_core::{
Err, Result, err, info,
Err, Result, err, info, is_true,
matrix::Event,
utils::{
TryFutureExtExt,
@@ -51,7 +48,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_public_rooms_filtered::v3::Request>,
) -> Result<get_public_rooms_filtered::v3::Response> {
check_banned(&services, body.server.as_deref())?;
check_server_banned(&services, body.server.as_deref())?;
let response = get_public_rooms_filtered_helper(
&services,
@@ -80,7 +77,7 @@ pub(crate) async fn get_public_rooms_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_public_rooms::v3::Request>,
) -> Result<get_public_rooms::v3::Response> {
check_banned(&services, body.server.as_deref())?;
check_server_banned(&services, body.server.as_deref())?;
let response = get_public_rooms_filtered_helper(
&services,
@@ -114,12 +111,7 @@ pub(crate) async fn set_room_visibility_route(
) -> Result<set_room_visibility::v3::Response> {
let sender_user = body.sender_user();
if !services
.rooms
.metadata
.exists(&body.room_id)
.await
{
if !services.metadata.exists(&body.room_id).await {
// Return 404 if the room doesn't exist
return Err!(Request(NotFound("Room not found")));
}
@@ -169,7 +161,7 @@ pub(crate) async fn set_room_visibility_route(
)));
}
services.rooms.directory.set_public(&body.room_id);
services.directory.set_public(&body.room_id);
if services.server.config.admin_room_notices {
services
@@ -182,10 +174,7 @@ pub(crate) async fn set_room_visibility_route(
}
info!("{sender_user} made {0} public to the room directory", body.room_id);
},
| room::Visibility::Private => services
.rooms
.directory
.set_not_public(&body.room_id),
| room::Visibility::Private => services.directory.set_not_public(&body.room_id),
| _ => {
return Err!(Request(InvalidParam("Room visibility type is not supported.",)));
},
@@ -201,19 +190,13 @@ pub(crate) async fn get_room_visibility_route(
State(services): State<crate::State>,
body: Ruma<get_room_visibility::v3::Request>,
) -> Result<get_room_visibility::v3::Response> {
if !services
.rooms
.metadata
.exists(&body.room_id)
.await
{
if !services.metadata.exists(&body.room_id).await {
// Return 404 if the room doesn't exist
return Err!(Request(NotFound("Room not found")));
}
Ok(get_room_visibility::v3::Response {
visibility: if services
.rooms
.directory
.is_public_room(&body.room_id)
.await
@@ -298,13 +281,12 @@ pub(crate) async fn get_public_rooms_filtered_helper(
let meta_public_rooms = search_room_id
.filter(|_| services.config.allow_unlisted_room_search_by_id)
.map(|prefix| services.rooms.metadata.public_ids_prefix(prefix))
.map(|prefix| services.metadata.public_ids_prefix(prefix))
.into_iter()
.stream()
.flatten();
let mut all_rooms: Vec<PublicRoomsChunk> = services
.rooms
.directory
.public_rooms()
.map(ToOwned::to_owned)
@@ -391,20 +373,14 @@ async fn user_can_publish_room(
room_id: &RoomId,
) -> Result<bool> {
match services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
.get_power_levels(room_id)
.await
{
| Ok(event) => serde_json::from_str(event.content().get())
.map_err(|_| err!(Database("Invalid event content for m.room.power_levels")))
.map(|content: RoomPowerLevelsEventContent| {
RoomPowerLevels::from(content)
.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
}),
| Ok(power_levels) =>
Ok(power_levels.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)),
| _ => {
match services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await
@@ -417,57 +393,57 @@ async fn user_can_publish_room(
}
async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> PublicRoomsChunk {
let name = services
.rooms
.state_accessor
.get_name(&room_id)
.ok();
let name = services.state_accessor.get_name(&room_id).ok();
let room_type = services
.rooms
.state_accessor
.get_room_type(&room_id)
.ok();
let canonical_alias = services
.rooms
.state_accessor
.get_canonical_alias(&room_id)
.ok()
.then(async |alias| {
if let Some(alias) = alias
&& services.globals.alias_is_local(&alias)
&& let Ok(alias_room_id) = services.alias.resolve_local_alias(&alias).await
&& alias_room_id == room_id
{
Some(alias)
} else {
None
}
});
let avatar_url = services
.state_accessor
.get_avatar(&room_id)
.map_ok(|content| content.url)
.ok();
let avatar_url = services.rooms.state_accessor.get_avatar(&room_id);
let topic = services
.rooms
.state_accessor
.get_room_topic(&room_id)
.ok();
let world_readable = services
.rooms
.state_accessor
.is_world_readable(&room_id);
let join_rule = services
.rooms
.state_accessor
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
| JoinRule::Public => PublicRoomJoinRule::Public,
| JoinRule::Public => "public".into(),
| JoinRule::Knock => "knock".into(),
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
| _ => "invite".into(),
});
let guest_can_join = services
.rooms
.state_accessor
.guest_can_join(&room_id);
let guest_can_join = services.state_accessor.guest_can_join(&room_id);
let num_joined_members = services
.rooms
.state_cache
.room_joined_count(&room_id);
let num_joined_members = services.state_cache.room_joined_count(&room_id);
let (
(avatar_url, canonical_alias, guest_can_join, join_rule, name),
@@ -480,7 +456,7 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
.await;
PublicRoomsChunk {
avatar_url: avatar_url.into_option().unwrap_or_default().url,
avatar_url: avatar_url.flatten(),
canonical_alias,
guest_can_join,
join_rule: join_rule.unwrap_or_default(),
@@ -497,24 +473,25 @@ async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> Public
}
}
fn check_banned(services: &Services, server: Option<&ServerName>) -> Result {
fn check_server_banned(services: &Services, server: Option<&ServerName>) -> Result {
let Some(server) = server else {
return Ok(());
};
let forbidden_remote_directory = services
.config
.forbidden_remote_room_directory_server_names
.is_match(server.host());
let conditions = [
services
.config
.forbidden_remote_room_directory_server_names
.is_match(server.host()),
services
.config
.forbidden_remote_server_names
.is_match(server.host()),
];
let forbidden_remote_server = services
.config
.forbidden_remote_server_names
.is_match(server.host());
if forbidden_remote_directory || forbidden_remote_server {
Err!(Request(Forbidden("Server is banned on this homeserver.")))
} else {
Ok(())
if conditions.iter().any(is_true!()) {
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
Ok(())
}
+19 -19
View File
@@ -3,7 +3,8 @@
use axum::extract::State;
use futures::{StreamExt, stream::FuturesUnordered};
use ruma::{
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
CanonicalJsonObject, CanonicalJsonValue, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId,
UserId,
api::{
client::{
error::ErrorKind,
@@ -39,7 +40,11 @@ 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.one_time_keys {
for (key_id, one_time_key) in body
.one_time_keys
.iter()
.take(services.config.one_time_key_limit)
{
if one_time_key
.deserialize()
.inspect_err(|e| {
@@ -162,10 +167,7 @@ pub(crate) async fn upload_signing_keys_route(
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match check_for_new_keys(
@@ -396,7 +398,6 @@ pub(crate) async fn get_key_changes_route(
);
let mut rooms_joined = services
.rooms
.state_cache
.rooms_joined(sender_user)
.boxed();
@@ -599,20 +600,19 @@ fn add_unsigned_device_display_name(
include_display_names: bool,
) -> Result {
if let Some(display_name) = metadata.display_name {
let mut object = keys.deserialize_as::<serde_json::Map<String, serde_json::Value>>()?;
let mut object = keys.deserialize_as_unchecked::<CanonicalJsonObject>()?;
let unsigned = object
.entry("unsigned")
.or_insert_with(|| json!({}));
if let serde_json::Value::Object(unsigned_object) = unsigned {
if include_display_names {
unsigned_object.insert("device_display_name".to_owned(), display_name.into());
if let CanonicalJsonValue::Object(unsigned) = object
.entry("unsigned".into())
.or_insert_with(|| CanonicalJsonObject::default().into())
{
let display_name = if include_display_names {
CanonicalJsonValue::String(display_name)
} else {
unsigned_object.insert(
"device_display_name".to_owned(),
Some(metadata.device_id.as_str().to_owned()).into(),
);
}
CanonicalJsonValue::String(metadata.device_id.into())
};
unsigned.insert("device_display_name".into(), display_name);
}
*keys = Raw::from_json(serde_json::value::to_raw_value(&object)?);
+7 -35
View File
@@ -1,9 +1,7 @@
use axum::extract::State;
use ruma::{
api::client::membership::ban_user,
events::room::member::{MembershipState, RoomMemberEventContent},
};
use tuwunel_core::{Err, Result, matrix::pdu::PduBuilder};
use futures::FutureExt;
use ruma::api::client::membership::ban_user;
use tuwunel_core::{Err, Result};
use crate::Ruma;
@@ -20,38 +18,12 @@ pub(crate) async fn ban_user_route(
return Err!(Request(Forbidden("You cannot ban yourself.")));
}
let state_lock = services
.rooms
.state
.mutex
.lock(&body.room_id)
.await;
let current_member_content = services
.rooms
.state_accessor
.get_member(&body.room_id, &body.user_id)
.await
.unwrap_or_else(|_| RoomMemberEventContent::new(MembershipState::Ban));
let state_lock = services.state.mutex.lock(&body.room_id).await;
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Ban,
reason: body.reason.clone(),
displayname: None, // display name may be offensive
avatar_url: None, // avatar may be offensive
is_direct: None,
join_authorized_via_users_server: None,
third_party_invite: None,
..current_member_content
}),
sender_user,
&body.room_id,
&state_lock,
)
.membership
.ban(&body.room_id, &body.user_id, body.reason.as_ref(), sender_user, &state_lock)
.boxed()
.await?;
drop(state_lock);
+4 -18
View File
@@ -21,18 +21,9 @@ pub(crate) async fn forget_room_route(
let user_id = body.sender_user();
let room_id = &body.room_id;
let joined = services
.rooms
.state_cache
.is_joined(user_id, room_id);
let knocked = services
.rooms
.state_cache
.is_knocked(user_id, room_id);
let invited = services
.rooms
.state_cache
.is_invited(user_id, room_id);
let joined = services.state_cache.is_joined(user_id, room_id);
let knocked = services.state_cache.is_knocked(user_id, room_id);
let invited = services.state_cache.is_invited(user_id, room_id);
pin_mut!(joined, knocked, invited);
if joined.or(knocked).or(invited).await {
@@ -40,7 +31,6 @@ pub(crate) async fn forget_room_route(
}
let membership = services
.rooms
.state_accessor
.get_member(room_id, user_id)
.await;
@@ -55,15 +45,11 @@ pub(crate) async fn forget_room_route(
if non_membership
|| services
.rooms
.state_cache
.is_left(user_id, room_id)
.await
{
services
.rooms
.state_cache
.forget(room_id, user_id);
services.state_cache.forget(room_id, user_id);
}
Ok(forget_room::v3::Response::new())
+50 -222
View File
@@ -1,19 +1,11 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::{FutureExt, join};
use ruma::{
OwnedServerName, RoomId, UserId,
api::{client::membership::invite_user, federation::membership::create_invite},
events::room::member::{MembershipState, RoomMemberEventContent},
};
use tuwunel_core::{
Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
};
use tuwunel_service::Services;
use ruma::{api::client::membership::invite_user, events::room::member::MembershipState};
use tuwunel_core::{Err, Result};
use super::banned_room_check;
use crate::Ruma;
use crate::{Ruma, client::utils::invite_check};
/// # `POST /_matrix/client/r0/rooms/{roomId}/invite`
///
@@ -26,220 +18,56 @@ pub(crate) async fn invite_user_route(
) -> Result<invite_user::v3::Response> {
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
debug_error!(
"User {sender_user} is not an admin and attempted to send an invite to room {}",
&body.room_id
);
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
let room_id = &body.room_id;
banned_room_check(
&services,
sender_user,
Some(&body.room_id),
body.room_id.server_name(),
client,
)
.await?;
invite_check(&services, sender_user, room_id).await?;
match &body.recipient {
| invite_user::v3::InvitationRecipient::UserId { user_id } => {
let sender_ignored_recipient = services
.users
.user_is_ignored(sender_user, user_id);
let recipient_ignored_by_sender = services
.users
.user_is_ignored(user_id, sender_user);
let (sender_ignored_recipient, recipient_ignored_by_sender) =
join!(sender_ignored_recipient, recipient_ignored_by_sender);
if sender_ignored_recipient {
return Ok(invite_user::v3::Response {});
}
if let Ok(target_user_membership) = services
.rooms
.state_accessor
.get_member(&body.room_id, user_id)
.await
{
if target_user_membership.membership == MembershipState::Ban {
return Err!(Request(Forbidden("User is banned from this room.")));
}
}
if recipient_ignored_by_sender {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
return Ok(invite_user::v3::Response {});
}
invite_helper(
&services,
sender_user,
user_id,
&body.room_id,
body.reason.clone(),
false,
)
.boxed()
.await?;
Ok(invite_user::v3::Response {})
},
| _ => {
Err!(Request(NotFound("User not found.")))
},
}
}
pub(crate) async fn invite_helper(
services: &Services,
sender_user: &UserId,
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
is_direct: bool,
) -> Result {
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
info!(
"User {sender_user} is not an admin and attempted to send an invite to room \
{room_id}"
);
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
if !services.globals.user_is_local(user_id) {
let (pdu, pdu_json, invite_room_state) = {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent {
avatar_url: services.users.avatar_url(user_id).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
};
let (pdu, pdu_json) = services
.rooms
.timeline
.create_hash_and_sign_event(
PduBuilder::state(user_id.to_string(), &content),
sender_user,
room_id,
&state_lock,
)
.await?;
let invite_room_state = services.rooms.state.summary_stripped(&pdu).await;
drop(state_lock);
(pdu, pdu_json, invite_room_state)
};
let room_version_id = services
.rooms
.state
.get_room_version(room_id)
.await?;
let response = services
.sending
.send_federation_request(user_id.server_name(), create_invite::v2::Request {
room_id: room_id.to_owned(),
event_id: (*pdu.event_id).to_owned(),
room_version: room_version_id.clone(),
event: services
.sending
.convert_to_outgoing_federation_event(pdu_json.clone())
.await,
invite_room_state,
via: services
.rooms
.state_cache
.servers_route_via(room_id)
.await
.ok(),
})
.await?;
// We do not add the event_id field to the pdu here because of signature and
// hashes checks
let (event_id, value) = gen_event_id_canonical_json(&response.event, &room_version_id)
.map_err(|e| {
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
})?;
if pdu.event_id != event_id {
return Err!(Request(BadJson(warn!(
%pdu.event_id, %event_id,
"Server {} sent event with wrong event ID",
user_id.server_name()
))));
}
let origin: OwnedServerName = serde_json::from_value(serde_json::to_value(
value
.get("origin")
.ok_or_else(|| err!(Request(BadJson("Event missing origin field."))))?,
)?)
.map_err(|e| {
err!(Request(BadJson(warn!("Origin field in event is not a valid server name: {e}"))))
})?;
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(&origin, room_id, &event_id, value, true)
.boxed()
.await?
.ok_or_else(|| {
err!(Request(InvalidParam("Could not accept incoming PDU as timeline event.")))
})?;
return services
.sending
.send_pdu_room(room_id, &pdu_id)
.await;
}
if !services
.rooms
.state_cache
.is_joined(sender_user, room_id)
.await
{
return Err!(Request(Forbidden(
"You must be joined in the room you are trying to invite from."
)));
}
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent {
displayname: services.users.displayname(user_id).await.ok(),
avatar_url: services.users.avatar_url(user_id).await.ok(),
blurhash: services.users.blurhash(user_id).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
};
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(user_id.to_string(), &content),
sender_user,
room_id,
&state_lock,
)
banned_room_check(&services, sender_user, Some(room_id), room_id.server_name(), client)
.await?;
drop(state_lock);
let invite_user::v3::InvitationRecipient::UserId { user_id } = &body.recipient else {
return Err!(Request(ThreepidDenied("Third party identifiers are not implemented")));
};
Ok(())
let sender_ignored_recipient = services
.users
.user_is_ignored(sender_user, user_id);
let recipient_ignored_by_sender = services
.users
.user_is_ignored(user_id, sender_user);
let (sender_ignored_recipient, recipient_ignored_by_sender) =
join!(sender_ignored_recipient, recipient_ignored_by_sender);
if sender_ignored_recipient {
return Ok(invite_user::v3::Response {});
}
// TODO: this should be in the service, but moving it from here would
// trigger the recipient_ignored_by_sender check before the banned check,
// revealing the ignore state to the sending user if the recipient is banned
if let Ok(target_user_membership) = services
.state_accessor
.get_member(room_id, user_id)
.await
{
if target_user_membership.membership == MembershipState::Ban {
return Err!(Request(Forbidden("User is banned from this room.")));
}
}
if recipient_ignored_by_sender {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
return Ok(invite_user::v3::Response {});
}
services
.membership
.invite(sender_user, user_id, room_id, body.reason.as_ref(), false)
.boxed()
.await?;
Ok(invite_user::v3::Response {})
}
File diff suppressed because it is too large Load Diff
+11 -45
View File
@@ -1,9 +1,7 @@
use axum::extract::State;
use ruma::{
api::client::membership::kick_user,
events::room::member::{MembershipState, RoomMemberEventContent},
};
use tuwunel_core::{Err, Result, matrix::pdu::PduBuilder};
use futures::FutureExt;
use ruma::api::client::membership::kick_user;
use tuwunel_core::{Err, Result};
use crate::Ruma;
@@ -14,50 +12,18 @@ pub(crate) async fn kick_user_route(
State(services): State<crate::State>,
body: Ruma<kick_user::v3::Request>,
) -> Result<kick_user::v3::Response> {
let state_lock = services
.rooms
.state
.mutex
.lock(&body.room_id)
.await;
let sender_user = body.sender_user();
let Ok(event) = services
.rooms
.state_accessor
.get_member(&body.room_id, &body.user_id)
.await
else {
// copy synapse's behaviour of returning 200 without any change to the state
// instead of erroring on left users
return Ok(kick_user::v3::Response::new());
};
if !matches!(
event.membership,
MembershipState::Invite | MembershipState::Knock | MembershipState::Join,
) {
return Err!(Request(Forbidden(
"Cannot kick a user who is not apart of the room (current membership: {})",
event.membership
)));
if sender_user == body.user_id {
return Err!(Request(Forbidden("You cannot kick yourself.")));
}
let state_lock = services.state.mutex.lock(&body.room_id).await;
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason: body.reason.clone(),
is_direct: None,
join_authorized_via_users_server: None,
third_party_invite: None,
..event
}),
body.sender_user(),
&body.room_id,
&state_lock,
)
.membership
.kick(&body.room_id, &body.user_id, body.reason.as_ref(), sender_user, &state_lock)
.boxed()
.await?;
drop(state_lock);
+45 -136
View File
@@ -4,11 +4,14 @@
use axum_client_ip::InsecureClientIp;
use futures::{FutureExt, StreamExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName, RoomId,
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedServerName, RoomId,
RoomVersionId, UserId,
api::{
client::knock::knock_room,
federation::{self},
federation::{
membership::RawStrippedState,
{self},
},
},
canonical_json::to_canonical_value,
events::{
@@ -17,14 +20,13 @@
},
};
use tuwunel_core::{
Err, Result, debug, debug_info, debug_warn, err, info,
Err, Result, debug, debug_info, debug_warn, err, extract_variant, info,
matrix::{
event::{Event, gen_event_id},
pdu::{PduBuilder, PduEvent},
},
result::FlatOk,
trace,
utils::{self, shuffle, stream::IterStream},
utils::{self},
warn,
};
use tuwunel_service::{
@@ -36,7 +38,7 @@
};
use super::banned_room_check;
use crate::Ruma;
use crate::{Ruma, client::membership::get_join_params};
/// # `POST /_matrix/client/*/knock/{roomIdOrAlias}`
///
@@ -50,102 +52,13 @@ pub(crate) async fn knock_room_route(
let sender_user = body.sender_user();
let body = &body.body;
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias.clone()) {
| Ok(room_id) => {
banned_room_check(
&services,
sender_user,
Some(&room_id),
room_id.server_name(),
client,
)
.await?;
let (room_id, servers) =
get_join_params(&services, sender_user, &body.room_id_or_alias, &body.via).await?;
let mut servers = body.via.clone();
servers.extend(
services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
banned_room_check(&services, sender_user, Some(&room_id), room_id.server_name(), client)
.await?;
servers.extend(
services
.rooms
.state_cache
.invite_state(sender_user, &room_id)
.await
.unwrap_or_default()
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
if let Some(server) = room_id.server_name() {
servers.push(server.to_owned());
}
servers.sort_unstable();
servers.dedup();
shuffle(&mut servers);
(servers, room_id)
},
| Err(room_alias) => {
let (room_id, mut servers) = services
.rooms
.alias
.resolve_alias(&room_alias, Some(body.via.clone()))
.await?;
banned_room_check(
&services,
sender_user,
Some(&room_id),
Some(room_alias.server_name()),
client,
)
.await?;
let addl_via_servers = services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned);
let addl_state_servers = services
.rooms
.state_cache
.invite_state(sender_user, &room_id)
.await
.unwrap_or_default();
let mut addl_servers: Vec<_> = addl_state_servers
.iter()
.map(|event| event.get_field("sender"))
.filter_map(FlatOk::flat_ok)
.map(|user: &UserId| user.server_name().to_owned())
.stream()
.chain(addl_via_servers)
.collect()
.await;
addl_servers.sort_unstable();
addl_servers.dedup();
shuffle(&mut addl_servers);
servers.append(&mut addl_servers);
(servers, room_id)
},
};
knock_room_by_id_helper(&services, sender_user, &room_id, body.reason.clone(), &servers)
.boxed()
.await
knock_room_by_id_helper(&services, sender_user, &room_id, body.reason.clone(), &servers).await
}
async fn knock_room_by_id_helper(
@@ -155,10 +68,9 @@ async fn knock_room_by_id_helper(
reason: Option<String>,
servers: &[OwnedServerName],
) -> Result<knock_room::v3::Response> {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let state_lock = services.state.mutex.lock(room_id).await;
if services
.rooms
.state_cache
.is_invited(sender_user, room_id)
.await
@@ -170,7 +82,6 @@ async fn knock_room_by_id_helper(
}
if services
.rooms
.state_cache
.is_joined(sender_user, room_id)
.await
@@ -180,7 +91,6 @@ async fn knock_room_by_id_helper(
}
if services
.rooms
.state_cache
.is_knocked(sender_user, room_id)
.await
@@ -190,7 +100,6 @@ async fn knock_room_by_id_helper(
}
if let Ok(membership) = services
.rooms
.state_accessor
.get_member(room_id, sender_user)
.await
@@ -202,7 +111,6 @@ async fn knock_room_by_id_helper(
}
let server_in_room = services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await;
@@ -234,11 +142,7 @@ async fn knock_room_helper_local(
) -> Result {
debug_info!("We can knock locally");
let room_version_id = services
.rooms
.state
.get_room_version(room_id)
.await?;
let room_version_id = services.state.get_room_version(room_id).await?;
if matches!(
room_version_id,
@@ -262,7 +166,6 @@ async fn knock_room_helper_local(
// Try normal knock first
let Err(error) = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(sender_user.to_string(), &content),
@@ -346,12 +249,12 @@ async fn knock_room_helper_local(
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let send_knock_request = federation::knock::send_knock::v1::Request {
let send_knock_request = federation::membership::create_knock_event::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
.sending
.convert_to_outgoing_federation_event(knock_event.clone())
.federation
.format_pdu_into(knock_event.clone(), Some(&room_version_id))
.await,
};
@@ -363,7 +266,6 @@ async fn knock_room_helper_local(
info!("send_knock finished");
services
.rooms
.short
.get_or_create_shortroomid(room_id)
.await;
@@ -375,7 +277,6 @@ async fn knock_room_helper_local(
info!("Updating membership locally to knock state with provided stripped state events");
services
.rooms
.state_cache
.update_membership(
room_id,
@@ -384,7 +285,13 @@ async fn knock_room_helper_local(
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
Some(
send_knock_response
.knock_room_state
.into_iter()
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
.collect(),
),
None,
false,
)
@@ -392,7 +299,6 @@ async fn knock_room_helper_local(
info!("Appending room knock event locally");
services
.rooms
.timeline
.append_pdu(
&parsed_knock_pdu,
@@ -477,12 +383,12 @@ async fn knock_room_helper_remote(
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let send_knock_request = federation::knock::send_knock::v1::Request {
let send_knock_request = federation::membership::create_knock_event::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
.sending
.convert_to_outgoing_federation_event(knock_event.clone())
.federation
.format_pdu_into(knock_event.clone(), Some(&room_version_id))
.await,
};
@@ -494,7 +400,6 @@ async fn knock_room_helper_remote(
info!("send_knock finished");
services
.rooms
.short
.get_or_create_shortroomid(room_id)
.await;
@@ -507,7 +412,14 @@ async fn knock_room_helper_remote(
let state = send_knock_response
.knock_room_state
.iter()
.map(|event| serde_json::from_str::<CanonicalJsonObject>(event.clone().into_json().get()))
.map(|event| {
serde_json::from_str::<CanonicalJsonObject>(
extract_variant!(event.clone(), RawStrippedState::Stripped)
.expect("Raw<AnyStrippedStateEvent>")
.json()
.get(),
)
})
.filter_map(Result::ok);
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
@@ -534,13 +446,11 @@ async fn knock_room_helper_remote(
let event_id = gen_event_id(&event, &room_version_id)?;
let shortstatekey = services
.rooms
.short
.get_or_create_shortstatekey(&event_type, &state_key)
.await;
services
.rooms
.timeline
.add_pdu_outlier(&event_id, &event);
@@ -549,7 +459,6 @@ async fn knock_room_helper_remote(
info!("Compressing state from send_knock");
let compressed: CompressedState = services
.rooms
.state_compressor
.compress_state_events(
state_map
@@ -565,27 +474,23 @@ async fn knock_room_helper_remote(
added,
removed,
} = services
.rooms
.state_compressor
.save_state(room_id, Arc::new(compressed))
.await?;
debug!("Forcing state for new room");
services
.rooms
.state
.force_state(room_id, statehash_before_knock, added, removed, &state_lock)
.await?;
let statehash_after_knock = services
.rooms
.state
.append_to_state(&parsed_knock_pdu)
.await?;
info!("Updating membership locally to knock state with provided stripped state events");
services
.rooms
.state_cache
.update_membership(
room_id,
@@ -594,7 +499,13 @@ async fn knock_room_helper_remote(
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
Some(
send_knock_response
.knock_room_state
.into_iter()
.filter_map(|s| extract_variant!(s, RawStrippedState::Stripped))
.collect(),
),
None,
false,
)
@@ -602,7 +513,6 @@ async fn knock_room_helper_remote(
info!("Appending room knock event locally");
services
.rooms
.timeline
.append_pdu(
&parsed_knock_pdu,
@@ -616,7 +526,6 @@ async fn knock_room_helper_remote(
// We set the room state after inserting the pdu, so that we never have a moment
// in time where events in the current room state do not exist
services
.rooms
.state
.set_room_state(room_id, statehash_after_knock, &state_lock);
@@ -628,7 +537,7 @@ async fn make_knock_request(
sender_user: &UserId,
room_id: &RoomId,
servers: &[OwnedServerName],
) -> Result<(federation::knock::create_knock_event_template::v1::Response, OwnedServerName)> {
) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> {
let mut make_knock_response_and_server =
Err!(BadServerResponse("No server available to assist in knocking."));
@@ -645,7 +554,7 @@ async fn make_knock_request(
.sending
.send_federation_request(
remote_server,
federation::knock::create_knock_event_template::v1::Request {
federation::membership::prepare_knock_event::v1::Request {
room_id: room_id.to_owned(),
user_id: sender_user.to_owned(),
ver: services
+17 -376
View File
@@ -1,25 +1,7 @@
use std::collections::HashSet;
use axum::extract::State;
use futures::{FutureExt, StreamExt, TryFutureExt, pin_mut};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, RoomVersionId, UserId,
api::{
client::membership::leave_room,
federation::{self},
},
events::{
StateEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
};
use tuwunel_core::{
Err, Result, debug_info, debug_warn, err,
matrix::{event::gen_event_id, pdu::PduBuilder},
utils::{self, FutureBoolExt, future::ReadyEqExt},
warn,
};
use tuwunel_service::Services;
use futures::FutureExt;
use ruma::api::client::membership::leave_room;
use tuwunel_core::Result;
use crate::Ruma;
@@ -32,362 +14,21 @@ pub(crate) async fn leave_room_route(
State(services): State<crate::State>,
body: Ruma<leave_room::v3::Request>,
) -> Result<leave_room::v3::Response> {
leave_room(&services, body.sender_user(), &body.room_id, body.reason.clone())
let state_lock = services.state.mutex.lock(&body.room_id).await;
services
.membership
.leave(body.sender_user(), &body.room_id, body.reason.clone(), false, &state_lock)
.boxed()
.await
.map(|()| leave_room::v3::Response::new())
}
// Make a user leave all their joined rooms, rescinds knocks, forgets all rooms,
// and ignores errors
pub async fn leave_all_rooms(services: &Services, user_id: &UserId) {
let rooms_joined = services
.rooms
.state_cache
.rooms_joined(user_id)
.map(ToOwned::to_owned);
let rooms_invited = services
.rooms
.state_cache
.rooms_invited(user_id)
.map(|(r, _)| r);
let rooms_knocked = services
.rooms
.state_cache
.rooms_knocked(user_id)
.map(|(r, _)| r);
let all_rooms: Vec<_> = rooms_joined
.chain(rooms_invited)
.chain(rooms_knocked)
.collect()
.await;
for room_id in all_rooms {
// ignore errors
if let Err(e) = leave_room(services, user_id, &room_id, None)
.boxed()
.await
{
warn!(%user_id, "Failed to leave {room_id} remotely: {e}");
}
services
.rooms
.state_cache
.forget(&room_id, user_id);
}
}
pub async fn leave_room(
services: &Services,
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
) -> Result {
let default_member_content = RoomMemberEventContent {
membership: MembershipState::Leave,
reason: reason.clone(),
join_authorized_via_users_server: None,
is_direct: None,
avatar_url: None,
displayname: None,
third_party_invite: None,
blurhash: None,
};
let is_banned = services.rooms.metadata.is_banned(room_id);
let is_disabled = services.rooms.metadata.is_disabled(room_id);
pin_mut!(is_banned, is_disabled);
if is_banned.or(is_disabled).await {
// the room is banned/disabled, the room must be rejected locally since we
// cant/dont want to federate with this server
services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
None,
None,
true,
)
.await?;
return Ok(());
}
let dont_have_room = services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.eq(&false);
let not_knocked = services
.rooms
.state_cache
.is_knocked(user_id, room_id)
.eq(&false);
// Ask a remote server if we don't have this room and are not knocking on it
if dont_have_room.and(not_knocked).await {
if let Err(e) = remote_leave_room(services, user_id, room_id)
.boxed()
.await
{
warn!(%user_id, "Failed to leave room {room_id} remotely: {e}");
// Don't tell the client about this error
}
let last_state = services
.rooms
.state_cache
.invite_state(user_id, room_id)
.or_else(|_| {
services
.rooms
.state_cache
.knock_state(user_id, room_id)
})
.or_else(|_| {
services
.rooms
.state_cache
.left_state(user_id, room_id)
})
.await
.ok();
// We always drop the invite, we can't rely on other servers
services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
last_state,
None,
true,
)
.await?;
} else {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let Ok(event) = services
.rooms
.state_accessor
.room_state_get_content::<RoomMemberEventContent>(
room_id,
&StateEventType::RoomMember,
user_id.as_str(),
)
.await
else {
debug_warn!(
"Trying to leave a room you are not a member of, marking room as left locally."
);
return services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
None,
None,
true,
)
.await;
};
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason,
join_authorized_via_users_server: None,
is_direct: None,
..event
}),
user_id,
room_id,
&state_lock,
)
.await?;
}
Ok(())
}
async fn remote_leave_room(services: &Services, user_id: &UserId, room_id: &RoomId) -> Result {
let mut make_leave_response_and_server =
Err!(BadServerResponse("No remote server available to assist in leaving {room_id}."));
let mut servers: HashSet<OwnedServerName> = services
.rooms
.state_cache
.servers_invite_via(room_id)
.map(ToOwned::to_owned)
.collect()
.await;
match services
.rooms
.state_cache
.invite_state(user_id, room_id)
.await
{
| Ok(invite_state) => {
servers.extend(
invite_state
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
},
| _ => {
match services
.rooms
.state_cache
.knock_state(user_id, room_id)
.await
{
| Ok(knock_state) => {
servers.extend(
knock_state
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.filter_map(|sender| {
if !services.globals.user_is_local(sender) {
Some(sender.server_name().to_owned())
} else {
None
}
}),
);
},
| _ => {},
}
},
}
if let Some(room_id_server_name) = room_id.server_name() {
servers.insert(room_id_server_name.to_owned());
}
debug_info!("servers in remote_leave_room: {servers:?}");
for remote_server in servers {
let make_leave_response = services
.sending
.send_federation_request(
&remote_server,
federation::membership::prepare_leave_event::v1::Request {
room_id: room_id.to_owned(),
user_id: user_id.to_owned(),
},
)
.await;
make_leave_response_and_server = make_leave_response.map(|r| (r, remote_server));
if make_leave_response_and_server.is_ok() {
break;
}
}
let (make_leave_response, remote_server) = make_leave_response_and_server?;
let Some(room_version_id) = make_leave_response.room_version else {
return Err!(BadServerResponse(warn!(
"No room version was returned by {remote_server} for {room_id}, room version is \
likely not supported by tuwunel"
)));
};
if !services
.server
.supported_room_version(&room_version_id)
{
return Err!(BadServerResponse(warn!(
"Remote room version {room_version_id} for {room_id} is not supported by conduwuit",
)));
}
let mut leave_event_stub = serde_json::from_str::<CanonicalJsonObject>(
make_leave_response.event.get(),
)
.map_err(|e| {
err!(BadServerResponse(warn!(
"Invalid make_leave event json received from {remote_server} for {room_id}: {e:?}"
)))
})?;
// TODO: Is origin needed?
leave_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
leave_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
// room v3 and above removed the "event_id" field from remote PDU format
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
leave_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut leave_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&leave_event_stub, &room_version_id)?;
// Add event_id back
leave_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let leave_event = leave_event_stub;
services
.sending
.send_federation_request(
&remote_server,
federation::membership::create_leave_event::v2::Request {
room_id: room_id.to_owned(),
event_id,
pdu: services
.sending
.convert_to_outgoing_federation_event(leave_event.clone())
.await,
},
)
.await?;
Ok(())
if services.config.delete_rooms_after_leave {
services
.delete
.delete_if_empty_local(&body.room_id, state_lock)
.boxed()
.await;
}
Ok(leave_room::v3::Response {})
}
+43 -29
View File
@@ -1,5 +1,5 @@
use axum::extract::State;
use futures::{FutureExt, StreamExt, future::join};
use futures::{FutureExt, StreamExt, pin_mut};
use ruma::{
api::client::membership::{
get_member_events::{self, v3::MembershipEventFilter},
@@ -7,15 +7,18 @@
},
events::{
StateEventType,
room::member::{MembershipState, RoomMemberEventContent},
room::{
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
};
use tuwunel_core::{
Err, Result, at,
matrix::Event,
utils::{
future::TryExtExt,
stream::{BroadbandExt, ReadyExt},
future::{BoolExt, TryExtExt},
stream::ReadyExt,
},
};
@@ -31,22 +34,20 @@ pub(crate) async fn get_member_events_route(
State(services): State<crate::State>,
body: Ruma<get_member_events::v3::Request>,
) -> Result<get_member_events::v3::Response> {
let sender_user = body.sender_user();
let membership = body.membership.as_ref();
let not_membership = body.not_membership.as_ref();
if !services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, &body.room_id)
.user_can_see_state_events(body.sender_user(), &body.room_id)
.await
{
return Err!(Request(Forbidden("You don't have permission to view this room.")));
return Err!(Request(Forbidden(
"You aren't a member of the room and weren't previously a member of the room."
)));
}
let membership = body.membership.as_ref();
let not_membership = body.not_membership.as_ref();
Ok(get_member_events::v3::Response {
chunk: services
.rooms
.state_accessor
.room_state_full(&body.room_id)
.ready_filter_map(Result::ok)
@@ -70,31 +71,44 @@ pub(crate) async fn joined_members_route(
State(services): State<crate::State>,
body: Ruma<joined_members::v3::Request>,
) -> Result<joined_members::v3::Response> {
if !services
.rooms
let is_joined = services
.state_cache
.is_joined(body.sender_user(), &body.room_id);
let is_world_readable = services
.state_accessor
.user_can_see_state_events(body.sender_user(), &body.room_id)
.await
{
return Err!(Request(Forbidden("You don't have permission to view this room.")));
.room_state_get_content(&body.room_id, &StateEventType::RoomHistoryVisibility, "")
.map_ok_or(false, |c: RoomHistoryVisibilityEventContent| {
c.history_visibility == HistoryVisibility::WorldReadable
});
pin_mut!(is_joined, is_world_readable);
if !is_joined.or(is_world_readable).await {
return Err!(Request(Forbidden("You aren't a member of the room.")));
}
Ok(joined_members::v3::Response {
joined: services
.rooms
.state_cache
.room_members(&body.room_id)
.map(ToOwned::to_owned)
.broad_then(async |user_id| {
let (display_name, avatar_url) = join(
services.users.displayname(&user_id).ok(),
services.users.avatar_url(&user_id).ok(),
)
.await;
.state_accessor
.room_state_full(&body.room_id)
.ready_filter_map(Result::ok)
.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
.map(at!(1))
.ready_filter_map(|pdu| {
membership_filter(pdu, Some(&MembershipEventFilter::Join), None)
})
.ready_filter_map(|pdu| {
let content = pdu.get_content::<RoomMemberEventContent>().ok()?;
let sender = pdu.sender().to_owned();
let member = RoomMember {
display_name: content.displayname,
avatar_url: content.avatar_url,
};
(user_id, RoomMember { display_name, avatar_url })
Some((sender, member))
})
.collect()
.boxed()
.await,
})
}
+144 -82
View File
@@ -8,18 +8,21 @@
mod members;
mod unban;
use std::net::IpAddr;
use std::{cmp::Ordering, net::IpAddr};
use axum::extract::State;
use futures::{FutureExt, StreamExt};
use ruma::{OwnedRoomId, RoomId, ServerName, UserId, api::client::membership::joined_rooms};
use tuwunel_core::{Err, Result, warn};
use ruma::{
OwnedRoomId, OwnedServerName, RoomId, RoomOrAliasId, ServerName, UserId,
api::client::membership::joined_rooms,
};
use tuwunel_core::{Err, Result, result::LogErr, utils::shuffle, warn};
use tuwunel_service::Services;
pub(crate) use self::{
ban::ban_user_route,
forget::forget_room_route,
invite::{invite_helper, invite_user_route},
invite::invite_user_route,
join::{join_room_by_id_or_alias_route, join_room_by_id_route},
kick::kick_user_route,
knock::knock_room_route,
@@ -27,11 +30,7 @@
members::{get_member_events_route, joined_members_route},
unban::unban_user_route,
};
pub use self::{
join::join_room_by_id_helper,
leave::{leave_all_rooms, leave_room},
};
use crate::{Ruma, client::full_user_deactivate};
use crate::Ruma;
/// # `POST /_matrix/client/r0/joined_rooms`
///
@@ -42,7 +41,6 @@ pub(crate) async fn joined_rooms_route(
) -> Result<joined_rooms::v3::Response> {
Ok(joined_rooms::v3::Response {
joined_rooms: services
.rooms
.state_cache
.rooms_joined(body.sender_user())
.map(ToOwned::to_owned)
@@ -68,53 +66,28 @@ pub(crate) async fn banned_room_check(
return Ok(());
}
// TODO: weird condition
if let Some(room_id) = room_id {
if services.rooms.metadata.is_banned(room_id).await
|| services
.config
.forbidden_remote_server_names
.is_match(
room_id
.server_name()
.expect("legacy room mxid")
.host(),
) {
if services.metadata.is_banned(room_id).await
|| (room_id.server_name().is_some()
&& services
.config
.forbidden_remote_server_names
.is_match(
room_id
.server_name()
.expect("legacy room mxid")
.host(),
)) {
warn!(
"User {user_id} who is not an admin attempted to send an invite for or \
attempted to join a banned room or banned room server name: {room_id}"
);
if services
.server
.config
.auto_deactivate_banned_room_attempts
{
warn!(
"Automatically deactivating user {user_id} due to attempted banned room join"
);
if services.server.config.admin_room_notices {
services
.admin
.send_text(&format!(
"Automatically deactivating user {user_id} due to attempted banned \
room join from IP {client_ip}"
))
.await;
}
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(user_id)
.map(Into::into)
.collect()
.await;
full_user_deactivate(services, user_id, &all_joined_rooms)
.boxed()
.await?;
}
maybe_deactivate(services, user_id, client_ip)
.await
.log_err()
.ok();
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
}
@@ -129,37 +102,10 @@ pub(crate) async fn banned_room_check(
name {server_name} that is globally forbidden. Rejecting.",
);
if services
.server
.config
.auto_deactivate_banned_room_attempts
{
warn!(
"Automatically deactivating user {user_id} due to attempted banned room join"
);
if services.server.config.admin_room_notices {
services
.admin
.send_text(&format!(
"Automatically deactivating user {user_id} due to attempted banned \
room join from IP {client_ip}"
))
.await;
}
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(user_id)
.map(Into::into)
.collect()
.await;
full_user_deactivate(services, user_id, &all_joined_rooms)
.boxed()
.await?;
}
maybe_deactivate(services, user_id, client_ip)
.await
.log_err()
.ok();
return Err!(Request(Forbidden("This remote server is banned on this homeserver.")));
}
@@ -167,3 +113,119 @@ pub(crate) async fn banned_room_check(
Ok(())
}
async fn maybe_deactivate(services: &Services, user_id: &UserId, client_ip: IpAddr) -> Result {
if services
.server
.config
.auto_deactivate_banned_room_attempts
{
warn!("Automatically deactivating user {user_id} due to attempted banned room join");
if services.server.config.admin_room_notices {
services
.admin
.send_text(&format!(
"Automatically deactivating user {user_id} due to attempted banned room \
join from IP {client_ip}"
))
.await;
}
services
.deactivate
.full_deactivate(user_id)
.boxed()
.await?;
}
Ok(())
}
// TODO: should this be in services? banned check would have to resolve again if
// room_id is not available at callsite
async fn get_join_params(
services: &Services,
user_id: &UserId,
room_id_or_alias: &RoomOrAliasId,
via: &[OwnedServerName],
) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
// servers tried first, additional_servers shuffled then tried after
let (room_id, mut servers, mut additional_servers) =
match OwnedRoomId::try_from(room_id_or_alias.to_owned()) {
// if room id, shuffle via + room_id server_name ...
| Ok(room_id) => {
let mut additional_servers = via.to_vec();
if let Some(server) = room_id.server_name() {
additional_servers.push(server.to_owned());
}
(room_id, Vec::new(), additional_servers)
},
// ... if room alias, resolve and don't shuffle ...
| Err(room_alias) => {
let (room_id, servers) = services
.alias
.resolve_alias(&room_alias, Some(via.to_vec()))
.await?;
(room_id, servers, Vec::new())
},
};
// either way, add invited vias
additional_servers.extend(
services
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
// either way, add invite senders' servers
additional_servers.extend(
services
.state_cache
.invite_state(user_id, &room_id)
.await
.unwrap_or_default()
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
// shuffle additionals, append to base servers
additional_servers.sort_unstable();
additional_servers.dedup();
shuffle(&mut additional_servers);
servers.sort_unstable();
servers.dedup();
servers.append(&mut additional_servers);
// sort deprioritized servers last
servers.sort_by(|a, b| {
let a_matches = services
.server
.config
.deprioritize_joins_through_servers
.is_match(a.host());
let b_matches = services
.server
.config
.deprioritize_joins_through_servers
.is_match(b.host());
if a_matches && !b_matches {
Ordering::Greater
} else if !a_matches && b_matches {
Ordering::Less
} else {
Ordering::Equal
}
});
Ok((room_id, servers))
}
+10 -37
View File
@@ -1,9 +1,7 @@
use axum::extract::State;
use ruma::{
api::client::membership::unban_user,
events::room::member::{MembershipState, RoomMemberEventContent},
};
use tuwunel_core::{Err, Result, matrix::pdu::PduBuilder};
use futures::FutureExt;
use ruma::api::client::membership::unban_user;
use tuwunel_core::Result;
use crate::Ruma;
@@ -14,43 +12,18 @@ pub(crate) async fn unban_user_route(
State(services): State<crate::State>,
body: Ruma<unban_user::v3::Request>,
) -> Result<unban_user::v3::Response> {
let state_lock = services
.rooms
.state
.mutex
.lock(&body.room_id)
.await;
let current_member_content = services
.rooms
.state_accessor
.get_member(&body.room_id, &body.user_id)
.await
.unwrap_or_else(|_| RoomMemberEventContent::new(MembershipState::Leave));
if current_member_content.membership != MembershipState::Ban {
return Err!(Request(Forbidden(
"Cannot unban a user who is not banned (current membership: {})",
current_member_content.membership
)));
}
let state_lock = services.state.mutex.lock(&body.room_id).await;
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason: body.reason.clone(),
join_authorized_via_users_server: None,
third_party_invite: None,
is_direct: None,
..current_member_content
}),
body.sender_user(),
.membership
.unban(
&body.room_id,
&body.user_id,
body.reason.as_ref(),
body.sender_user(),
&state_lock,
)
.boxed()
.await?;
drop(state_lock);
+2 -8
View File
@@ -73,7 +73,7 @@ pub(crate) async fn get_message_events_route(
let room_id = &body.room_id;
let filter = &body.filter;
if !services.rooms.metadata.exists(room_id).await {
if !services.metadata.exists(room_id).await {
return Err!(Request(Forbidden("Room does not exist to this server")));
}
@@ -97,7 +97,6 @@ pub(crate) async fn get_message_events_route(
if matches!(body.dir, Direction::Backward) {
services
.rooms
.timeline
.backfill_if_required(room_id, from)
.await
@@ -107,14 +106,12 @@ pub(crate) async fn get_message_events_route(
let it = match body.dir {
| Direction::Forward => services
.rooms
.timeline
.pdus(Some(sender_user), room_id, Some(from))
.ignore_err()
.boxed(),
| Direction::Backward => services
.rooms
.timeline
.pdus_rev(Some(sender_user), room_id, Some(from))
.ignore_err()
@@ -192,7 +189,7 @@ pub(crate) async fn lazy_loading_witness<'a, I>(
.max()
.unwrap_or_else(PduCount::max);
let receipts = services.rooms.read_receipt.readreceipts_since(
let receipts = services.read_receipt.readreceipts_since(
lazy_loading_context.room_id,
oldest.into_unsigned(),
Some(newest.into_unsigned()),
@@ -213,7 +210,6 @@ pub(crate) async fn lazy_loading_witness<'a, I>(
.await;
services
.rooms
.lazy_loading
.witness_retain(witness, lazy_loading_context)
.await
@@ -225,7 +221,6 @@ async fn get_member_event(
user_id: &UserId,
) -> Option<Raw<AnyStateEvent>> {
services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())
.map_ok(Event::into_format)
@@ -293,7 +288,6 @@ pub(crate) async fn visibility_filter(
let (_, pdu) = &item;
services
.rooms
.state_accessor
.user_can_see_event(user_id, pdu.room_id(), pdu.event_id())
.await
+2 -3
View File
@@ -40,7 +40,8 @@
pub(super) mod voip;
pub(super) mod well_known;
pub use account::full_user_deactivate;
mod utils;
pub(super) use account::*;
pub(super) use account_data::*;
pub(super) use alias::*;
@@ -55,12 +56,10 @@
pub(super) use media::*;
pub(super) use media_legacy::*;
pub(super) use membership::*;
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room};
pub(super) use message::*;
pub(super) use openid::*;
pub(super) use presence::*;
pub(super) use profile::*;
pub use profile::{update_all_rooms, update_avatar_url, update_displayname};
pub(super) use push::*;
pub(super) use read_marker::*;
pub(super) use redact::*;
-1
View File
@@ -44,7 +44,6 @@ pub(crate) async fn get_presence_route(
let mut presence_event = None;
let has_shared_rooms = services
.rooms
.state_cache
.user_sees_user(body.sender_user(), &body.user_id)
.await;
+44 -156
View File
@@ -2,27 +2,20 @@
use axum::extract::State;
use futures::{
StreamExt, TryStreamExt,
future::{join, join3, join4},
StreamExt,
future::{join, join4},
};
use ruma::{
OwnedMxcUri, OwnedRoomId, UserId,
OwnedRoomId,
api::{
client::profile::{
get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
},
federation,
},
events::room::member::{MembershipState, RoomMemberEventContent},
presence::PresenceState,
};
use tuwunel_core::{
Err, Result,
matrix::pdu::PduBuilder,
utils::{IterStream, future::TryExtExt, stream::TryIgnore},
warn,
};
use tuwunel_service::Services;
use tuwunel_core::{Err, Result, utils::future::TryExtExt};
use crate::Ruma;
@@ -42,14 +35,15 @@ pub(crate) async fn set_displayname_route(
}
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(ToOwned::to_owned)
.collect()
.await;
update_displayname(&services, &body.user_id, body.displayname.clone(), &all_joined_rooms)
services
.users
.update_displayname(&body.user_id, body.displayname.clone(), &all_joined_rooms)
.await;
if services.config.allow_local_presence {
@@ -138,21 +132,21 @@ pub(crate) async fn set_avatar_url_route(
}
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(ToOwned::to_owned)
.collect()
.await;
update_avatar_url(
&services,
&body.user_id,
body.avatar_url.clone(),
body.blurhash.clone(),
&all_joined_rooms,
)
.await;
services
.users
.update_avatar_url(
&body.user_id,
body.avatar_url.clone(),
body.blurhash.clone(),
&all_joined_rooms,
)
.await;
if services.config.allow_local_presence {
// Presence update
@@ -279,13 +273,20 @@ pub(crate) async fn get_profile_route(
);
}
return Ok(get_profile::v3::Response {
displayname: response.displayname,
avatar_url: response.avatar_url,
blurhash: response.blurhash,
tz: response.tz,
custom_profile_fields: response.custom_profile_fields,
});
let canonical_fields = [
("avatar_url", response.avatar_url.map(Into::into)),
("blurhash", response.blurhash),
("displayname", response.displayname),
("tz", response.tz),
];
let response = canonical_fields
.into_iter()
.filter_map(|(key, val)| val.map(|val| (key, val)))
.map(|(key, val)| (key.to_owned(), val.into()))
.chain(response.custom_profile_fields.into_iter());
return Ok(response.collect::<get_profile::v3::Response>());
}
}
@@ -313,131 +314,18 @@ pub(crate) async fn get_profile_route(
)
.await;
Ok(get_profile::v3::Response {
avatar_url,
blurhash,
displayname,
tz,
custom_profile_fields,
})
}
pub async fn update_displayname(
services: &Services,
user_id: &UserId,
displayname: Option<String>,
all_joined_rooms: &[OwnedRoomId],
) {
let (current_avatar_url, current_blurhash, current_displayname) = join3(
services.users.avatar_url(user_id).ok(),
services.users.blurhash(user_id).ok(),
services.users.displayname(user_id).ok(),
)
.await;
if displayname == current_displayname {
return;
}
services
.users
.set_displayname(user_id, displayname.clone());
// Send a new join membership event into all joined rooms
let avatar_url = &current_avatar_url;
let blurhash = &current_blurhash;
let displayname = &displayname;
let all_joined_rooms: Vec<_> = all_joined_rooms
.iter()
.try_stream()
.and_then(async |room_id: &OwnedRoomId| {
let pdu = PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
displayname: displayname.clone(),
membership: MembershipState::Join,
avatar_url: avatar_url.clone(),
blurhash: blurhash.clone(),
join_authorized_via_users_server: None,
reason: None,
is_direct: None,
third_party_invite: None,
});
Ok((pdu, room_id))
})
.ignore_err()
.collect()
.await;
update_all_rooms(services, all_joined_rooms, user_id).await;
}
pub async fn update_avatar_url(
services: &Services,
user_id: &UserId,
avatar_url: Option<OwnedMxcUri>,
blurhash: Option<String>,
all_joined_rooms: &[OwnedRoomId],
) {
let (current_avatar_url, current_blurhash, current_displayname) = join3(
services.users.avatar_url(user_id).ok(),
services.users.blurhash(user_id).ok(),
services.users.displayname(user_id).ok(),
)
.await;
if current_avatar_url == avatar_url && current_blurhash == blurhash {
return;
}
services
.users
.set_avatar_url(user_id, avatar_url.clone());
services
.users
.set_blurhash(user_id, blurhash.clone());
// Send a new join membership event into all joined rooms
let avatar_url = &avatar_url;
let blurhash = &blurhash;
let displayname = &current_displayname;
let all_joined_rooms: Vec<_> = all_joined_rooms
.iter()
.try_stream()
.and_then(async |room_id: &OwnedRoomId| {
let pdu = PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
avatar_url: avatar_url.clone(),
blurhash: blurhash.clone(),
membership: MembershipState::Join,
displayname: displayname.clone(),
join_authorized_via_users_server: None,
reason: None,
is_direct: None,
third_party_invite: None,
});
Ok((pdu, room_id))
})
.ignore_err()
.collect()
.await;
update_all_rooms(services, all_joined_rooms, user_id).await;
}
pub async fn update_all_rooms(
services: &Services,
all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>,
user_id: &UserId,
) {
for (pdu_builder, room_id) in all_joined_rooms {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
if let Err(e) = services
.rooms
.timeline
.build_and_append_pdu(pdu_builder, user_id, room_id, &state_lock)
.await
{
warn!(%user_id, %room_id, "Failed to update/send new profile join membership update in room: {e}");
}
}
let canonical_fields = [
("avatar_url", avatar_url.map(Into::into)),
("blurhash", blurhash),
("displayname", displayname),
("tz", tz),
];
let response = canonical_fields
.into_iter()
.filter_map(|(key, val)| val.map(|val| (key, val)))
.map(|(key, val)| (key.to_owned(), val.into()))
.chain(custom_profile_fields.into_iter());
Ok(response.collect::<get_profile::v3::Response>())
}
+14 -24
View File
@@ -44,20 +44,18 @@ pub(crate) async fn set_read_marker_route(
if body.private_read_receipt.is_some() || body.read_receipt.is_some() {
services
.rooms
.user
.reset_notification_counts(sender_user, &body.room_id);
}
// ping presence
if services.config.allow_local_presence {
services
.presence
.ping_presence(sender_user, &ruma::presence::PresenceState::Online)
.await?;
}
if let Some(event) = &body.read_receipt {
if services.config.allow_local_presence {
services
.presence
.ping_presence(sender_user, &ruma::presence::PresenceState::Online)
.await?;
}
let receipt_content = BTreeMap::from_iter([(
event.to_owned(),
BTreeMap::from_iter([(
@@ -70,7 +68,6 @@ pub(crate) async fn set_read_marker_route(
)]);
services
.rooms
.read_receipt
.readreceipt_update(
sender_user,
@@ -85,7 +82,6 @@ pub(crate) async fn set_read_marker_route(
if let Some(event) = &body.private_read_receipt {
let count = services
.rooms
.timeline
.get_pdu_count(event)
.await
@@ -98,7 +94,6 @@ pub(crate) async fn set_read_marker_route(
};
services
.rooms
.read_receipt
.private_read_set(&body.room_id, sender_user, count);
}
@@ -120,19 +115,10 @@ pub(crate) async fn create_receipt_route(
create_receipt::v3::ReceiptType::Read | create_receipt::v3::ReceiptType::ReadPrivate
) {
services
.rooms
.user
.reset_notification_counts(sender_user, &body.room_id);
}
// ping presence
if services.config.allow_local_presence {
services
.presence
.ping_presence(sender_user, &ruma::presence::PresenceState::Online)
.await?;
}
match body.receipt_type {
| create_receipt::v3::ReceiptType::FullyRead => {
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
@@ -151,6 +137,13 @@ pub(crate) async fn create_receipt_route(
.await?;
},
| create_receipt::v3::ReceiptType::Read => {
if services.config.allow_local_presence {
services
.presence
.ping_presence(sender_user, &ruma::presence::PresenceState::Online)
.await?;
}
let receipt_content = BTreeMap::from_iter([(
body.event_id.clone(),
BTreeMap::from_iter([(
@@ -166,7 +159,6 @@ pub(crate) async fn create_receipt_route(
)]);
services
.rooms
.read_receipt
.readreceipt_update(
sender_user,
@@ -180,7 +172,6 @@ pub(crate) async fn create_receipt_route(
},
| create_receipt::v3::ReceiptType::ReadPrivate => {
let count = services
.rooms
.timeline
.get_pdu_count(&body.event_id)
.await
@@ -193,7 +184,6 @@ pub(crate) async fn create_receipt_route(
};
services
.rooms
.read_receipt
.private_read_set(&body.room_id, sender_user, count);
},
+1 -7
View File
@@ -18,15 +18,9 @@ pub(crate) async fn redact_event_route(
let sender_user = body.sender_user();
let body = &body.body;
let state_lock = services
.rooms
.state
.mutex
.lock(&body.room_id)
.await;
let state_lock = services.state.mutex.lock(&body.room_id).await;
let event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
+25 -18
View File
@@ -19,7 +19,7 @@
use tuwunel_core::{Err, Error, Result, debug_info, error, info, is_equal_to, utils, warn};
use tuwunel_service::users::device::generate_refresh_token;
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, join_room_by_id_helper};
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH};
use crate::Ruma;
const RANDOM_USER_ID_LENGTH: usize = 10;
@@ -316,7 +316,7 @@ pub(crate) async fn register_route(
stages: vec![AuthType::RegistrationToken],
}],
completed: Vec::new(),
params: Box::default(),
params: Default::default(),
session: None,
auth_error: None,
};
@@ -326,7 +326,7 @@ pub(crate) async fn register_route(
uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
completed: Vec::new(),
params: Box::default(),
params: Default::default(),
session: None,
auth_error: None,
};
@@ -517,13 +517,16 @@ pub(crate) async fn register_route(
&& services.config.grant_admin_to_first_user
&& let Ok(admin_room) = services.admin.get_admin_room().await
&& services
.rooms
.state_cache
.room_joined_count(&admin_room)
.await
.is_ok_and(is_equal_to!(1))
{
services.admin.make_user_admin(&user_id).await?;
services
.admin
.make_user_admin(&user_id)
.boxed()
.await?;
warn!("Granting {user_id} admin privileges as the first user");
}
@@ -532,7 +535,7 @@ pub(crate) async fn register_route(
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
{
for room in &services.server.config.auto_join_rooms {
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
let Ok(room_id) = services.alias.resolve(room).await else {
error!(
"Failed to resolve room alias to room ID when attempting to auto join \
{room}, skipping"
@@ -541,7 +544,6 @@ pub(crate) async fn register_route(
};
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), &room_id)
.await
@@ -553,17 +555,20 @@ pub(crate) async fn register_route(
}
if let Some(room_server_name) = room.server_name() {
match join_room_by_id_helper(
&services,
&user_id,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
None,
&body.appservice_info,
)
.boxed()
.await
let state_lock = services.state.mutex.lock(&room_id).await;
match services
.membership
.join(
&user_id,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
&body.appservice_info,
&state_lock,
)
.boxed()
.await
{
| Err(e) => {
// don't return this error so we don't fail registrations
@@ -575,6 +580,8 @@ pub(crate) async fn register_route(
info!("Automatically joined room {room} for user {user_id}");
},
}
drop(state_lock);
}
}
}
-2
View File
@@ -133,7 +133,6 @@ async fn paginate_relations_with_filter(
let depth: u8 = if recurse { 3 } else { 1 };
let events: Vec<_> = services
.rooms
.pdu_metadata
.get_relations(sender_user, room_id, target, start, limit, depth, dir)
.await
@@ -183,7 +182,6 @@ async fn visibility_filter<Pdu: Event>(
let (_, pdu) = &item;
services
.rooms
.state_accessor
.user_can_see_event(sender_user, pdu.room_id(), pdu.event_id())
.await
+9 -19
View File
@@ -15,6 +15,8 @@
use crate::Ruma;
const REASON_MAX_LEN: usize = 750;
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
///
/// Reports an abusive room to homeserver admins
@@ -29,24 +31,18 @@ pub(crate) async fn report_room_route(
info!(
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
body.room_id,
body.reason.as_deref().unwrap_or("")
body.room_id, body.reason,
);
if body
.reason
.as_ref()
.is_some_and(|s| s.len() > 750)
{
return Err!(Request(
InvalidParam("Reason too long, should be 750 characters or fewer",)
));
if body.reason.len().gt(&REASON_MAX_LEN) {
return Err!(Request(InvalidParam(
"Reason too long, should be {REASON_MAX_LEN} characters or fewer"
)));
}
delay_response().await;
if !services
.rooms
.state_cache
.server_in_room(&services.server.name, &body.room_id)
.await
@@ -64,7 +60,7 @@ pub(crate) async fn report_room_route(
"@room Room report received from {} -\n\nRoom ID: {}\n\nReport Reason: {}",
sender_user.to_owned(),
body.room_id,
body.reason.as_deref().unwrap_or("")
body.reason,
)))
.await
.ok();
@@ -95,12 +91,7 @@ pub(crate) async fn report_event_route(
delay_response().await;
// check if we know about the reported event ID or if it's invalid
let Ok(pdu) = services
.rooms
.timeline
.get_pdu(&body.event_id)
.await
else {
let Ok(pdu) = services.timeline.get_pdu(&body.event_id).await else {
return Err!(Request(NotFound("Event ID is not known to us or Event ID is invalid")));
};
@@ -170,7 +161,6 @@ async fn is_event_report_valid(
}
if !services
.rooms
.state_cache
.room_members(room_id)
.ready_any(|user_id| user_id == sender_user)
-2
View File
@@ -18,7 +18,6 @@ pub(crate) async fn get_room_aliases_route(
let sender_user = body.sender_user();
if !services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, &body.room_id)
.await
@@ -28,7 +27,6 @@ pub(crate) async fn get_room_aliases_route(
Ok(aliases::v3::Response {
aliases: services
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.map(ToOwned::to_owned)
+383 -204
View File
@@ -1,10 +1,13 @@
use std::collections::BTreeMap;
use axum::extract::State;
use futures::FutureExt;
use futures::{FutureExt, future::OptionFuture};
use ruma::{
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
api::client::room::{self, create_room},
api::client::room::{
self, create_room,
create_room::v3::{CreationContent, RoomPreset},
},
events::{
TimelineEventType,
room::{
@@ -16,21 +19,23 @@
member::{MembershipState, RoomMemberEventContent},
name::RoomNameEventContent,
power_levels::RoomPowerLevelsEventContent,
topic::RoomTopicEventContent,
topic::{RoomTopicEventContent, TopicContentBlock},
},
},
int,
room_version_rules::{RoomIdFormatVersion, RoomVersionRules},
serde::{JsonObject, Raw},
};
use serde_json::{json, value::to_raw_value};
use tuwunel_core::{
Err, Result, debug_info, debug_warn, err, info,
matrix::{StateKey, pdu::PduBuilder},
matrix::{StateKey, pdu::PduBuilder, room_version},
utils::BoolExt,
warn,
};
use tuwunel_service::{Services, appservice::RegistrationInfo};
use tuwunel_service::{Services, appservice::RegistrationInfo, rooms::state::RoomMutexGuard};
use crate::{Ruma, client::invite_helper};
use crate::{Ruma, client::utils::invite_check};
/// # `POST /_matrix/client/v3/createRoom`
///
@@ -53,166 +58,62 @@ pub(crate) async fn create_room_route(
State(services): State<crate::State>,
body: Ruma<create_room::v3::Request>,
) -> Result<create_room::v3::Response> {
use create_room::v3::RoomPreset;
can_create_room_check(&services, &body).await?;
can_publish_directory_check(&services, &body).await?;
let sender_user = body.sender_user();
// Figure out preset. We need it for preset specific events
let preset = body
.preset
.clone()
.unwrap_or(match &body.visibility {
| room::Visibility::Public => RoomPreset::PublicChat,
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
});
if !services.globals.allow_room_creation()
&& body.appservice_info.is_none()
&& !services.users.is_admin(sender_user).await
{
return Err!(Request(Forbidden("Room creation has been disabled.",)));
}
let alias: OptionFuture<_> = body
.room_alias_name
.as_ref()
.map(|alias| room_alias_check(&services, alias, body.appservice_info.as_ref()))
.into();
let room_id: OwnedRoomId = match &body.room_id {
| Some(custom_room_id) => custom_room_id_check(&services, custom_room_id)?,
| _ => RoomId::new(&services.server.name),
};
// check if room ID doesn't already exist instead of erroring on auth check
if services
.rooms
.short
.get_shortroomid(&room_id)
.await
.is_ok()
{
return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
}
if body.visibility == room::Visibility::Public
&& services
.server
.config
.lockdown_public_room_directory
&& !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none()
{
warn!(
"Non-admin user {sender_user} tried to publish {room_id} to the room directory \
while \"lockdown_public_room_directory\" is enabled"
);
if services.server.config.admin_room_notices {
// Determine room version
let (room_version, version_rules) = body
.room_version
.as_ref()
.map_or(Ok(&services.server.config.default_room_version), |version| {
services
.admin
.notice(&format!(
"Non-admin user {sender_user} tried to publish {room_id} to the room \
directory while \"lockdown_public_room_directory\" is enabled"
))
.await;
}
return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")));
}
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&room_id)
.await;
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() {
| Some(alias) =>
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?),
| _ => None,
};
let room_version = match body.room_version.clone() {
| Some(room_version) =>
if services
.server
.supported_room_version(&room_version)
{
room_version
} else {
return Err!(Request(UnsupportedRoomVersion(
"This server does not support that room version."
)));
},
| None => services
.server
.config
.default_room_version
.clone(),
};
.supported_room_version(version)
.then_ok_or_else(version, || {
err!(Request(UnsupportedRoomVersion(
"This server does not support room version {version:?}"
)))
})
})
.and_then(|version| Ok((version, room_version::rules(version)?)))?;
let create_content = match &body.creation_content {
| Some(content) => {
use RoomVersionId::*;
let mut content = content
.deserialize_as::<CanonicalJsonObject>()
.map_err(|e| {
err!(Request(BadJson(error!(
"Failed to deserialise content as canonical JSON: {e}"
))))
})?;
match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|e| {
err!(Request(BadJson(debug_error!("Invalid creation content: {e}"))))
})?,
);
},
| _ => {
// V11+ removed the "creator" key
},
}
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
);
content
},
| None => {
use RoomVersionId::*;
let content = match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(sender_user.to_owned()),
| _ => RoomCreateEventContent::new_v11(),
};
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())
.unwrap();
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
};
// Error on existing alias before committing to creation.
let alias = alias.await.transpose()?;
// Increment and hold the counter; the room will sync atomically to clients
// which is preferable.
let next_count = services.globals.next_count();
// 1. The room create event
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
state_key: Some(StateKey::new()),
..Default::default()
},
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
// 1. Create the create event.
let (room_id, state_lock) = match version_rules.room_id_format {
| RoomIdFormatVersion::V1 =>
create_create_event_legacy(&services, &body, room_version, &version_rules).await?,
| RoomIdFormatVersion::V2 =>
create_create_event(&services, &body, &preset, room_version, &version_rules)
.await
.map_err(|e| {
err!(Request(InvalidParam("Error while creating m.room.create event: {e}")))
})?,
};
// 2. Let the room creator join
let sender_user = body.sender_user();
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(sender_user.to_string(), &RoomMemberEventContent {
@@ -230,17 +131,14 @@ pub(crate) async fn create_room_route(
.await?;
// 3. Power levels
// Figure out preset. We need it for preset specific events
let preset = body
.preset
.clone()
.unwrap_or(match &body.visibility {
| room::Visibility::Public => RoomPreset::PublicChat,
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom
});
let mut users = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
let mut users = if !version_rules
.authorization
.explicitly_privilege_room_creators
{
BTreeMap::from_iter([(sender_user.to_owned(), int!(100))])
} else {
BTreeMap::new()
};
if preset == RoomPreset::TrustedPrivateChat {
for invite in &body.invite {
@@ -260,18 +158,23 @@ pub(crate) async fn create_room_route(
continue;
}
users.insert(invite.clone(), int!(100));
if !version_rules
.authorization
.additional_room_creators
{
users.insert(invite.clone(), int!(100));
}
}
}
let power_levels_content = default_power_levels_content(
&version_rules,
body.power_level_content_override.as_ref(),
&body.visibility,
users,
)?;
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
@@ -290,7 +193,6 @@ pub(crate) async fn create_room_route(
// 4. Canonical room alias
if let Some(room_alias_id) = &alias {
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomCanonicalAliasEventContent {
@@ -309,7 +211,6 @@ pub(crate) async fn create_room_route(
// 5.1 Join Rules
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(
@@ -329,7 +230,6 @@ pub(crate) async fn create_room_route(
// 5.2 History Visibility
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(
@@ -345,7 +245,6 @@ pub(crate) async fn create_room_route(
// 5.3 Guest Access
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(
@@ -365,7 +264,7 @@ pub(crate) async fn create_room_route(
// 6. Events listed in initial_state
for event in &body.initial_state {
let mut pdu_builder = event
.deserialize_as::<PduBuilder>()
.deserialize_as_unchecked::<PduBuilder>()
.map_err(|e| {
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
})?;
@@ -394,7 +293,6 @@ pub(crate) async fn create_room_route(
}
services
.rooms
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
.boxed()
@@ -404,7 +302,6 @@ pub(crate) async fn create_room_route(
// 7. Events implied by name and topic
if let Some(name) = &body.name {
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomNameEventContent::new(name.clone())),
@@ -418,10 +315,12 @@ pub(crate) async fn create_room_route(
if let Some(topic) = &body.topic {
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }),
PduBuilder::state(String::new(), &RoomTopicEventContent {
topic: topic.clone(),
topic_block: TopicContentBlock::default(),
}),
sender_user,
&room_id,
&state_lock,
@@ -433,43 +332,50 @@ pub(crate) async fn create_room_route(
drop(next_count);
drop(state_lock);
// 8. Events implied by invite (and TODO: invite_3pid)
for user_id in &body.invite {
if services
.users
.user_is_ignored(sender_user, user_id)
// if inviting anyone with room creation and invite check passes
if (!body.invite.is_empty() || !body.invite_3pid.is_empty())
&& invite_check(&services, sender_user, &room_id)
.await
{
continue;
} else if services
.users
.user_is_ignored(user_id, sender_user)
.await
{
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
continue;
}
.is_ok()
{
// 8. Events implied by invite (and TODO: invite_3pid)
for user_id in &body.invite {
if services
.users
.user_is_ignored(sender_user, user_id)
.await
{
continue;
} else if services
.users
.user_is_ignored(user_id, sender_user)
.await
{
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
continue;
}
if let Err(e) =
invite_helper(&services, sender_user, user_id, &room_id, None, body.is_direct)
if let Err(e) = services
.membership
.invite(sender_user, user_id, &room_id, None, body.is_direct)
.boxed()
.await
{
warn!(%e, "Failed to send invite");
{
warn!(%e, "Failed to send invite");
}
}
}
// Homeserver specific stuff
if let Some(alias) = alias {
services
.rooms
.alias
.set_alias(&alias, &room_id, sender_user)?;
}
if body.visibility == room::Visibility::Public {
services.rooms.directory.set_public(&room_id);
services.directory.set_public(&room_id);
if services.server.config.admin_room_notices {
services
@@ -488,25 +394,243 @@ pub(crate) async fn create_room_route(
Ok(create_room::v3::Response::new(room_id))
}
async fn create_create_event(
services: &Services,
body: &Ruma<create_room::v3::Request>,
preset: &RoomPreset,
room_version: &RoomVersionId,
version_rules: &RoomVersionRules,
) -> Result<(OwnedRoomId, RoomMutexGuard)> {
let _sender_user = body.sender_user();
let mut create_content = match &body.creation_content {
| Some(content) => {
let mut content = content
.deserialize_as_unchecked::<CanonicalJsonObject>()
.map_err(|e| {
err!(Request(BadJson(error!(
"Failed to deserialise content as canonical JSON: {e}"
))))
})?;
if !services.config.federate_created_rooms {
if !services.config.allow_federation || !content.contains_key("m.federate") {
content.insert("m.federate".into(), json!(false).try_into()?);
}
}
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
);
content
},
| None => {
let content = RoomCreateEventContent::new_v11();
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
if !services.config.federate_created_rooms {
content.insert("m.federate".into(), json!(false).try_into()?);
}
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
};
if version_rules
.authorization
.additional_room_creators
{
let mut additional_creators = body
.creation_content
.as_ref()
.and_then(|c| {
c.deserialize_as_unchecked::<CreationContent>()
.ok()
})
.unwrap_or_default()
.additional_creators;
if *preset == RoomPreset::TrustedPrivateChat {
additional_creators.extend(body.invite.clone());
}
additional_creators.sort();
additional_creators.dedup();
if !additional_creators.is_empty() {
create_content
.insert("additional_creators".into(), json!(additional_creators).try_into()?);
}
}
// 1. The room create event, using a placeholder room_id
let room_id = ruma::room_id!("!thiswillbereplaced").to_owned();
let state_lock = services.state.mutex.lock(&room_id).await;
let create_event_id = services
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
state_key: Some(StateKey::new()),
..Default::default()
},
body.sender_user(),
&room_id,
&state_lock,
)
.boxed()
.await?;
drop(state_lock);
// The real room_id is now the event_id.
let room_id = OwnedRoomId::from_parts('!', create_event_id.localpart(), None)?;
let state_lock = services.state.mutex.lock(&room_id).await;
Ok((room_id, state_lock))
}
async fn create_create_event_legacy(
services: &Services,
body: &Ruma<create_room::v3::Request>,
room_version: &RoomVersionId,
_version_rules: &RoomVersionRules,
) -> Result<(OwnedRoomId, RoomMutexGuard)> {
let room_id: OwnedRoomId = match &body.room_id {
| None => RoomId::new_v1(&services.server.name),
| Some(custom_id) => custom_room_id_check(services, custom_id).await?,
};
let state_lock = services.state.mutex.lock(&room_id).await;
let _short_id = services
.short
.get_or_create_shortroomid(&room_id)
.await;
let create_content = match &body.creation_content {
| Some(content) => {
use RoomVersionId::*;
let mut content = content
.deserialize_as_unchecked::<CanonicalJsonObject>()
.map_err(|e| {
err!(Request(BadJson(error!(
"Failed to deserialise content as canonical JSON: {e}"
))))
})?;
match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
content.insert(
"creator".into(),
json!(body.sender_user())
.try_into()
.map_err(|e| {
err!(Request(BadJson(debug_error!(
"Invalid creation content: {e}"
))))
})?,
);
},
| _ => {
// V11+ removed the "creator" key
},
}
if !services.config.federate_created_rooms {
if !services.config.allow_federation || !content.contains_key("m.federate") {
content.insert("m.federate".into(), json!(false).try_into()?);
}
}
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
);
content
},
| None => {
use RoomVersionId::*;
let content = match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(body.sender_user().to_owned()),
| _ => RoomCreateEventContent::new_v11(),
};
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
if !services.config.federate_created_rooms {
content.insert("m.federate".into(), json!(false).try_into()?);
}
content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
content
},
};
// 1. The room create event
services
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
state_key: Some(StateKey::new()),
..Default::default()
},
body.sender_user(),
&room_id,
&state_lock,
)
.boxed()
.await?;
Ok((room_id, state_lock))
}
/// creates the power_levels_content for the PDU builder
fn default_power_levels_content(
version_rules: &RoomVersionRules,
power_level_content_override: Option<&Raw<RoomPowerLevelsEventContent>>,
visibility: &room::Visibility,
users: BTreeMap<OwnedUserId, Int>,
) -> Result<serde_json::Value> {
use serde_json::to_value;
let mut power_levels_content =
to_value(RoomPowerLevelsEventContent { users, ..Default::default() })?;
let mut power_levels_content = RoomPowerLevelsEventContent::new(&version_rules.authorization);
power_levels_content.users = users;
let mut power_levels_content = to_value(power_levels_content)?;
// secure proper defaults of sensitive/dangerous permissions that moderators
// (power level 50) should not have easy access to
power_levels_content["events"]["m.room.power_levels"] = to_value(100)?;
power_levels_content["events"]["m.room.server_acl"] = to_value(100)?;
power_levels_content["events"]["m.room.tombstone"] = to_value(100)?;
power_levels_content["events"]["m.room.encryption"] = to_value(100)?;
power_levels_content["events"]["m.room.history_visibility"] = to_value(100)?;
if version_rules
.authorization
.explicitly_privilege_room_creators
{
power_levels_content["events"]["m.room.tombstone"] = to_value(150)?;
} else {
power_levels_content["events"]["m.room.tombstone"] = to_value(100)?;
}
// always allow users to respond (not post new) to polls. this is primarily
// useful in read-only announcement rooms that post a public poll.
power_levels_content["events"]["org.matrix.msc3381.poll.response"] = to_value(0)?;
@@ -572,7 +696,6 @@ async fn room_alias_check(
})?;
if services
.rooms
.alias
.resolve_local_alias(&full_room_alias)
.await
@@ -599,7 +722,7 @@ async fn room_alias_check(
}
/// if a room is being created with a custom room ID, run our checks against it
fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
async fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
// apply forbidden room alias checks to custom room IDs too
if services
.globals
@@ -623,8 +746,64 @@ fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<Own
let server_name = services.globals.server_name();
let full_room_id = format!("!{custom_room_id}:{server_name}");
OwnedRoomId::parse(full_room_id)
.map_err(Into::into)
let room_id = OwnedRoomId::parse(full_room_id)
.inspect(|full_room_id| debug_info!(?full_room_id, "Full custom room ID"))
.inspect_err(|e| warn!(?e, ?custom_room_id, "Failed to create room with custom room ID",))
.inspect_err(|e| {
warn!(?e, ?custom_room_id, "Failed to create room with custom room ID");
})?;
// check if room ID doesn't already exist instead of erroring on auth check
if services
.short
.get_shortroomid(&room_id)
.await
.is_ok()
{
return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
}
Ok(room_id)
}
async fn can_publish_directory_check(
services: &Services,
body: &Ruma<create_room::v3::Request>,
) -> Result {
if !services
.server
.config
.lockdown_public_room_directory
|| body.appservice_info.is_some()
|| body.visibility != room::Visibility::Public
|| services.users.is_admin(body.sender_user()).await
{
return Ok(());
}
let msg = format!(
"Non-admin user {} tried to publish new to the directory while \
lockdown_public_room_directory is enabled",
body.sender_user(),
);
warn!("{msg}");
if services.server.config.admin_room_notices {
services.admin.notice(&msg).await;
}
Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")))
}
async fn can_create_room_check(
services: &Services,
body: &Ruma<create_room::v3::Request>,
) -> Result {
if !services.globals.allow_room_creation()
&& body.appservice_info.is_none()
&& !services.users.is_admin(body.sender_user()).await
{
return Err!(Request(Forbidden("Room creation has been disabled.",)));
}
Ok(())
}
-2
View File
@@ -16,13 +16,11 @@ pub(crate) async fn get_room_event_route(
let room_id = &body.room_id;
let event = services
.rooms
.timeline
.get_pdu(event_id)
.map_err(|_| err!(Request(NotFound("Event {} not found.", event_id))));
let visible = services
.rooms
.state_accessor
.user_can_see_event(body.sender_user(), room_id, event_id)
.map(Ok);
+1 -9
View File
@@ -17,7 +17,6 @@ pub(crate) async fn room_initial_sync_route(
let room_id = &body.room_id;
if !services
.rooms
.state_accessor
.user_can_see_state_events(body.sender_user(), room_id)
.await
@@ -26,19 +25,13 @@ pub(crate) async fn room_initial_sync_route(
}
let membership = services
.rooms
.state_cache
.user_membership(body.sender_user(), room_id)
.map(Ok);
let visibility = services
.rooms
.directory
.visibility(room_id)
.map(Ok);
let visibility = services.directory.visibility(room_id).map(Ok);
let state = services
.rooms
.state_accessor
.room_state_full_pdus(room_id)
.map_ok(Event::into_format)
@@ -46,7 +39,6 @@ pub(crate) async fn room_initial_sync_route(
let limit = LIMIT_MAX;
let events = services
.rooms
.timeline
.pdus_rev(None, room_id, None)
.try_take(limit)
+57 -91
View File
@@ -1,7 +1,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::{
FutureExt, StreamExt,
FutureExt, StreamExt, TryFutureExt,
future::{OptionFuture, join3},
stream::FuturesUnordered,
};
@@ -12,7 +12,7 @@
federation::space::{SpaceHierarchyParentSummary, get_hierarchy},
},
events::room::member::MembershipState,
space::SpaceRoomJoinRule::{self, *},
room::{JoinRuleSummary, RoomSummary},
};
use tuwunel_core::{
Err, Result, debug_warn, trace,
@@ -34,8 +34,8 @@
pub(crate) async fn get_room_summary_legacy(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_summary::msc3266::Request>,
) -> Result<RumaResponse<get_summary::msc3266::Response>> {
body: Ruma<get_summary::v1::Request>,
) -> Result<RumaResponse<get_summary::v1::Response>> {
get_room_summary(State(services), InsecureClientIp(client), body)
.boxed()
.await
@@ -51,15 +51,14 @@ pub(crate) async fn get_room_summary_legacy(
pub(crate) async fn get_room_summary(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_summary::msc3266::Request>,
) -> Result<get_summary::msc3266::Response> {
body: Ruma<get_summary::v1::Request>,
) -> Result<get_summary::v1::Response> {
let (room_id, servers) = services
.rooms
.alias
.resolve_with_servers(&body.room_id_or_alias, Some(body.via.clone()))
.await?;
if services.rooms.metadata.is_banned(&room_id).await {
if services.metadata.is_banned(&room_id).await {
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
}
@@ -73,9 +72,8 @@ async fn room_summary_response(
room_id: &RoomId,
servers: &[OwnedServerName],
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
) -> Result<get_summary::v1::Response> {
if services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await
@@ -85,23 +83,12 @@ async fn room_summary_response(
.await;
}
let room =
remote_room_summary_hierarchy_response(services, room_id, servers, sender_user).await?;
let summary = remote_room_summary_hierarchy_response(services, room_id, servers, sender_user)
.await?
.summary;
Ok(get_summary::msc3266::Response {
room_id: room_id.to_owned(),
canonical_alias: room.canonical_alias,
avatar_url: room.avatar_url,
guest_can_join: room.guest_can_join,
name: room.name,
num_joined_members: room.num_joined_members,
topic: room.topic,
world_readable: room.world_readable,
join_rule: room.join_rule,
room_type: room.room_type,
room_version: room.room_version,
encryption: room.encryption,
allowed_room_ids: room.allowed_room_ids,
Ok(get_summary::v1::Response {
summary,
membership: sender_user
.is_some()
.then_some(MembershipState::Leave),
@@ -112,22 +99,13 @@ async fn local_room_summary_response(
services: &Services,
room_id: &RoomId,
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
) -> Result<get_summary::v1::Response> {
trace!(?sender_user, "Sending local room summary response for {room_id:?}");
let join_rule = services
.rooms
.state_accessor
.get_join_rules(room_id);
let join_rule = services.state_accessor.get_join_rules(room_id);
let world_readable = services
.rooms
.state_accessor
.is_world_readable(room_id);
let world_readable = services.state_accessor.is_world_readable(room_id);
let guest_can_join = services
.rooms
.state_accessor
.guest_can_join(room_id);
let guest_can_join = services.state_accessor.guest_can_join(room_id);
let (join_rule, world_readable, guest_can_join) =
join3(join_rule, world_readable, guest_can_join).await;
@@ -139,55 +117,43 @@ async fn local_room_summary_response(
&join_rule.clone().into(),
guest_can_join,
world_readable,
join_rule.allowed_rooms(),
join_rule.allowed_room_ids(),
sender_user,
)
.await?;
let canonical_alias = services
.rooms
.state_accessor
.get_canonical_alias(room_id)
.ok();
let name = services
.rooms
.state_accessor
.get_name(room_id)
.ok();
let name = services.state_accessor.get_name(room_id).ok();
let topic = services
.rooms
.state_accessor
.get_room_topic(room_id)
.ok();
let room_type = services
.rooms
.state_accessor
.get_room_type(room_id)
.ok();
let avatar_url = services
.rooms
.state_accessor
.get_avatar(room_id)
.map(|res| res.into_option().unwrap_or_default().url);
.map_ok(|content| content.url)
.ok()
.map(Option::flatten);
let room_version = services
.rooms
.state
.get_room_version(room_id)
.ok();
let room_version = services.state.get_room_version(room_id).ok();
let encryption = services
.rooms
.state_accessor
.get_room_encryption(room_id)
.ok();
let num_joined_members = services
.rooms
.state_cache
.room_joined_count(room_id)
.unwrap_or(0);
@@ -195,7 +161,6 @@ async fn local_room_summary_response(
let membership: OptionFuture<_> = sender_user
.map(|sender_user| {
services
.rooms
.state_accessor
.get_member(room_id, sender_user)
.map_ok_or(MembershipState::Leave, |content| content.membership)
@@ -224,24 +189,22 @@ async fn local_room_summary_response(
membership,
);
Ok(get_summary::msc3266::Response {
room_id: room_id.to_owned(),
canonical_alias,
avatar_url,
guest_can_join,
name,
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
topic,
world_readable,
room_type,
room_version,
encryption,
Ok(get_summary::v1::Response {
summary: RoomSummary {
room_id: room_id.to_owned(),
canonical_alias,
avatar_url,
guest_can_join,
name,
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
topic,
world_readable,
room_type,
room_version,
encryption,
join_rule: join_rule.into(),
},
membership,
allowed_room_ids: join_rule
.allowed_rooms()
.map(Into::into)
.collect(),
join_rule: join_rule.into(),
})
}
@@ -257,7 +220,7 @@ async fn remote_room_summary_hierarchy_response(
return Err!(Request(Forbidden("Federation is disabled.")));
}
if services.rooms.metadata.is_disabled(room_id).await {
if services.metadata.is_disabled(room_id).await {
return Err!(Request(Forbidden(
"Federaton of room {room_id} is currently disabled on this server."
)));
@@ -277,10 +240,11 @@ async fn remote_room_summary_hierarchy_response(
while let Some(Ok(response)) = requests.next().await {
trace!("{response:?}");
let room = response.room.clone();
if room.room_id != room_id {
let summary = &room.summary;
if summary.room_id != room_id {
debug_warn!(
"Room ID {} returned does not belong to the requested room ID {}",
room.room_id,
summary.room_id,
room_id
);
continue;
@@ -289,10 +253,10 @@ async fn remote_room_summary_hierarchy_response(
return user_can_see_summary(
services,
room_id,
&room.join_rule,
room.guest_can_join,
room.world_readable,
room.allowed_room_ids.iter().map(AsRef::as_ref),
&summary.join_rule,
summary.guest_can_join,
summary.world_readable,
summary.join_rule.allowed_room_ids(),
sender_user,
)
.await
@@ -308,7 +272,7 @@ async fn remote_room_summary_hierarchy_response(
async fn user_can_see_summary<'a, I>(
services: &Services,
room_id: &RoomId,
join_rule: &SpaceRoomJoinRule,
join_rule: &JoinRuleSummary,
guest_can_join: bool,
world_readable: bool,
allowed_room_ids: I,
@@ -317,23 +281,25 @@ async fn user_can_see_summary<'a, I>(
where
I: Iterator<Item = &'a RoomId> + Send,
{
let is_public_room = matches!(join_rule, Public | Knock | KnockRestricted);
let is_public_room = matches!(
join_rule,
JoinRuleSummary::Public | JoinRuleSummary::Knock | JoinRuleSummary::KnockRestricted(_)
);
match sender_user {
| Some(sender_user) => {
let user_can_see_state_events = services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, room_id);
let is_guest = services
.users
.is_deactivated(sender_user)
.unwrap_or(false);
let user_in_allowed_restricted_room = allowed_room_ids.stream().any(|room| {
services
.rooms
.state_cache
.is_joined(sender_user, room)
});
let user_in_allowed_restricted_room = allowed_room_ids
.stream()
.any(|room| services.state_cache.is_joined(sender_user, room));
let (user_can_see_state_events, is_guest, user_in_allowed_restricted_room) =
join3(user_can_see_state_events, is_guest, user_in_allowed_restricted_room)
+19 -31
View File
@@ -4,7 +4,7 @@
use futures::StreamExt;
use ruma::{
CanonicalJsonObject, RoomId, RoomVersionId,
api::client::{error::ErrorKind, room::upgrade_room},
api::client::room::upgrade_room,
events::{
StateEventType, TimelineEventType,
room::{
@@ -14,11 +14,12 @@
},
},
int,
room_version_rules::RoomIdFormatVersion,
};
use serde_json::{json, value::to_raw_value};
use tuwunel_core::{
Err, Error, Result, err,
matrix::{Event, StateKey, pdu::PduBuilder},
Err, Result, err,
matrix::{Event, StateKey, pdu::PduBuilder, room_version},
};
use crate::Ruma;
@@ -61,33 +62,35 @@ pub(crate) async fn upgrade_room_route(
.server
.supported_room_version(&body.new_version)
{
return Err(Error::BadRequest(
ErrorKind::UnsupportedRoomVersion,
return Err!(Request(UnsupportedRoomVersion(
"This server does not support that room version.",
));
)));
}
if matches!(body.new_version, RoomVersionId::V12) {
return Err!(Request(UnsupportedRoomVersion(
"Upgrading to version 12 is still under development.",
)));
}
let room_version_rules = room_version::rules(&body.new_version)?;
let room_id_format = &room_version_rules.room_id_format;
assert!(*room_id_format == RoomIdFormatVersion::V1, "TODO");
// Create a replacement room
let replacement_room = RoomId::new(services.globals.server_name());
let replacement_room = RoomId::new_v1(services.globals.server_name());
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&replacement_room)
.await;
let state_lock = services
.rooms
.state
.mutex
.lock(&body.room_id)
.await;
let state_lock = services.state.mutex.lock(&body.room_id).await;
// Send a m.room.tombstone event to the old room to indicate that it is not
// intended to be used any further Fail if the sender does not have the required
// permissions
let tombstone_event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
@@ -102,16 +105,10 @@ pub(crate) async fn upgrade_room_route(
// Change lock to replacement room
drop(state_lock);
let state_lock = services
.rooms
.state
.mutex
.lock(&replacement_room)
.await;
let state_lock = services.state.mutex.lock(&replacement_room).await;
// Get the old room creation event
let mut create_event_content: CanonicalJsonObject = services
.rooms
.state_accessor
.room_state_get_content(&body.room_id, &StateEventType::RoomCreate, "")
.await
@@ -164,7 +161,6 @@ pub(crate) async fn upgrade_room_route(
}
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
@@ -183,7 +179,6 @@ pub(crate) async fn upgrade_room_route(
// Join the new room
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
@@ -212,7 +207,6 @@ pub(crate) async fn upgrade_room_route(
// Replicate transferable state events to the new room
for event_type in TRANSFERABLE_STATE_EVENTS {
let event_content = match services
.rooms
.state_accessor
.room_state_get(&body.room_id, event_type, "")
.await
@@ -222,7 +216,6 @@ pub(crate) async fn upgrade_room_route(
};
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
@@ -240,27 +233,23 @@ pub(crate) async fn upgrade_room_route(
// Moves any local aliases to the new room
let mut local_aliases = services
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.boxed();
while let Some(alias) = local_aliases.next().await {
services
.rooms
.alias
.remove_alias(alias, sender_user)
.await?;
services
.rooms
.alias
.set_alias(alias, &replacement_room, sender_user)?;
}
// Get the old room power levels
let power_levels_event_content: RoomPowerLevelsEventContent = services
.rooms
.state_accessor
.room_state_get_content(&body.room_id, &StateEventType::RoomPowerLevels, "")
.await
@@ -280,7 +269,6 @@ pub(crate) async fn upgrade_room_route(
// Modify the power levels in the old room to prevent sending of events and
// inviting new users
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(StateKey::new(), &RoomPowerLevelsEventContent {
+1 -10
View File
@@ -87,7 +87,6 @@ async fn category_room_events(
.map(StreamExt::boxed)
.unwrap_or_else(|| {
services
.rooms
.state_cache
.rooms_joined(sender_user)
.map(ToOwned::to_owned)
@@ -110,12 +109,7 @@ async fn category_room_events(
limit,
};
let (count, results) = services
.rooms
.search
.search_pdus(&query)
.await
.ok()?;
let (count, results) = services.search.search_pdus(&query).await.ok()?;
results
.collect::<Vec<_>>()
@@ -187,7 +181,6 @@ async fn category_room_events(
async fn procure_room_state(services: &Services, room_id: &RoomId) -> Result<RoomState> {
let state = services
.rooms
.state_accessor
.room_state_full_pdus(room_id)
.map_ok(Event::into_format)
@@ -208,14 +201,12 @@ async fn check_room_visible(
let is_joined = !check_visible
|| services
.rooms
.state_cache
.is_joined(user_id, room_id)
.await;
let state_visible = !check_state
|| services
.rooms
.state_accessor
.user_can_see_state_events(user_id, room_id)
.await;
+1 -8
View File
@@ -30,16 +30,10 @@ pub(crate) async fn send_message_event_route(
return Err!(Request(Forbidden("Encryption has been disabled")));
}
let state_lock = services
.rooms
.state
.mutex
.lock(&body.room_id)
.await;
let state_lock = services.state.mutex.lock(&body.room_id).await;
if body.event_type == MessageLikeEventType::CallInvite
&& services
.rooms
.directory
.is_public_room(&body.room_id)
.await
@@ -75,7 +69,6 @@ pub(crate) async fn send_message_event_route(
.map_err(|e| err!(Request(BadJson("Invalid JSON body: {e}"))))?;
let event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
+18 -13
View File
@@ -22,20 +22,9 @@ pub(super) async fn handle_login(
_body: &Ruma<Request>,
info: &Token,
) -> Result<OwnedUserId> {
let config = &services.config.jwt;
if !config.enable {
return Err!(Request(Unknown("JWT login is not enabled.")));
}
let claim = validate(config, &info.token)?;
let local = claim.sub.to_lowercase();
let server = &services.server.name;
let user_id = UserId::parse_with_server_name(local, server).map_err(|e| {
err!(Request(InvalidUsername("JWT subject is not a valid user MXID: {e}")))
})?;
let user_id = validate_user(services, &info.token)?;
if !services.users.exists(&user_id).await {
let config = &services.config.jwt;
if !config.register_user {
return Err!(Request(NotFound("User {user_id} is not registered on this server.")));
}
@@ -49,6 +38,22 @@ pub(super) async fn handle_login(
Ok(user_id)
}
pub(crate) fn validate_user(services: &Services, token: &str) -> Result<OwnedUserId> {
let config = &services.config.jwt;
if !config.enable {
return Err!(Request(Unauthorized("JWT login is not enabled.")));
}
let claim = validate(config, token)?;
let local = claim.sub.to_lowercase();
let server = &services.server.name;
let user_id = UserId::parse_with_server_name(local, server).map_err(|e| {
err!(Request(InvalidUsername("JWT subject is not a valid user MXID: {e}")))
})?;
Ok(user_id)
}
fn validate(config: &JwtConfig, token: &str) -> Result<Claim> {
let verifier = init_verifier(config)?;
let validator = init_validator(config)?;
+2
View File
@@ -1,3 +1,4 @@
use futures::FutureExt;
use ruma::{OwnedUserId, UserId};
use tuwunel_core::{Err, Result, debug};
use tuwunel_service::Services;
@@ -63,6 +64,7 @@ pub(super) async fn ldap_login(
services
.admin
.make_user_admin(lowercased_user_id)
.boxed()
.await?;
} else if !is_ldap_admin && is_tuwunel_admin {
services
+1 -1
View File
@@ -1,5 +1,5 @@
mod appservice;
mod jwt;
pub(crate) mod jwt;
mod ldap;
mod logout;
mod password;
+1 -4
View File
@@ -56,10 +56,7 @@ pub(crate) async fn login_token_route(
let mut uiaainfo = uiaa::UiaaInfo {
flows: vec![password_flow],
completed: Vec::new(),
params: Box::default(),
session: None,
auth_error: None,
..Default::default()
};
match &body.auth {
+52 -28
View File
@@ -1,21 +1,21 @@
use std::{
collections::{BTreeSet, VecDeque},
str::FromStr,
};
use std::{collections::BTreeSet, iter::once, str::FromStr};
use axum::extract::State;
use futures::{StreamExt, TryFutureExt, future::OptionFuture};
use futures::{FutureExt, StreamExt, TryFutureExt, future::OptionFuture, stream::FuturesOrdered};
use ruma::{
OwnedRoomId, OwnedServerName, RoomId, UInt, UserId, api::client::space::get_hierarchy,
};
use tuwunel_core::{
Err, Result,
Err, Result, debug_error,
utils::{future::TryExtExt, stream::IterStream},
};
use tuwunel_service::{
Services,
rooms::spaces::{
PaginationToken, SummaryAccessibility, get_parent_children_via, summary_to_chunk,
rooms::{
short::ShortRoomId,
spaces::{
PaginationToken, SummaryAccessibility, get_parent_children_via, summary_to_chunk,
},
},
};
@@ -77,30 +77,38 @@ async fn get_client_hierarchy<'a, ShortRoomIds>(
short_room_ids: ShortRoomIds,
) -> Result<get_hierarchy::v1::Response>
where
ShortRoomIds: Iterator<Item = &'a u64> + Clone + Send + Sync + 'a,
ShortRoomIds: Iterator<Item = &'a ShortRoomId> + Clone + Send + Sync + 'a,
{
type Via = Vec<OwnedServerName>;
type Entry = (OwnedRoomId, Via);
type Rooms = VecDeque<Entry>;
type Via = Vec<OwnedServerName>;
let mut queue: Rooms = [(
room_id.to_owned(),
room_id
let initial = async move {
let via = room_id
.server_name()
.map(ToOwned::to_owned)
.into_iter()
.collect(),
)]
.into();
.collect::<Vec<_>>();
let mut rooms = Vec::with_capacity(limit);
let mut parents = BTreeSet::new();
while let Some((current_room, via)) = queue.pop_front() {
let summary = services
.rooms
.spaces
.get_summary_and_children_client(&current_room, suggested_only, sender_user, &via)
.await?;
.get_summary_and_children_client(room_id, suggested_only, sender_user, &via)
.await;
(room_id.to_owned(), via, summary)
};
let mut parents = BTreeSet::new();
let mut rooms = Vec::with_capacity(limit);
let mut queue: FuturesOrdered<_> = once(initial.boxed()).collect();
while let Some((current_room, via, summary)) = queue.next().await {
let summary = match summary {
| Ok(summary) => summary,
| Err(e) => {
debug_error!(?current_room, ?via, ?e, "error getting summary");
continue;
},
};
match (summary, current_room == room_id) {
| (None | Some(SummaryAccessibility::Inaccessible), false) => {
@@ -130,7 +138,6 @@ async fn get_client_hierarchy<'a, ShortRoomIds>(
.stream()
.skip_while(|(room, _)| {
services
.rooms
.short
.get_shortroomid(room)
.map_ok(|short| {
@@ -159,20 +166,37 @@ async fn get_client_hierarchy<'a, ShortRoomIds>(
continue;
}
queue.extend(children);
children
.into_iter()
.map(|(room_id, via)| async move {
let summary = services
.spaces
.get_summary_and_children_client(
&room_id,
suggested_only,
sender_user,
&via,
)
.await;
(room_id, via, summary)
})
.map(FutureExt::boxed)
.for_each(|entry| queue.push_back(entry));
},
}
}
let next_batch: OptionFuture<_> = queue
.pop_front()
.map(async |(room, _)| {
.next()
.await
.map(async |(room, ..)| {
parents.insert(room);
let next_short_room_ids: Vec<_> = parents
.iter()
.stream()
.filter_map(|room_id| services.rooms.short.get_shortroomid(room_id).ok())
.filter_map(|room_id| services.short.get_shortroomid(room_id).ok())
.collect()
.await;
+52 -58
View File
@@ -2,7 +2,7 @@
use futures::{FutureExt, TryFutureExt, TryStreamExt};
use ruma::{
OwnedEventId, RoomId, UserId,
api::client::state::{get_state_events, get_state_events_for_key, send_state_event},
api::client::state::{get_state_event_for_key, get_state_events, send_state_event},
events::{
AnyStateEventContent, StateEventType,
room::{
@@ -78,7 +78,6 @@ pub(crate) async fn get_state_events_route(
let sender_user = body.sender_user();
if !services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, &body.room_id)
.await
@@ -88,7 +87,6 @@ pub(crate) async fn get_state_events_route(
Ok(get_state_events::v3::Response {
room_state: services
.rooms
.state_accessor
.room_state_full_pdus(&body.room_id)
.map_ok(Event::into_format)
@@ -107,12 +105,11 @@ pub(crate) async fn get_state_events_route(
/// readable
pub(crate) async fn get_state_events_for_key_route(
State(services): State<crate::State>,
body: Ruma<get_state_events_for_key::v3::Request>,
) -> Result<get_state_events_for_key::v3::Response> {
body: Ruma<get_state_event_for_key::v3::Request>,
) -> Result<get_state_event_for_key::v3::Response> {
let sender_user = body.sender_user();
if !services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, &body.room_id)
.await
@@ -123,7 +120,6 @@ pub(crate) async fn get_state_events_for_key_route(
}
let event = services
.rooms
.state_accessor
.room_state_get(&body.room_id, &body.event_type, &body.state_key)
.await
@@ -140,7 +136,7 @@ pub(crate) async fn get_state_events_for_key_route(
.as_ref()
.is_some_and(|f| f.to_lowercase().eq("event"));
Ok(get_state_events_for_key::v3::Response {
Ok(get_state_event_for_key::v3::Response {
content: event_format.or(|| event.get_content_as_value()),
event: event_format.then(|| {
json!({
@@ -167,8 +163,8 @@ pub(crate) async fn get_state_events_for_key_route(
/// readable
pub(crate) async fn get_state_events_for_empty_key_route(
State(services): State<crate::State>,
body: Ruma<get_state_events_for_key::v3::Request>,
) -> Result<RumaResponse<get_state_events_for_key::v3::Response>> {
body: Ruma<get_state_event_for_key::v3::Request>,
) -> Result<RumaResponse<get_state_event_for_key::v3::Response>> {
get_state_events_for_key_route(State(services), body)
.await
.map(RumaResponse)
@@ -184,9 +180,8 @@ async fn send_state_event_for_key_helper(
timestamp: Option<ruma::MilliSecondsSinceUnixEpoch>,
) -> Result<OwnedEventId> {
allowed_to_send_state_event(services, room_id, event_type, state_key, json).await?;
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let state_lock = services.state.mutex.lock(room_id).await;
let event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
@@ -222,7 +217,7 @@ async fn allowed_to_send_state_event(
| StateEventType::RoomServerAcl => {
// prevents common ACL paw-guns as ACL management is difficult and prone to
// irreversible mistakes
match json.deserialize_as::<RoomServerAclEventContent>() {
match json.deserialize_as_unchecked::<RoomServerAclEventContent>() {
| Ok(acl_content) => {
if acl_content.allow_is_empty() {
return Err!(Request(BadJson(debug_warn!(
@@ -282,7 +277,7 @@ async fn allowed_to_send_state_event(
// admin room is a sensitive room, it should not ever be made public
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
if admin_room_id == room_id {
match json.deserialize_as::<RoomJoinRulesEventContent>() {
match json.deserialize_as_unchecked::<RoomJoinRulesEventContent>() {
| Ok(join_rule) =>
if join_rule.join_rule == JoinRule::Public {
return Err!(Request(Forbidden(
@@ -301,7 +296,7 @@ async fn allowed_to_send_state_event(
| StateEventType::RoomHistoryVisibility => {
// admin room is a sensitive room, it should not ever be made world readable
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
match json.deserialize_as::<RoomHistoryVisibilityEventContent>() {
match json.deserialize_as_unchecked::<RoomHistoryVisibilityEventContent>() {
| Ok(visibility_content) => {
if admin_room_id == room_id
&& visibility_content.history_visibility
@@ -322,7 +317,7 @@ async fn allowed_to_send_state_event(
}
},
| StateEventType::RoomCanonicalAlias => {
match json.deserialize_as::<RoomCanonicalAliasEventContent>() {
match json.deserialize_as_unchecked::<RoomCanonicalAliasEventContent>() {
| Ok(canonical_alias_content) => {
let mut aliases = canonical_alias_content.alt_aliases.clone();
@@ -332,7 +327,6 @@ async fn allowed_to_send_state_event(
for alias in aliases {
let (alias_room_id, _servers) = services
.rooms
.alias
.resolve_alias(&alias, None)
.await
@@ -354,52 +348,52 @@ async fn allowed_to_send_state_event(
},
}
},
| StateEventType::RoomMember => match json.deserialize_as::<RoomMemberEventContent>() {
| Ok(membership_content) => {
let Ok(_state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"Membership event has invalid or non-existent state key"
)));
};
if let Some(authorising_user) =
membership_content.join_authorized_via_users_server
{
if membership_content.membership != MembershipState::Join {
| StateEventType::RoomMember =>
match json.deserialize_as_unchecked::<RoomMemberEventContent>() {
| Ok(membership_content) => {
let Ok(_state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
"Membership event has invalid or non-existent state key"
)));
}
};
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this \
homeserver"
)));
}
if let Some(authorising_user) =
membership_content.join_authorized_via_users_server
{
if membership_content.membership != MembershipState::Join {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
)));
}
services
.rooms
.state_cache
.is_joined(&authorising_user, room_id)
.map(is_false!())
.map(BoolExt::into_result)
.map_err(|()| {
err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room. They \
cannot authorise the join."
)))
})
.await?;
}
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this \
homeserver"
)));
}
services
.state_cache
.is_joined(&authorising_user, room_id)
.map(is_false!())
.map(BoolExt::into_result)
.map_err(|()| {
err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room. \
They cannot authorise the join."
)))
})
.await?;
}
},
| Err(e) => {
return Err!(Request(BadJson(
"Membership content must have a valid JSON body with at least a valid \
membership state: {e}"
)));
},
},
| Err(e) => {
return Err!(Request(BadJson(
"Membership content must have a valid JSON body with at least a valid \
membership state: {e}"
)));
},
},
| _ => (),
}
+6 -10
View File
@@ -11,7 +11,7 @@
use tuwunel_core::{
Error, PduCount, Result,
matrix::pdu::PduEvent,
utils::stream::{BroadbandExt, ReadyExt, TryIgnore},
utils::stream::{BroadbandExt, ReadyExt},
};
use tuwunel_service::Services;
@@ -27,22 +27,20 @@ async fn load_timeline(
roomsincecount: PduCount,
next_batch: Option<PduCount>,
limit: usize,
) -> Result<(Vec<(PduCount, PduEvent)>, bool), Error> {
) -> Result<(Vec<(PduCount, PduEvent)>, bool, PduCount), Error> {
let last_timeline_count = services
.rooms
.timeline
.last_timeline_count(Some(sender_user), room_id)
.last_timeline_count(Some(sender_user), room_id, next_batch)
.await?;
if last_timeline_count <= roomsincecount {
return Ok((Vec::new(), false));
return Ok((Vec::new(), false, last_timeline_count));
}
let non_timeline_pdus = services
.rooms
.timeline
.pdus_rev(Some(sender_user), room_id, None)
.ignore_err()
.ready_filter_map(Result::ok)
.ready_skip_while(|&(pducount, _)| pducount > next_batch.unwrap_or_else(PduCount::max))
.ready_take_while(|&(pducount, _)| pducount > roomsincecount);
@@ -60,7 +58,7 @@ async fn load_timeline(
// is limited unless there are events in non_timeline_pdus
let limited = non_timeline_pdus.next().await.is_some();
Ok((timeline_pdus, limited))
Ok((timeline_pdus, limited, last_timeline_count))
}
async fn share_encrypted_room(
@@ -70,14 +68,12 @@ async fn share_encrypted_room(
ignore_room: Option<&RoomId>,
) -> bool {
services
.rooms
.state_cache
.get_shared_rooms(sender_user, user_id)
.ready_filter(|&room_id| Some(room_id) != ignore_room)
.map(ToOwned::to_owned)
.broad_any(async |other_room_id| {
services
.rooms
.state_accessor
.is_encrypted_room(&other_room_id)
.await
+73 -117
View File
@@ -6,7 +6,7 @@
use axum::extract::State;
use futures::{
FutureExt, StreamExt, TryFutureExt, TryStreamExt,
future::{OptionFuture, join, join3, join4, join5, try_join4},
future::{OptionFuture, join, join3, join4, join5, try_join3},
pin_mut,
};
use ruma::{
@@ -18,7 +18,7 @@
v3::{
Ephemeral, Filter, GlobalAccountData, InviteState, InvitedRoom, JoinedRoom,
KnockState, KnockedRoom, LeftRoom, Presence, RoomAccountData, RoomSummary, Rooms,
State as RoomState, Timeline, ToDevice,
State as RoomState, StateEvents, Timeline, ToDevice,
},
},
uiaa::UiaaResponse,
@@ -133,6 +133,11 @@ pub(crate) async fn sync_events_route(
.await
.log_err()
.ok();
// Record user as actively syncing for push suppression heuristic.
if services.config.suppress_push_when_active {
services.presence.note_sync(sender_user).await;
}
}
let mut since = body
@@ -177,9 +182,10 @@ pub(crate) async fn sync_events_route(
}
// Wait for activity
if time::timeout_at(stop_at, watchers).await.is_err() {
if time::timeout_at(stop_at, watchers).await.is_err() || services.server.is_stopping() {
let response = build_empty_response(&services, &body, next_batch).await;
trace!(since, next_batch, "empty response");
return Ok(sync_events::v3::Response::new(next_batch.to_string()));
return Ok(response);
}
trace!(
@@ -194,6 +200,21 @@ pub(crate) async fn sync_events_route(
}
}
async fn build_empty_response(
services: &Services,
body: &Ruma<sync_events::v3::Request>,
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())
.await,
..sync_events::v3::Response::new(next_batch.to_string())
}
}
#[tracing::instrument(
name = "build",
level = "debug",
@@ -225,7 +246,6 @@ async fn build_sync_events(
};
let joined_rooms = services
.rooms
.state_cache
.rooms_joined(sender_user)
.map(ToOwned::to_owned)
@@ -258,7 +278,6 @@ async fn build_sync_events(
);
let left_rooms = services
.rooms
.state_cache
.rooms_left(sender_user)
.broad_filter_map(|(room_id, _)| {
@@ -278,12 +297,10 @@ async fn build_sync_events(
.collect();
let invited_rooms = services
.rooms
.state_cache
.rooms_invited(sender_user)
.fold_default(async |mut invited_rooms: BTreeMap<_, _>, (room_id, invite_state)| {
let invite_count = services
.rooms
.state_cache
.get_invite_count(&room_id, sender_user)
.await
@@ -303,12 +320,10 @@ async fn build_sync_events(
});
let knocked_rooms = services
.rooms
.state_cache
.rooms_knocked(sender_user)
.fold_default(async |mut knocked_rooms: BTreeMap<_, _>, (room_id, knock_state)| {
let knock_count = services
.rooms
.state_cache
.get_knock_count(&room_id, sender_user)
.await
@@ -439,7 +454,6 @@ async fn process_presence_updates(
.presence_since(since, Some(next_batch))
.filter(|(user_id, ..)| {
services
.rooms
.state_cache
.user_sees_user(syncing_user, user_id)
})
@@ -475,7 +489,6 @@ async fn handle_left_room(
filter: &FilterDefinition,
) -> Result<Option<LeftRoom>> {
let left_count = services
.rooms
.state_cache
.get_left_count(room_id, sender_user)
.await
@@ -504,11 +517,11 @@ async fn handle_left_room(
return Ok(None);
}
let is_not_found = services.rooms.metadata.exists(room_id).eq(&false);
let is_not_found = services.metadata.exists(room_id).eq(&false);
let is_disabled = services.rooms.metadata.is_disabled(room_id);
let is_disabled = services.metadata.is_disabled(room_id);
let is_banned = services.rooms.metadata.is_banned(room_id);
let is_banned = services.metadata.is_banned(room_id);
pin_mut!(is_not_found, is_disabled, is_banned);
if is_not_found.or(is_disabled).or(is_banned).await {
@@ -540,21 +553,19 @@ async fn handle_left_room(
prev_batch: Some(next_batch.to_string()),
events: Vec::new(),
},
state: RoomState { events: vec![event.into_format()] },
state: RoomState::Before(StateEvents { events: vec![event.into_format()] }),
}));
}
let mut left_state_events = Vec::new();
let since_shortstatehash = services
.rooms
.user
.get_token_shortstatehash(room_id, since);
let since_state_ids: HashMap<_, OwnedEventId> = since_shortstatehash
.map_ok(|since_shortstatehash| {
services
.rooms
.state_accessor
.state_full_ids(since_shortstatehash)
.map(Ok)
@@ -565,7 +576,6 @@ async fn handle_left_room(
.unwrap_or_default();
let Ok(left_event_id): Result<OwnedEventId> = services
.rooms
.state_accessor
.room_state_get_id(room_id, &StateEventType::RoomMember, sender_user.as_str())
.await
@@ -575,7 +585,6 @@ async fn handle_left_room(
};
let Ok(left_shortstatehash) = services
.rooms
.state_accessor
.pdu_shortstatehash(&left_event_id)
.await
@@ -585,14 +594,12 @@ async fn handle_left_room(
};
let mut left_state_ids: HashMap<_, _> = services
.rooms
.state_accessor
.state_full_ids(left_shortstatehash)
.collect()
.await;
let leave_shortstatekey = services
.rooms
.short
.get_or_create_shortstatekey(&StateEventType::RoomMember, sender_user.as_str())
.await;
@@ -602,7 +609,6 @@ async fn handle_left_room(
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
.rooms
.short
.get_statekey_from_short(shortstatekey)
.await?;
@@ -618,7 +624,7 @@ async fn handle_left_room(
continue;
}
let Ok(pdu) = services.rooms.timeline.get_pdu(&event_id).await else {
let Ok(pdu) = services.timeline.get_pdu(&event_id).await else {
error!("Pdu in state not found: {event_id}");
continue;
};
@@ -635,7 +641,7 @@ async fn handle_left_room(
prev_batch: Some(next_batch.to_string()),
events: Vec::new(), // and so we dont need to set this to empty vec
},
state: RoomState { events: left_state_events },
state: RoomState::Before(StateEvents { events: left_state_events }),
}))
}
@@ -658,17 +664,7 @@ async fn load_joined_room(
full_state: bool,
filter: &FilterDefinition,
) -> Result<(JoinedRoom, HashSet<OwnedUserId>, HashSet<OwnedUserId>)> {
let sincecount = PduCount::Normal(since);
let next_batchcount = PduCount::Normal(next_batch);
let current_shortstatehash = services
.rooms
.state
.get_room_shortstatehash(room_id)
.map_err(|_| err!(Database(error!("Room {room_id} has no state"))));
let since_shortstatehash = services
.rooms
.user
.get_token_shortstatehash(room_id, since)
.ok()
@@ -685,13 +681,12 @@ async fn load_joined_room(
services,
sender_user,
room_id,
sincecount,
Some(next_batchcount),
PduCount::Normal(since),
Some(PduCount::Normal(next_batch)),
timeline_limit,
);
let receipt_events = services
.rooms
.read_receipt
.readreceipts_since(room_id, since, Some(next_batch))
.filter_map(async |(read_user, _, edu)| {
@@ -704,29 +699,40 @@ async fn load_joined_room(
.collect::<HashMap<OwnedUserId, Raw<AnySyncEphemeralRoomEvent>>>()
.map(Ok);
let (current_shortstatehash, since_shortstatehash, (timeline_pdus, limited), receipt_events) =
try_join4(current_shortstatehash, since_shortstatehash, timeline, receipt_events)
let (since_shortstatehash, (timeline_pdus, limited, last_timeline_count), receipt_events) =
try_join3(since_shortstatehash, timeline, receipt_events)
.boxed()
.await?;
// State was changed after the cutoff for this sync; similar to other handlers.
if current_shortstatehash > next_batch {
// Transfer the since_shortstatehash not the current over to the next sync.
if let Some(since_shortstatehash) = since_shortstatehash {
let horizon_shortstatehash: OptionFuture<_> = timeline_pdus
.iter()
.map(at!(0))
.map(PduCount::into_unsigned)
.map(|shorteventid| {
services
.rooms
.user
.associate_token_shortstatehash(room_id, next_batch, since_shortstatehash)
.await;
}
.state_accessor
.get_shortstatehash(shorteventid)
})
.next()
.into();
return Ok((JoinedRoom::default(), HashSet::new(), HashSet::new()));
}
let current_shortstatehash = services
.state_accessor
.get_shortstatehash(last_timeline_count.into_unsigned())
.or_else(|_| services.state.get_room_shortstatehash(room_id));
let associate_token = services
.rooms
.user
.associate_token_shortstatehash(room_id, next_batch, current_shortstatehash);
let (horizon_shortstatehash, current_shortstatehash) =
join(horizon_shortstatehash, current_shortstatehash)
.boxed()
.await;
let current_shortstatehash = current_shortstatehash
.map_err(|_| err!(Database(error!("Room {room_id} has no state"))))?;
let associate_token =
services
.user
.associate_token_shortstatehash(room_id, next_batch, current_shortstatehash);
let lazy_loading_enabled = filter.room.state.lazy_load_options.is_enabled()
|| filter
@@ -747,12 +753,7 @@ async fn load_joined_room(
// Reset lazy loading because this is an initial sync
let lazy_load_reset: OptionFuture<_> = initial
.then(|| {
services
.rooms
.lazy_loading
.reset(lazy_loading_context)
})
.then(|| services.lazy_loading.reset(lazy_loading_context))
.into();
lazy_load_reset.await;
@@ -767,30 +768,15 @@ async fn load_joined_room(
.collect();
services
.rooms
.lazy_loading
.witness_retain(witness, lazy_loading_context)
})
.into();
let horizon_shortstatehash: OptionFuture<_> = timeline_pdus
.iter()
.map(at!(0))
.map(PduCount::into_unsigned)
.map(|shorteventid| {
services
.rooms
.state_accessor
.get_shortstatehash(shorteventid)
})
.next()
.into();
let last_notification_read: OptionFuture<_> = timeline_pdus
.is_empty()
.then(|| {
services
.rooms
.user
.last_notification_read(sender_user, room_id)
})
@@ -799,32 +785,26 @@ async fn load_joined_room(
let since_sender_member: OptionFuture<_> = since_shortstatehash
.map(|short| {
services
.rooms
.state_accessor
.state_get_content(short, &StateEventType::RoomMember, sender_user.as_str())
.ok()
})
.into();
let encrypted_room = services
.rooms
.state_accessor
.state_get(current_shortstatehash, &StateEventType::RoomEncryption, "")
.is_ok();
let encrypted_room = services.state_accessor.is_encrypted_room(room_id);
let last_privateread_update = services
.rooms
.read_receipt
.last_privateread_update(sender_user, room_id);
let (
witness,
(encrypted_room, last_privateread_update, last_notification_read),
(since_sender_member, horizon_shortstatehash, ()),
(witness, since_sender_member),
(encrypted_room, ()),
(last_privateread_update, last_notification_read),
) = join3(
witness,
join3(encrypted_room, last_privateread_update, last_notification_read),
join3(since_sender_member, horizon_shortstatehash, associate_token),
join(witness, since_sender_member),
join(encrypted_room, associate_token),
join(last_privateread_update, last_notification_read),
)
.boxed()
.await;
@@ -877,7 +857,6 @@ async fn load_joined_room(
let notification_count: OptionFuture<_> = send_notification_counts
.then(|| {
services
.rooms
.user
.notification_count(sender_user, room_id)
.map(TryInto::try_into)
@@ -888,7 +867,6 @@ async fn load_joined_room(
let highlight_count: OptionFuture<_> = send_notification_counts
.then(|| {
services
.rooms
.user
.highlight_count(sender_user, room_id)
.map(TryInto::try_into)
@@ -900,7 +878,6 @@ async fn load_joined_room(
.gt(&since)
.then(|| {
services
.rooms
.read_receipt
.private_read_get(room_id, sender_user)
.map(Result::ok)
@@ -908,7 +885,6 @@ async fn load_joined_room(
.into();
let typing_events = services
.rooms
.typing
.last_typing_update(room_id)
.and_then(async |count| {
@@ -1042,7 +1018,7 @@ async fn load_joined_room(
let joined_room = JoinedRoom {
account_data: RoomAccountData { events: account_data_events },
ephemeral: Ephemeral { events: edus },
state: RoomState { events: state_events },
state: RoomState::Before(StateEvents { events: state_events }),
summary: RoomSummary {
joined_member_count: joined_member_count.map(ruma_from_u64),
invited_member_count: invited_member_count.map(ruma_from_u64),
@@ -1096,7 +1072,6 @@ async fn calculate_state_changes<'a>(
let state_get_shorteventid = |user_id: &'a UserId| {
services
.rooms
.state_accessor
.state_get_shortid(
horizon_shortstatehash,
@@ -1122,7 +1097,6 @@ async fn calculate_state_changes<'a>(
.then(|| {
StreamExt::into_future(
services
.rooms
.state_accessor
.state_added((since_shortstatehash, horizon_shortstatehash))
.boxed(),
@@ -1134,7 +1108,6 @@ async fn calculate_state_changes<'a>(
.then(|| {
StreamExt::into_future(
services
.rooms
.state_accessor
.state_full_shortids(horizon_shortstatehash)
.expect_ok(),
@@ -1155,18 +1128,12 @@ async fn calculate_state_changes<'a>(
.chain(lazy_state_ids.stream())
.broad_filter_map(|shorteventid| {
services
.rooms
.short
.get_eventid_from_short(shorteventid)
.ok()
})
.broad_filter_map(async |event_id: OwnedEventId| {
services
.rooms
.timeline
.get_pdu(&event_id)
.ok()
.await
services.timeline.get_pdu(&event_id).ok().await
})
.collect::<Vec<_>>()
.boxed()
@@ -1198,7 +1165,6 @@ async fn lazy_filter(
shorteventid: ShortEventId,
) -> Option<ShortEventId> {
let (event_type, state_key) = services
.rooms
.short
.get_statekey_from_short(shortstatekey)
.await
@@ -1214,13 +1180,11 @@ async fn calculate_counts(
sender_user: &UserId,
) -> (Option<u64>, Option<u64>, Option<Vec<OwnedUserId>>) {
let joined_member_count = services
.rooms
.state_cache
.room_joined_count(room_id)
.unwrap_or(0);
let invited_member_count = services
.rooms
.state_cache
.room_invited_count(room_id)
.unwrap_or(0);
@@ -1243,7 +1207,6 @@ async fn calculate_heroes(
sender_user: &UserId,
) -> Vec<OwnedUserId> {
services
.rooms
.timeline
.all_pdus(sender_user, room_id)
.ready_filter(|(_, pdu)| pdu.kind == RoomMember)
@@ -1287,14 +1250,8 @@ async fn fold_hero(
}
let (is_invited, is_joined) = join(
services
.rooms
.state_cache
.is_invited(user_id, room_id),
services
.rooms
.state_cache
.is_joined(user_id, room_id),
services.state_cache.is_invited(user_id, room_id),
services.state_cache.is_joined(user_id, room_id),
)
.await;
@@ -1314,7 +1271,6 @@ async fn typings_event_for_user(
Ok(SyncEphemeralRoomEvent {
content: TypingEventContent {
user_ids: services
.rooms
.typing
.typing_users_for_user(room_id, sender_user)
.await?,
+896 -646
View File
File diff suppressed because it is too large Load Diff
+1 -11
View File
@@ -3,7 +3,7 @@
use ruma::api::client::thirdparty::get_protocols;
use tuwunel_core::Result;
use crate::{Ruma, RumaResponse};
use crate::Ruma;
/// # `GET /_matrix/client/r0/thirdparty/protocols`
///
@@ -14,13 +14,3 @@ pub(crate) async fn get_protocols_route(
// TODO
Ok(get_protocols::v3::Response { protocols: BTreeMap::new() })
}
/// # `GET /_matrix/client/unstable/thirdparty/protocols`
///
/// Same as `get_protocols_route`, except for some reason Element Android legacy
/// calls this
pub(crate) async fn get_protocols_route_unstable(
body: Ruma<get_protocols::v3::Request>,
) -> Result<RumaResponse<get_protocols::v3::Response>> {
get_protocols_route(body).await.map(RumaResponse)
}
-2
View File
@@ -32,13 +32,11 @@ pub(crate) async fn get_threads_route(
.unwrap_or_else(PduCount::max);
let threads: Vec<(PduCount, PduEvent)> = services
.rooms
.threads
.threads_until(body.sender_user(), &body.room_id, from, &body.include)
.take(limit)
.try_filter_map(async |(count, pdu)| {
Ok(services
.rooms
.state_accessor
.user_can_see_event(body.sender_user(), &body.room_id, &pdu.event_id)
.await
-3
View File
@@ -19,7 +19,6 @@ pub(crate) async fn create_typing_event_route(
}
if !services
.rooms
.state_cache
.is_joined(sender_user, &body.room_id)
.await
@@ -46,7 +45,6 @@ pub(crate) async fn create_typing_event_route(
.try_mul(1000)?,
);
services
.rooms
.typing
.typing_add(
sender_user,
@@ -59,7 +57,6 @@ pub(crate) async fn create_typing_event_route(
},
| _ => {
services
.rooms
.typing
.typing_remove(sender_user, &body.room_id)
.await?;
+58 -90
View File
@@ -1,5 +1,3 @@
use std::collections::BTreeMap;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use futures::StreamExt;
@@ -10,8 +8,8 @@
error::ErrorKind,
membership::mutual_rooms,
profile::{
delete_profile_key, delete_timezone_key, get_profile_key, get_timezone_key,
set_profile_key, set_timezone_key,
ProfileFieldName, ProfileFieldValue, delete_profile_field, delete_timezone_key,
get_profile_field, get_timezone_key, set_profile_field, set_timezone_key,
},
},
federation,
@@ -20,7 +18,6 @@
};
use tuwunel_core::{Err, Error, Result};
use super::{update_avatar_url, update_displayname};
use crate::Ruma;
/// # `GET /_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms`
@@ -47,7 +44,6 @@ pub(crate) async fn get_mutual_rooms_route(
}
let mutual_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.get_shared_rooms(sender_user, &body.user_id)
.map(ToOwned::to_owned)
@@ -123,80 +119,55 @@ pub(crate) async fn set_timezone_key_route(
/// Updates the profile key-value field of a user, as per MSC4133.
///
/// This also handles the avatar_url and displayname being updated.
pub(crate) async fn set_profile_key_route(
pub(crate) async fn set_profile_field_route(
State(services): State<crate::State>,
body: Ruma<set_profile_key::unstable::Request>,
) -> Result<set_profile_key::unstable::Response> {
body: Ruma<set_profile_field::v3::Request>,
) -> Result<set_profile_field::v3::Response> {
let sender_user = body.sender_user();
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));
}
if body.kv_pair.is_empty() {
return Err!(Request(BadJson(
"The key-value pair JSON body is empty. Use DELETE to delete a key"
)));
}
if body.kv_pair.len() > 1 {
// TODO: support PATCH or "recursively" adding keys in some sort
return Err!(Request(BadJson(
"This endpoint can only take one key-value pair at a time"
)));
}
let Some(profile_key_value) = body.kv_pair.get(&body.key_name) else {
return Err!(Request(BadJson(
"The key does not match the URL field key, or JSON body is empty (use DELETE)"
)));
};
if body
.kv_pair
.keys()
.any(|key| key.starts_with("u.") && !profile_key_value.is_string())
{
return Err!(Request(BadJson("u.* profile key fields must be strings")));
}
if body.kv_pair.keys().any(|key| key.len() > 128) {
if body.value.field_name().as_str().len() > 128 {
return Err!(Request(BadJson("Key names cannot be longer than 128 bytes")));
}
if body.key_name == "displayname" {
if body.value.field_name() == ProfileFieldName::DisplayName {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(Into::into)
.collect()
.await;
update_displayname(
&services,
&body.user_id,
Some(profile_key_value.to_string()),
&all_joined_rooms,
)
.await;
} else if body.key_name == "avatar_url" {
let mxc = ruma::OwnedMxcUri::from(profile_key_value.to_string());
services
.users
.update_displayname(
&body.user_id,
Some(body.value.value().to_string()),
&all_joined_rooms,
)
.await;
} else if body.value.field_name() == ProfileFieldName::AvatarUrl {
let mxc = ruma::OwnedMxcUri::from(body.value.value().to_string());
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(Into::into)
.collect()
.await;
update_avatar_url(&services, &body.user_id, Some(mxc), None, &all_joined_rooms).await;
services
.users
.update_avatar_url(&body.user_id, Some(mxc), None, &all_joined_rooms)
.await;
} else {
services.users.set_profile_key(
&body.user_id,
&body.key_name,
Some(profile_key_value.clone()),
body.value.field_name().as_str(),
Some(body.value.value().into_owned()),
);
}
@@ -208,7 +179,7 @@ pub(crate) async fn set_profile_key_route(
.await?;
}
Ok(set_profile_key::unstable::Response {})
Ok(set_profile_field::v3::Response {})
}
/// # `DELETE /_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}`
@@ -216,47 +187,44 @@ pub(crate) async fn set_profile_key_route(
/// Deletes the profile key-value field of a user, as per MSC4133.
///
/// This also handles the avatar_url and displayname being updated.
pub(crate) async fn delete_profile_key_route(
pub(crate) async fn delete_profile_field_route(
State(services): State<crate::State>,
body: Ruma<delete_profile_key::unstable::Request>,
) -> Result<delete_profile_key::unstable::Response> {
body: Ruma<delete_profile_field::v3::Request>,
) -> Result<delete_profile_field::v3::Response> {
let sender_user = body.sender_user();
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));
}
if body.kv_pair.len() > 1 {
// TODO: support PATCH or "recursively" adding keys in some sort
return Err!(Request(BadJson(
"This endpoint can only take one key-value pair at a time"
)));
}
if body.key_name == "displayname" {
if body.field == ProfileFieldName::DisplayName {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(Into::into)
.collect()
.await;
update_displayname(&services, &body.user_id, None, &all_joined_rooms).await;
} else if body.key_name == "avatar_url" {
services
.users
.update_displayname(&body.user_id, None, &all_joined_rooms)
.await;
} else if body.field == ProfileFieldName::AvatarUrl {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(Into::into)
.collect()
.await;
update_avatar_url(&services, &body.user_id, None, None, &all_joined_rooms).await;
services
.users
.update_avatar_url(&body.user_id, None, None, &all_joined_rooms)
.await;
} else {
services
.users
.set_profile_key(&body.user_id, &body.key_name, None);
.set_profile_key(&body.user_id, body.field.as_str(), None);
}
if services.config.allow_local_presence {
@@ -267,7 +235,7 @@ pub(crate) async fn delete_profile_key_route(
.await?;
}
Ok(delete_profile_key::unstable::Response {})
Ok(delete_profile_field::v3::Response {})
}
/// # `GET /_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/us.cloke.msc4175.tz`
@@ -337,12 +305,10 @@ pub(crate) async fn get_timezone_key_route(
///
/// - If user is on another server and we do not have a local copy already fetch
/// `timezone` over federation
pub(crate) async fn get_profile_key_route(
pub(crate) async fn get_profile_field_route(
State(services): State<crate::State>,
body: Ruma<get_profile_key::unstable::Request>,
) -> Result<get_profile_key::unstable::Response> {
let mut profile_key_value: BTreeMap<String, serde_json::Value> = BTreeMap::new();
body: Ruma<get_profile_field::v3::Request>,
) -> Result<get_profile_field::v3::Response> {
if !services.globals.user_is_local(&body.user_id) {
// Create and update our local copy of the user
if let Ok(response) = services
@@ -379,25 +345,29 @@ pub(crate) async fn get_profile_key_route(
.users
.set_timezone(&body.user_id, response.tz.clone());
match response.custom_profile_fields.get(&body.key_name) {
let profile_key_value: Option<ProfileFieldValue> = match response
.custom_profile_fields
.get(body.field.as_str())
{
| Some(value) => {
profile_key_value.insert(body.key_name.clone(), value.clone());
services.users.set_profile_key(
&body.user_id,
&body.key_name,
body.field.as_str(),
Some(value.clone()),
);
Some(ProfileFieldValue::new(body.field.as_str(), value.clone())?)
},
| _ => {
return Err!(Request(NotFound("The requested profile key does not exist.")));
},
}
};
if profile_key_value.is_empty() {
if profile_key_value.is_none() {
return Err!(Request(NotFound("The requested profile key does not exist.")));
}
return Ok(get_profile_key::unstable::Response { value: profile_key_value });
return Ok(get_profile_field::v3::Response { value: profile_key_value });
}
}
@@ -407,22 +377,20 @@ pub(crate) async fn get_profile_key_route(
return Err!(Request(NotFound("Profile was not found.")));
}
match services
let profile_key_value: Option<ProfileFieldValue> = match services
.users
.profile_key(&body.user_id, &body.key_name)
.profile_key(&body.user_id, body.field.as_str())
.await
{
| Ok(value) => {
profile_key_value.insert(body.key_name.clone(), value);
},
| Ok(value) => Some(ProfileFieldValue::new(body.field.as_str(), value)?),
| _ => {
return Err!(Request(NotFound("The requested profile key does not exist.")));
},
}
};
if profile_key_value.is_empty() {
if profile_key_value.is_none() {
return Err!(Request(NotFound("The requested profile key does not exist.")));
}
Ok(get_profile_key::unstable::Response { value: profile_key_value })
Ok(get_profile_field::v3::Response { value: profile_key_value })
}
-3
View File
@@ -57,13 +57,11 @@ pub(crate) async fn search_users_route(
}
let user_in_public_room = services
.rooms
.state_cache
.rooms_joined(&user_id)
.map(ToOwned::to_owned)
.broad_any(async |room_id| {
services
.rooms
.state_accessor
.get_join_rules(&room_id)
.map(|rule| matches!(rule, JoinRule::Public))
@@ -71,7 +69,6 @@ pub(crate) async fn search_users_route(
});
let user_sees_user = services
.rooms
.state_cache
.user_sees_user(sender_user, &user_id);
+16
View File
@@ -0,0 +1,16 @@
use ruma::{RoomId, UserId};
use tuwunel_core::{Err, Result, warn};
use tuwunel_service::Services;
pub(crate) async fn invite_check(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
) -> Result {
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
warn!("{sender_user} is not an admin and attempted to send an invite to {room_id}");
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
Ok(())
}
+11 -14
View File
@@ -1,12 +1,9 @@
use axum::{Json, extract::State, response::IntoResponse};
use ruma::api::client::{
discovery::{
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
discover_support::{self, Contact},
},
error::ErrorKind,
use ruma::api::client::discovery::{
discover_homeserver::{self, HomeserverInfo},
discover_support::{self, Contact},
};
use tuwunel_core::{Error, Result};
use tuwunel_core::{Err, Result};
use crate::Ruma;
@@ -19,13 +16,12 @@ pub(crate) async fn well_known_client(
) -> Result<discover_homeserver::Response> {
let client_url = match services.server.config.well_known.client.as_ref() {
| Some(url) => url.to_string(),
| None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
| None => return Err!(Request(NotFound("Not found."))),
};
Ok(discover_homeserver::Response {
homeserver: HomeserverInfo { base_url: client_url.clone() },
homeserver: HomeserverInfo { base_url: client_url },
identity_server: None,
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
tile_server: None,
})
}
@@ -54,7 +50,7 @@ pub(crate) async fn well_known_support(
// support page or role must be either defined for this to be valid
if support_page.is_none() && role.is_none() {
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
return Err!(Request(NotFound("Not found.")));
}
let email_address = services
@@ -63,6 +59,7 @@ pub(crate) async fn well_known_support(
.well_known
.support_email
.clone();
let matrix_id = services
.server
.config
@@ -72,7 +69,7 @@ pub(crate) async fn well_known_support(
// if a role is specified, an email address or matrix id is required
if role.is_some() && (email_address.is_none() && matrix_id.is_none()) {
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
return Err!(Request(NotFound("Not found.")));
}
// TODO: support defining multiple contacts in the config
@@ -86,7 +83,7 @@ pub(crate) async fn well_known_support(
// support page or role+contacts must be either defined for this to be valid
if contacts.is_empty() && support_page.is_none() {
return Err(Error::BadRequest(ErrorKind::NotFound, "Not found."));
return Err!(Request(NotFound("Not found.")));
}
Ok(discover_support::Response { contacts, support_page })
@@ -103,7 +100,7 @@ pub(crate) async fn syncv3_client_server_json(
| Some(url) => url.to_string(),
| None => match services.server.config.well_known.server.as_ref() {
| Some(url) => url.to_string(),
| None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
| None => return Err!(Request(NotFound("Not found."))),
},
};
+1 -1
View File
@@ -1,4 +1,4 @@
#![type_length_limit = "65536"] //TODO: reduce me
#![type_length_limit = "163840"] //TODO: REDUCE ME
#![allow(clippy::toplevel_ref_arg)]
pub mod client;
+6 -6
View File
@@ -16,16 +16,18 @@
use tuwunel_core::{Server, err};
use self::handler::RouterExt;
pub(super) use self::{args::Args as Ruma, response::RumaResponse, state::State};
pub(super) use self::{
args::Args as Ruma, auth::auth_uiaa, response::RumaResponse, state::State,
};
use crate::{client, server};
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
let config = &server.config;
let mut router = router
.ruma_route(&client::get_timezone_key_route)
.ruma_route(&client::get_profile_key_route)
.ruma_route(&client::set_profile_key_route)
.ruma_route(&client::delete_profile_key_route)
.ruma_route(&client::get_profile_field_route)
.ruma_route(&client::set_profile_field_route)
.ruma_route(&client::delete_profile_field_route)
.ruma_route(&client::set_timezone_key_route)
.ruma_route(&client::delete_timezone_key_route)
.ruma_route(&client::appservice_ping)
@@ -116,8 +118,6 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::search_users_route)
.ruma_route(&client::get_member_events_route)
.ruma_route(&client::get_protocols_route)
.route("/_matrix/client/unstable/thirdparty/protocols",
get(client::get_protocols_route_unstable))
.ruma_route(&client::send_message_event_route)
.ruma_route(&client::send_state_event_for_key_route)
.ruma_route(&client::get_state_events_route)
+7 -1
View File
@@ -82,7 +82,13 @@ impl<T> FromRequest<State, Body> for Args<T>
{
type Rejection = Error;
#[tracing::instrument(name = "ar", level = "debug", skip(services), ret, err)]
#[tracing::instrument(
name = "ar",
level = "debug",
skip(services),
err(level = "debug")
ret,
)]
async fn from_request(
request: hyper::Request<Body>,
services: &State,
+32 -175
View File
@@ -1,10 +1,13 @@
mod appservice;
mod server;
mod uiaa;
use std::{fmt::Debug, time::SystemTime};
use axum::RequestPartsExt;
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
typed_header::TypedHeaderRejectionReason,
};
use futures::{
TryFutureExt,
@@ -15,29 +18,26 @@
pin_mut,
};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId,
api::{
AuthScheme, IncomingRequest, Metadata,
client::{
directory::get_public_rooms,
error::ErrorKind,
profile::{
get_avatar_url, get_display_name, get_profile, get_profile_key, get_timezone_key,
get_avatar_url, get_display_name, get_profile, get_profile_field,
get_timezone_key,
},
voip::get_turn_server_info,
},
federation::{authentication::XMatrix, openid::get_openid_userinfo},
federation::openid::get_openid_userinfo,
},
};
use tuwunel_core::{
Err, Error, Result, debug_error, err, is_less_than, utils::result::LogDebugErr, warn,
};
use tuwunel_service::{
Services,
appservice::RegistrationInfo,
server_keys::{PubKeyMap, PubKeys},
};
use tuwunel_core::{Err, Error, Result, is_less_than, utils::result::LogDebugErr};
use tuwunel_service::{Services, appservice::RegistrationInfo};
pub(crate) use self::uiaa::auth_uiaa;
use self::{appservice::auth_appservice, server::auth_server};
use super::request::Request;
enum Token {
@@ -60,8 +60,8 @@ pub(super) struct Auth {
#[tracing::instrument(
level = "trace",
skip(services, request, json_body),
ret,
err
err(level = "debug"),
ret
)]
pub(super) async fn auth(
services: &Services,
@@ -69,7 +69,10 @@ pub(super) async fn auth(
json_body: Option<&CanonicalJsonValue>,
metadata: &Metadata,
) -> Result<Auth> {
use AuthScheme::{AccessToken, AccessTokenOptional, AppserviceToken, ServerSignatures};
use AuthScheme::{
AccessToken, AccessTokenOptional, AppserviceToken, AppserviceTokenOptional,
ServerSignatures,
};
use Error::BadRequest;
use ErrorKind::UnknownToken;
use Token::{Appservice, Expired, Invalid, User};
@@ -129,7 +132,7 @@ pub(super) async fn auth(
| (AccessToken, Appservice(info)) => Ok(auth_appservice(services, request, info).await?),
| (AccessToken, Token::None) => match metadata {
| (AccessToken | AppserviceToken, Token::None) => match metadata {
| &get_turn_server_info::v3::Request::METADATA
if services.server.config.turn_allow_guests =>
Ok(Auth::default()),
@@ -137,22 +140,25 @@ pub(super) async fn auth(
| _ => Err!(Request(MissingToken("Missing access token."))),
},
| (AccessToken | AccessTokenOptional | AuthScheme::None, User(user)) => Ok(Auth {
| (
AccessToken | AccessTokenOptional | AppserviceTokenOptional | AuthScheme::None,
User(user),
) => Ok(Auth {
sender_user: Some(user.0),
sender_device: Some(user.1),
_expires_at: user.2,
..Auth::default()
}),
//TODO: add AppserviceTokenOptional
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Appservice(info)) =>
Ok(Auth {
appservice_info: Some(*info),
..Auth::default()
}),
| (
AccessTokenOptional | AppserviceTokenOptional | AppserviceToken | AuthScheme::None,
Appservice(info),
) => Ok(Auth {
appservice_info: Some(*info),
..Auth::default()
}),
//TODO: add AppserviceTokenOptional
| (AccessTokenOptional | AppserviceToken | AuthScheme::None, Token::None) =>
| (AccessTokenOptional | AppserviceTokenOptional | AuthScheme::None, Token::None) =>
Ok(Auth::default()),
}
}
@@ -166,7 +172,7 @@ fn check_auth_still_required(services: &Services, metadata: &Metadata, token: &T
match metadata {
| &get_profile::v3::Request::METADATA
| &get_profile_key::unstable::Request::METADATA
| &get_profile_field::v3::Request::METADATA
| &get_display_name::v3::Request::METADATA
| &get_avatar_url::v3::Request::METADATA
| &get_timezone_key::unstable::Request::METADATA
@@ -216,152 +222,3 @@ async fn find_token(services: &Services, token: Option<&str>) -> Result<Token> {
| _ => Ok(Token::Invalid),
}
}
async fn auth_appservice(
services: &Services,
request: &Request,
info: Box<RegistrationInfo>,
) -> Result<Auth> {
let user_id_default = || {
UserId::parse_with_server_name(
info.registration.sender_localpart.as_str(),
services.globals.server_name(),
)
};
let Ok(user_id) = request
.query
.user_id
.clone()
.map_or_else(user_id_default, OwnedUserId::parse)
else {
return Err!(Request(InvalidUsername("Username is invalid.")));
};
if !info.is_user_match(&user_id) {
return Err!(Request(Exclusive("User is not in namespace.")));
}
Ok(Auth {
sender_user: Some(user_id),
appservice_info: Some(*info),
..Auth::default()
})
}
async fn auth_server(
services: &Services,
request: &mut Request,
body: Option<&CanonicalJsonValue>,
) -> Result<Auth> {
type Member = (String, CanonicalJsonValue);
type Object = CanonicalJsonObject;
type Value = CanonicalJsonValue;
let x_matrix = parse_x_matrix(request).await?;
auth_server_checks(services, &x_matrix)?;
let destination = services.globals.server_name();
let origin = &x_matrix.origin;
let signature_uri = request
.parts
.uri
.path_and_query()
.expect("all requests have a path")
.to_string();
let signature: [Member; 1] =
[(x_matrix.key.as_str().into(), Value::String(x_matrix.sig.to_string()))];
let signatures: [Member; 1] = [(origin.as_str().into(), Value::Object(signature.into()))];
let authorization: Object = if let Some(body) = body.cloned() {
let authorization: [Member; 6] = [
("content".into(), body),
("destination".into(), Value::String(destination.into())),
("method".into(), Value::String(request.parts.method.as_str().into())),
("origin".into(), Value::String(origin.as_str().into())),
("signatures".into(), Value::Object(signatures.into())),
("uri".into(), Value::String(signature_uri)),
];
authorization.into()
} else {
let authorization: [Member; 5] = [
("destination".into(), Value::String(destination.into())),
("method".into(), Value::String(request.parts.method.as_str().into())),
("origin".into(), Value::String(origin.as_str().into())),
("signatures".into(), Value::Object(signatures.into())),
("uri".into(), Value::String(signature_uri)),
];
authorization.into()
};
let key = services
.server_keys
.get_verify_key(origin, &x_matrix.key)
.await
.map_err(|e| err!(Request(Forbidden(warn!("Failed to fetch signing keys: {e}")))))?;
let keys: PubKeys = [(x_matrix.key.to_string(), 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}");
if request.parts.uri.to_string().contains('@') {
warn!(
"Request uri contained '@' character. Make sure your reverse proxy gives \
tuwunel the raw uri (apache: use nocanon)"
);
}
return Err!(Request(Forbidden("Failed to verify X-Matrix signatures.")));
}
Ok(Auth {
origin: origin.to_owned().into(),
..Auth::default()
})
}
fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result {
if !services.server.config.allow_federation {
return Err!(Config("allow_federation", "Federation is disabled."));
}
let destination = services.globals.server_name();
if x_matrix.destination.as_deref() != Some(destination) {
return Err!(Request(Forbidden("Invalid destination.")));
}
let origin = &x_matrix.origin;
if services
.config
.forbidden_remote_server_names
.is_match(origin.host())
{
return Err!(Request(Forbidden(debug_warn!(
"Federation requests from {origin} denied."
))));
}
Ok(())
}
async fn parse_x_matrix(request: &mut Request) -> Result<XMatrix> {
let TypedHeader(Authorization(x_matrix)) = request
.parts
.extract::<TypedHeader<Authorization<XMatrix>>>()
.await
.map_err(|e| {
let msg = match e.reason() {
| TypedHeaderRejectionReason::Missing => "Missing Authorization header.",
| TypedHeaderRejectionReason::Error(_) => "Invalid X-Matrix signatures.",
| _ => "Unknown header-related error",
};
err!(Request(Forbidden(warn!("{msg}: {e}"))))
})?;
Ok(x_matrix)
}
+37
View File
@@ -0,0 +1,37 @@
use ruma::{OwnedUserId, UserId};
use tuwunel_core::{Err, Result};
use tuwunel_service::{Services, appservice::RegistrationInfo};
use super::{Auth, Request};
pub(super) async fn auth_appservice(
services: &Services,
request: &Request,
info: Box<RegistrationInfo>,
) -> Result<Auth> {
let user_id_default = || {
UserId::parse_with_server_name(
info.registration.sender_localpart.as_str(),
services.globals.server_name(),
)
};
let Ok(user_id) = request
.query
.user_id
.clone()
.map_or_else(user_id_default, OwnedUserId::parse)
else {
return Err!(Request(InvalidUsername("Username is invalid.")));
};
if !info.is_user_match(&user_id) {
return Err!(Request(Exclusive("User is not in namespace.")));
}
Ok(Auth {
sender_user: Some(user_id),
appservice_info: Some(*info),
..Auth::default()
})
}
+129
View File
@@ -0,0 +1,129 @@
use axum::RequestPartsExt;
use axum_extra::{TypedHeader, headers::Authorization, typed_header::TypedHeaderRejectionReason};
use ruma::{CanonicalJsonObject, CanonicalJsonValue, api::federation::authentication::XMatrix};
use tuwunel_core::{Err, Result, debug_error, err, warn};
use tuwunel_service::{
Services,
server_keys::{PubKeyMap, PubKeys},
};
use super::{Auth, Request};
pub(super) async fn auth_server(
services: &Services,
request: &mut Request,
body: Option<&CanonicalJsonValue>,
) -> Result<Auth> {
type Member = (String, CanonicalJsonValue);
type Object = CanonicalJsonObject;
type Value = CanonicalJsonValue;
let x_matrix = parse_x_matrix(request).await?;
auth_server_checks(services, &x_matrix)?;
let destination = services.globals.server_name();
let origin = &x_matrix.origin;
let signature_uri = request
.parts
.uri
.path_and_query()
.expect("all requests have a path")
.to_string();
let signature: [Member; 1] =
[(x_matrix.key.as_str().into(), Value::String(x_matrix.sig.to_string()))];
let signatures: [Member; 1] = [(origin.as_str().into(), Value::Object(signature.into()))];
let authorization: Object = if let Some(body) = body.cloned() {
let authorization: [Member; 6] = [
("content".into(), body),
("destination".into(), Value::String(destination.into())),
("method".into(), Value::String(request.parts.method.as_str().into())),
("origin".into(), Value::String(origin.as_str().into())),
("signatures".into(), Value::Object(signatures.into())),
("uri".into(), Value::String(signature_uri)),
];
authorization.into()
} else {
let authorization: [Member; 5] = [
("destination".into(), Value::String(destination.into())),
("method".into(), Value::String(request.parts.method.as_str().into())),
("origin".into(), Value::String(origin.as_str().into())),
("signatures".into(), Value::Object(signatures.into())),
("uri".into(), Value::String(signature_uri)),
];
authorization.into()
};
let key = services
.server_keys
.get_verify_key(origin, &x_matrix.key)
.await
.map_err(|e| {
err!(Request(Forbidden(debug_warn!("Failed to fetch signing keys: {e}"))))
})?;
let keys: PubKeys = [(x_matrix.key.to_string(), 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}");
if request.parts.uri.to_string().contains('@') {
warn!(
"Request uri contained '@' character. Make sure your reverse proxy gives \
tuwunel the raw uri (apache: use nocanon)"
);
}
return Err!(Request(Forbidden("Failed to verify X-Matrix signatures.")));
}
Ok(Auth {
origin: origin.to_owned().into(),
..Auth::default()
})
}
fn auth_server_checks(services: &Services, x_matrix: &XMatrix) -> Result {
if !services.server.config.allow_federation {
return Err!(Config("allow_federation", "Federation is disabled."));
}
let destination = services.globals.server_name();
if x_matrix.destination.as_deref() != Some(destination) {
return Err!(Request(Forbidden("Invalid destination.")));
}
let origin = &x_matrix.origin;
if services
.config
.forbidden_remote_server_names
.is_match(origin.host())
{
return Err!(Request(Forbidden(debug_warn!(
"Federation requests from {origin} denied."
))));
}
Ok(())
}
async fn parse_x_matrix(request: &mut Request) -> Result<XMatrix> {
let TypedHeader(Authorization(x_matrix)) = request
.parts
.extract::<TypedHeader<Authorization<XMatrix>>>()
.await
.map_err(|e| {
let msg = match e.reason() {
| TypedHeaderRejectionReason::Missing => "Missing Authorization header.",
| TypedHeaderRejectionReason::Error(_) => "Invalid X-Matrix signatures.",
| _ => "Unknown header-related error",
};
err!(Request(Forbidden(debug_warn!("{msg}: {e}"))))
})?;
Ok(x_matrix)
}
+81
View File
@@ -0,0 +1,81 @@
use ruma::{
CanonicalJsonValue, OwnedUserId,
api::{
IncomingRequest,
client::uiaa::{AuthData, AuthFlow, AuthType, Jwt, UiaaInfo},
},
};
use tuwunel_core::{Err, Error, Result, err, utils};
use tuwunel_service::{Services, uiaa::SESSION_ID_LENGTH};
use crate::{Ruma, client::jwt};
pub(crate) async fn auth_uiaa<T>(services: &Services, body: &Ruma<T>) -> Result<OwnedUserId>
where
T: IncomingRequest + Send + Sync,
{
let flows = [
AuthFlow::new([AuthType::Password].into()),
AuthFlow::new([AuthType::Jwt].into()),
];
let mut uiaainfo = UiaaInfo {
flows: flows.into(),
..Default::default()
};
match body
.json_body
.as_ref()
.and_then(CanonicalJsonValue::as_object)
.and_then(|body| body.get("auth"))
.cloned()
.map(CanonicalJsonValue::into)
.map(serde_json::from_value)
.transpose()?
{
| Some(AuthData::Jwt(Jwt { ref token, .. })) => {
let sender_user = jwt::validate_user(services, token)?;
if !services.users.exists(&sender_user).await {
return Err!(Request(NotFound("User {sender_user} is not registered.")));
}
// Success!
Ok(sender_user)
},
| Some(ref auth) => {
let sender_user = body
.sender_user
.as_deref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
Ok(sender_user.to_owned())
},
| _ => match body.json_body {
| Some(ref json) => {
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, body.sender_device(), &uiaainfo, json);
Err(Error::Uiaa(uiaainfo))
},
| _ => Err!(Request(NotJson("JSON body is not valid"))),
},
}
}
+2 -6
View File
@@ -46,7 +46,6 @@ pub(crate) async fn get_backfill_route(
.stream()
.filter_map(|event_id| {
services
.rooms
.timeline
.get_pdu_count(event_id)
.map(Result::ok)
@@ -60,13 +59,11 @@ pub(crate) async fn get_backfill_route(
origin: services.globals.server_name().to_owned(),
pdus: services
.rooms
.timeline
.pdus_rev(None, &body.room_id, Some(from.saturating_add(1)))
.try_take(limit)
.try_filter_map(async |(_, pdu)| {
Ok(services
.rooms
.state_accessor
.server_can_see_event(body.origin(), &pdu.room_id, &pdu.event_id)
.await
@@ -74,7 +71,6 @@ pub(crate) async fn get_backfill_route(
})
.try_filter_map(async |pdu| {
Ok(services
.rooms
.timeline
.get_pdu_json(&pdu.event_id)
.await
@@ -82,8 +78,8 @@ pub(crate) async fn get_backfill_route(
})
.and_then(|pdu| {
services
.sending
.convert_to_outgoing_federation_event(pdu)
.federation
.format_pdu_into(pdu, None)
.map(Ok)
})
.try_collect()
+2 -3
View File
@@ -16,7 +16,6 @@ pub(crate) async fn get_event_route(
body: Ruma<get_event::v1::Request>,
) -> Result<get_event::v1::Response> {
let event = services
.rooms
.timeline
.get_pdu_json(&body.event_id)
.await
@@ -42,8 +41,8 @@ pub(crate) async fn get_event_route(
origin: services.globals.server_name().to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
pdu: services
.sending
.convert_to_outgoing_federation_event(event)
.federation
.format_pdu_into(event, None)
.await,
})
}
+2 -15
View File
@@ -30,7 +30,6 @@ pub(crate) async fn get_event_authorization_route(
.await?;
let event = services
.rooms
.timeline
.get_pdu_json(&body.event_id)
.await
@@ -45,23 +44,11 @@ pub(crate) async fn get_event_authorization_route(
.map_err(|_| Error::bad_database("Invalid room_id in event in database."))?;
let auth_chain = services
.rooms
.auth_chain
.event_ids_iter(room_id, once(body.event_id.borrow()))
.ready_filter_map(Result::ok)
.filter_map(async |id| {
services
.rooms
.timeline
.get_pdu_json(&id)
.await
.ok()
})
.then(|pdu| {
services
.sending
.convert_to_outgoing_federation_event(pdu)
})
.filter_map(async |id| services.timeline.get_pdu_json(&id).await.ok())
.then(|pdu| services.federation.format_pdu_into(pdu, None))
.collect()
.await;

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