Compare commits

..

242 Commits

Author SHA1 Message Date
Ginger b0bb6e9fa5 fix: Add unstable feature flag 2026-05-13 13:43:15 -04:00
Ginger 03a178a794 chore: News fragment 2026-05-13 13:41:10 -04:00
Ginger 0eeb4243be feat: Add support for MSC4466 2026-05-13 13:40:16 -04:00
Ginger 6f83925a4f fix: Use correct service name in membership service 2026-05-13 08:53:15 -04:00
31a05b9c e349dd284f chore: changelog 2026-05-12 14:18:35 +00:00
31a05b9c c57fe66d8d fix: query account-data account-data-get output 2026-05-12 14:18:35 +00:00
Ginger ff28fd0927 fix: Disable debugging parameter in Askama template 2026-05-12 14:17:23 +00:00
Ginger 7307f2dc80 fix: Remove deprecated MatrixRTC focus config option 2026-05-12 14:17:23 +00:00
Renovate Bot 6f56b665e7 chore(deps): update ruma digest to 9c9dccc 2026-05-12 14:17:23 +00:00
Renovate Bot 7018ce4180 chore(deps): update node-patch-updates to v2.0.11 2026-05-12 05:02:17 +00:00
timedout 10dd8bebfe fix: Don't advertise stable MSC2666
Turns out ruma doesn't have the stable definition yet, will need a version bump.

Reverts 088fa3e725
2026-05-11 23:27:00 +01:00
Henry-Hiles 1658b3bf6c chore: Add changelog 2026-05-10 03:48:25 +00:00
Henry-Hiles 088fa3e725 fix: Use stable ID for Mutual Rooms support 2026-05-10 03:48:25 +00:00
Henry-Hiles 4694186c97 fix: Link to community guidelines in CoC file 2026-05-09 16:53:03 -04:00
Renovate Bot a5c61d5137 chore(deps): update pre-commit hook crate-ci/typos to v1.46.1 2026-05-09 05:03:19 +00:00
Ginger 39a882c4a1 chore: Clippy fixes 2026-05-08 12:41:57 -04:00
Ginger f091d3a732 fix: Correctly check for local users' existence 2026-05-08 11:48:20 -04:00
nex ebf9a08cd1 fix: Correct typo that prevented state compressor service being loaded 2026-05-08 03:10:28 +00:00
timedout 4fef0a7ff2 chore: Publish admin announcement 2026-05-08 00:20:23 +01:00
timedout 2f37b446bc chore: Bump version 2026-05-07 22:27:44 +01:00
timedout 6185841b6a fix: Restore event auth check 4 in v12 rooms
Reviewed-By: Jacob Taylor <jacob@explodie.org>
2026-05-07 21:10:32 +01:00
Renovate Bot 3e0d4b066e chore(deps): update dependency cargo-bins/cargo-binstall to v1.19.1 2026-05-07 16:10:45 +00:00
Ginger 0d2eeed567 refactor: Move room joining logic into a new service 2026-05-06 14:01:50 -04:00
31a05b9c b296720540 chore: alter wording 2026-05-06 17:25:45 +00:00
31a05b9c d600aed8db chore: changelog 2026-05-06 17:25:45 +00:00
31a05b9c 9724953b5e feat: admin commands for mass-rejecting invites 2026-05-06 17:25:45 +00:00
stratself 1605176956 docs(perf): Improve introduction wording 2026-05-06 09:03:14 +00:00
stratself 2b0aedf5fd chore: Fix changelog newline 2026-05-06 09:03:14 +00:00
stratself c78c431703 fix(docs): Small wording fixes in perf tuning page 2026-05-06 09:03:13 +00:00
stratself 49b48b857d docs(perf): Change compression warning to SHOULD and better notes on
trusted_servers verification
2026-05-06 09:03:13 +00:00
stratself bf1e42b225 docs(perf): Remove section on .well-known
It is not a clear performance gain, and should be added
later in delegation.mdx
2026-05-06 09:03:13 +00:00
stratself ec76a234db docs(perf): Various changes from feedback
* Only recommend turning off all presences
* Add example trusted_servers and notice to vet them
* Add explained benefits of HTTP/3
* Some grammar nits
2026-05-06 09:03:13 +00:00
stratself 091514e9f9 docs(perf): Grammar/wording edits from feedback
Also combined the introductory paragraphs into one
2026-05-06 09:03:13 +00:00
stratself 789ad499f7 fix: Grammar + wording for perftuning page from feedback 2026-05-06 09:03:13 +00:00
stratself 1e6eaa4337 fix(docs): Remove sysctl and limits.conf recs from perf tuning 2026-05-06 09:03:13 +00:00
stratself de97900b07 chore(docs): Add performance tuning navigation 2026-05-06 09:03:13 +00:00
stratself cb68a3d0ae docs(perf): Rewrite notary tuning stuff and add section on HTTP/3
Some more wording fixes too
2026-05-06 09:03:12 +00:00
stratself d3852abe51 docs(perf): Add .well-known, nofiles, and sysctl recs + some copyedits 2026-05-06 09:03:12 +00:00
stratself 15845b1c55 chore: Add changelog for #1498 2026-05-06 09:03:12 +00:00
stratself f7d558baa6 docs(perf-tuning): Add notaries batch size advise + copyedits from comments
performance.md is now performance.mdx
2026-05-06 09:03:11 +00:00
stratself edd80b2600 docs(perf-tuning): Add bottommost compression section and other changes
* docs: Move typing/readmarks disabling section up, and separate
  outbound sending disablement from total disablement of features
* docs: Change introduction tone to "get more out of your server",
  remove wrong notion of tuned for small instances
2026-05-06 09:03:11 +00:00
stratself 03eab32c27 docs: Add initial performance tuning guidance 2026-05-06 09:03:10 +00:00
Ian Wagner 636de8a708 docs: Add link to matrix.org federation tester 2026-05-06 07:30:27 +00:00
Ginger e212c91ebf fix: Address review comments 2026-05-05 13:35:35 -04:00
Ginger 83f3314f08 chore: News fragment 2026-05-05 09:10:51 -04:00
Ginger 8c2cf67783 refactor: Remove support for guest user registration 2026-05-05 09:09:48 -04:00
Ginger 7436e2f4e1 chore: Update admin command docs 2026-05-05 09:05:43 -04:00
timedout 9ba406761b chore: Regenerate example config 2026-05-04 21:14:22 +01:00
nex 97f49d6357 chore: Remove news fragment checkbox from PR template
Requiring them is now handled by an action
2026-05-04 20:06:40 +00:00
new-years-eve 1a49bc6f87 docs: Add changelog 2026-05-04 20:05:26 +00:00
new-years-eve 833216256b feat: Add support for fallback keys
Fallback keys can be provided by client devices to be used in case the
supply of one-time keys run out. The server will store one fallback key
per user, per device, per algorithm. The server will keep track of
whether this fallback key has been used or not.

The  /keys/claim endpoint now provides a fallback key
if no one-time key is found

The /keys/upload endpoint now accepts fallback keys

The /sync endpoint now informs the client of the algorithms for which it
has an unused fallback key in stock.
2026-05-04 20:05:26 +00:00
new-years-eve 5fa3087401 feat: Implement serialization/deserialization for booleans 2026-05-04 20:05:26 +00:00
Ginger e95c0bd53f chore: News fragment 2026-05-04 11:28:11 -04:00
Ginger 52d1ed24a9 refactor: Remove LDAP support 2026-05-04 11:27:47 -04:00
timedout 4c1638e495 style: Resolve end-of-line lint failure 2026-05-04 14:57:21 +01:00
grgergo 3f69cf8ed7 chore: update example config 2026-05-04 14:57:21 +01:00
grgergo 560a615c29 chore: news fragment for #1706 2026-05-04 14:57:21 +01:00
grgergo 2e19310a87 clarify max_request_size limiting federation
Signed-off-by: grgergo <csakbek@freemail.hu>
2026-05-04 14:57:21 +01:00
Renovate Bot 81c5c6b2bc chore(deps): update sentry-rust monorepo to 0.48.0 2026-05-03 14:41:05 +00:00
Renovate Bot 73d8462ace chore(deps): update rust crate askama to 0.16.0 2026-05-03 14:40:44 +00:00
Renovate Bot 8b5fda1fb5 chore(deps): lock file maintenance 2026-05-03 14:40:16 +00:00
Ginger 6f9b4a989e fix: Update ctor macro arguments 2026-05-03 14:39:30 +00:00
Renovate Bot fe0d83d447 chore(deps): update rust crate ctor to 0.13.0 2026-05-03 14:39:30 +00:00
Renovate Bot 37dccdbeb0 chore(deps): update https://github.com/taiki-e/install-action digest to b5fddbb 2026-05-03 12:30:32 +00:00
Renovate Bot 1060adc670 chore(deps): update dependency cargo-bins/cargo-binstall to v1.19.0 2026-05-03 05:04:01 +00:00
Renovate Bot d963b89a07 chore(deps): update rust-zerover-patch-updates 2026-05-01 17:40:13 +00:00
Ginger 680c972b44 chore: News fragment 2026-05-01 13:17:00 -04:00
Ginger 88b59eb053 fix: Include target user's membership when building stripped state 2026-05-01 13:15:55 -04:00
timedout 4a99de0d28 fix: Store incoming federated invite membership events correctly
Co-Authored-By: Ginger <ginger@gingershaped.computer>
Reviewed-By: Ginger <ginger@gingershaped.computer>
2026-05-01 14:49:27 +01:00
Renovate Bot 0e1f0683c6 chore(deps): update pre-commit hook crate-ci/typos to v1.46.0 2026-05-01 05:04:01 +00:00
Renovate Bot cec4abc7cd chore(deps): update ruma digest to 5742fec 2026-04-30 05:05:10 +00:00
Ginger e6cae5b8ed fix: Fix membership check in kick handler 2026-04-29 12:45:15 -04:00
Ginger 02ccf64d2e fix: Properly create room summary stripped state 2026-04-29 12:44:57 -04:00
Renovate Bot 4d4d875231 chore(deps): update node-patch-updates to v2.0.10 2026-04-29 14:14:58 +00:00
Renovate Bot cdf05b9a8b chore(deps): update rust crate serde-saphyr to 0.0.25 2026-04-29 14:14:17 +00:00
Ginger 9491be928d fix: Fix panic when creating rooms 2026-04-29 09:26:13 -04:00
Ginger 049babc7ca fix: Fix appservice authentication 2026-04-29 09:09:09 -04:00
Ginger 7b99757337 chore: Update news fragment 2026-04-28 09:16:57 -04:00
Ginger d09de005e3 refactor: Drop unused MSC3843 endpoint definition 2026-04-28 09:16:57 -04:00
Ginger e34fd76dc0 fix: Re-add support for MSC4293 2026-04-28 09:16:57 -04:00
Ginger 72dfe579ec docs: Remove ruwuma mention from development.md 2026-04-28 09:16:57 -04:00
Ginger cfae9a34f4 fix: Panic on PL content deserialization failures 2026-04-28 09:16:57 -04:00
Ginger 0a4808ea79 fix: Add stable routes for admin suspension endpoints 2026-04-28 09:16:57 -04:00
Ginger a9a18fc5f0 fix: Re-add support for custom room IDs 2026-04-28 09:16:57 -04:00
Ginger c1434c7935 refactor: Remove mystery initial state hack 2026-04-28 09:16:57 -04:00
Ginger 2e98ba3ed8 fix: Increase max length for report reasons 2026-04-28 09:16:57 -04:00
Ginger 551cf48642 fix: Add bounds checking for profile data 2026-04-28 09:16:57 -04:00
Ginger d256a1c1fa fix: Add bounds checking for profile data 2026-04-28 09:16:57 -04:00
Ginger 5578144da9 refactor: Clean up api/client/membership/kick.rs 2026-04-28 09:16:57 -04:00
Ginger 5309a064e8 refactor: Remove old project name in api/client/membership/ 2026-04-28 09:16:57 -04:00
Ginger 56d35b4e39 refactor: Clean up api/client/membership.ban.rs 2026-04-28 09:16:57 -04:00
Ginger 7375d1cad4 fix: Check existing key equality when uploading new E2EE keys 2026-04-28 09:16:57 -04:00
Ginger 80baf948ae refactor: Switch back to upstream Ruma 2026-04-28 09:16:57 -04:00
Ginger ed37696cef fix: Enable compatibility feature to fix federation with old conduit-family homeservers 2026-04-28 09:16:57 -04:00
Ginger 0a04c60f31 refactor: Improve summary service logging 2026-04-28 09:16:57 -04:00
Ginger e44ac230a6 fix: Fix failing test 2026-04-28 09:16:57 -04:00
Ginger 57c4567380 chore: Changelog 2026-04-28 09:16:57 -04:00
Ginger a8a8e1ea51 chore: Clippy fixes 2026-04-28 09:16:57 -04:00
Ginger 02f69a7160 fix: FIx code that was causing rustc to panic somehow 2026-04-28 09:16:56 -04:00
Ginger f68205a341 refactor: Remove pointless assert 2026-04-28 09:16:56 -04:00
Ginger 9899632b8b chore: Clippy fixes 2026-04-28 09:16:56 -04:00
Ginger a0524a9566 refactor: Fix errors in api/client/directory.rs, again 2026-04-28 09:16:56 -04:00
Ginger e70004c98f chore: Clippy fixes 2026-04-28 09:16:56 -04:00
Ginger e185f56f3a refactor: Fix errors in admin/processor.rs 2026-04-28 09:16:56 -04:00
Ginger 5058b7979a refactor: Fix errors in admin/user/ 2026-04-28 09:16:56 -04:00
Ginger 7f06a61242 refactor: Fix errors in admin/room/ 2026-04-28 09:16:56 -04:00
Ginger 54fefb421b refactor: Fix errors in admin/query/ 2026-04-28 09:16:56 -04:00
Ginger 9d39321deb refactor: Fix errors in admin/media/commands.rs 2026-04-28 09:16:56 -04:00
Ginger c64a4a71bc refactor: Resolve errors in admin/federation/commands.rs 2026-04-28 09:16:56 -04:00
Ginger 385b4b10d1 refactor: Fix errors in admin/debug/commands.rs 2026-04-28 09:16:56 -04:00
Ginger c12dd20431 refactor: Fix errors in admin/check/commands.rs 2026-04-28 09:16:56 -04:00
Ginger 3ad7c3b30d refactor: Fix remaining errors in api/ (and temporarily switch to a fork of ruma) 2026-04-28 09:16:56 -04:00
Ginger 7a58074a0d refactor: Fix errors in web/ 2026-04-28 09:16:56 -04:00
Ginger 0c7abd792d refactor: Fix errors in api/router/ 2026-04-28 09:16:56 -04:00
Ginger 0f64e6b49c refactor: Fix errors in api/server/well_known.rs 2026-04-28 09:16:56 -04:00
Ginger e7a1c71a25 refactor: Fix errors in api/server/version.rs 2026-04-28 09:16:56 -04:00
Ginger cd3b97ea26 refactor: Fix errors in api/server/user.rs 2026-04-28 09:16:56 -04:00
Ginger 845b731f8c refactor: Fix errors in api/server/state.rs 2026-04-28 09:16:56 -04:00
Ginger 97d2388717 refactor: Fix errors in api/server/state_ids.rs 2026-04-28 09:16:56 -04:00
Ginger 962a4aedc6 refactor: Fix errors in api/server/send.rs 2026-04-28 09:16:56 -04:00
Ginger 0eee63f7a1 refactor: Fix errors in api/server/send_leave.rs 2026-04-28 09:16:52 -04:00
Ginger eba38c2fa0 refactor: Fix errors in api/server/send_knock.rs 2026-04-28 09:16:52 -04:00
Ginger 338cdc2a75 refactor: Fix errors in api/server/send_join.rs 2026-04-28 09:16:52 -04:00
Ginger 2dacb8e071 refactor: Fix errors in api/server/query.rs 2026-04-28 09:16:52 -04:00
Ginger 398f73b690 refactor: Fix errors in api/server/publicrooms.rs 2026-04-28 09:16:52 -04:00
Ginger 78d9c29a05 refactor: Fix errors in api/server/media.rs 2026-04-28 09:16:52 -04:00
Ginger 0406f755c2 refactor: Fix errors in api/server/make_leave.rs 2026-04-28 09:16:52 -04:00
Ginger 1827888f09 refactor: Fix errors in api/server/make_knock.rs 2026-04-28 09:16:52 -04:00
Ginger 8871b1f74b refactor: Fix most errors in api/server/make_join.rs 2026-04-28 09:16:52 -04:00
Ginger c7489fd008 refactor: Fix errors in api/server/key.rs 2026-04-28 09:16:52 -04:00
Ginger 7f5f4df64e refactor: Fix errors in api/server/invite.rs 2026-04-28 09:16:52 -04:00
Ginger 15d87c00bf refactor: Fix errors in api/server/get_missing_events.rs 2026-04-28 09:16:52 -04:00
Ginger 7cae42634e refactor: Fix errors in api/server/event.rs 2026-04-28 09:16:52 -04:00
Ginger bd94ec4033 refactor: Fix errors in api/server/event_auth.rs 2026-04-28 09:16:52 -04:00
Ginger db7d378a2e refactor: Fix errors in api/server/backfill.rs 2026-04-28 09:16:52 -04:00
Ginger 39b2e461be refactor: Fix remaining errors in api/cient/message.rs 2026-04-28 09:16:52 -04:00
Ginger ca358438ee refactor: Fix mystery weirdness in api/client/sync/v3/mod.rs 2026-04-28 09:16:52 -04:00
Ginger 4282d60181 refactor: Fix errors in api/client/well_known.rs 2026-04-28 09:16:52 -04:00
Ginger 10dbea72e8 refactor: Fix errors in api/client/voip.rs 2026-04-28 09:16:52 -04:00
Ginger aa7c2ea1ad refactor: Fix errors in api/client/user_directory.rs 2026-04-28 09:16:52 -04:00
Ginger 698d959407 refactor: Fix errors in api/client/unversioned.rs 2026-04-28 09:16:52 -04:00
Ginger 4c831c3531 refactor: Fix errors in api/client/typing.rs 2026-04-28 09:16:52 -04:00
Ginger 4dfdce303f refactor: Fix errors in api/client/to_device.rs 2026-04-28 09:16:52 -04:00
Ginger 8d8c310a64 refactor: Fix errors in api/client/threads.rs 2026-04-28 09:16:52 -04:00
Ginger e50e24e22d refactor: Fix errors in api/client/thirdparty.rs 2026-04-28 09:16:52 -04:00
Ginger a215b63077 refactor: Fix errors in api/client/tag.rs 2026-04-28 09:16:52 -04:00
Ginger 1d39210a0c refactor: Fix errors in api/client/state.rs 2026-04-28 09:16:52 -04:00
Ginger 360e0dada8 refactor: Fix errors in api/client/session.rs 2026-04-28 09:16:52 -04:00
Ginger cbf24a9483 refactor: Fix errors in api/client/send.rs 2026-04-28 09:16:52 -04:00
Ginger 6cb3f909c9 refactor: Fix errors in api/client/search.rs 2026-04-28 09:16:52 -04:00
Ginger b7c9ef89f0 refactor: Fix errors in api/client/report.rs 2026-04-28 09:16:52 -04:00
Ginger 64f7791ddb refactor: Fix errors in api/client/relations.rs 2026-04-28 09:16:52 -04:00
Ginger 836047b54e refactor: Fix errors in api/client/redact.rs 2026-04-28 09:16:52 -04:00
Ginger 256f8f679d refactor: Fix errors in api/client/read_marker.rs 2026-04-28 09:16:52 -04:00
Ginger 154cda35f3 refactor: Fix errors in api/client/push.rs 2026-04-28 09:16:52 -04:00
Ginger 1bf6d2a117 refactor: Fix errors in api/client/profile.rs and api/client/unstable.rs 2026-04-28 09:16:52 -04:00
Ginger 69d33931fa refactor: Fix errors in api/client/presence.rs 2026-04-28 09:16:52 -04:00
Ginger 83902a584b refactor: Fix errors in api/client/openid.rs 2026-04-28 09:16:52 -04:00
Ginger bcff259875 refactor: Fix most errors in api/client/messages.rs 2026-04-28 09:16:52 -04:00
Ginger 496ca80393 refactor: Fix errors in api/client/media.rs 2026-04-28 09:16:52 -04:00
Ginger 34b992fc40 refactor: Fix errors in api/client/media_legacy.rs
Sent from my Steam Deck
2026-04-28 09:16:52 -04:00
Ginger 1ea9330df8 refactor: Fix errors in api/client/keys.rs 2026-04-28 09:16:52 -04:00
Ginger 267e1c5d65 refactor: Fix errors in api/client/directory.rs 2026-04-28 09:16:52 -04:00
Ginger 36285e7784 refactor: Fix errors in api/client/device.rs 2026-04-28 09:16:52 -04:00
Ginger 53ab20d1cd refactor: Fix errors in api/client/dehydrated_device.rs 2026-04-28 09:16:52 -04:00
Ginger 96adf034e6 refactor: Fix errors in api/client/context.rs 2026-04-28 09:16:51 -04:00
Ginger a75bf32a34 refactor: Fix errors in api/client/capabilities.rs 2026-04-28 09:16:51 -04:00
Ginger c89ecd7b63 refactor: Fix errors in api/client/backup.rs 2026-04-28 09:16:51 -04:00
Ginger 7f30f8419b refactor: Fix errors in api/client/appservice.rs 2026-04-28 09:16:51 -04:00
Ginger 0a81f4d629 refactor: Fix errors in api/client/account_data.rs 2026-04-28 09:16:51 -04:00
Ginger 4e456249ac refactor: Fix errors in api/client/sync 2026-04-28 09:16:51 -04:00
Ginger 01e403f05f refactor: Resolve remaining errors in threepid.rs 2026-04-28 09:16:51 -04:00
Ginger a2f6141f4b refactor: Fix errors in api/client/room/ 2026-04-28 09:16:51 -04:00
Ginger 97a01a1500 refactor: Rename PduBuilder to PartialPdu 2026-04-28 09:16:51 -04:00
Ginger bf9c9716eb refactor: Add function to state_accessor to get create event 2026-04-28 09:16:51 -04:00
Ginger 471eb54c66 refactor: Consolidate hierarchy and summary logic in a new service 2026-04-28 09:16:51 -04:00
Ginger 755006c66d refactor: Fix errors in api/client/membership/ 2026-04-28 09:16:51 -04:00
Ginger ccd6072f2d refactor: Fix (most) errors in api/client/account/ 2026-04-28 09:16:51 -04:00
Ginger 24f7e1d658 chore: Clippy fixes 2026-04-28 09:16:51 -04:00
Ginger d62eeda130 refactor: Replace more uses of RoomVersionId with RoomVersionRules 2026-04-28 09:16:51 -04:00
Ginger 3e1f97487f fix: Resolve errors in recently added services 2026-04-28 09:16:51 -04:00
Jade Ellis a4e64383b7 refactor: Ruma upstraming, bake a little more 2026-04-28 09:16:51 -04:00
Ginger 204bc1367e refactor: Ruma upstreaming, half-baked edition
Co-authored-by: Jade Ellis <jade@ellis.link>
2026-04-28 09:16:51 -04:00
Jade Ellis 1cc9dbf2a4 chore: Update lockfile 2026-04-28 09:27:00 +01:00
Renovate Bot 2cf28baf03 chore(deps): update pre-commit hook crate-ci/typos to v1.45.2 2026-04-28 05:03:35 +00:00
timedout f3fb218652 style: Clippy conflicts with cargo fmt, apparently 2026-04-27 22:15:52 +00:00
timedout 0924b7d27e style: Use debug assert instead of a normal assert 2026-04-27 22:15:52 +00:00
timedout 8575f191a0 style: Simplify build_local_dag return 2026-04-27 22:15:52 +00:00
timedout fe7cfd96e7 feat: Assert that no events were dropped during sorting 2026-04-27 22:15:52 +00:00
timedout 8b0e86a05d fix: Don't consider out-of-scope nodes as prev events before sorting incoming events 2026-04-27 22:15:52 +00:00
Jade Ellis 8b8fef998c fix(deps): Enable rustls roots on old rustls 2026-04-27 22:51:21 +01:00
Jade Ellis decd6083a0 fix(deps): Enable a TLS backend for outdated reqwest 2026-04-27 13:10:47 +01:00
Renovate Bot 06184d8c9f chore(deps): update https://github.com/taiki-e/install-action digest to 787505c 2026-04-25 12:21:19 +01:00
Renovate Bot 7c20e22b75 chore(deps): pin https://github.com/dorny/paths-filter action to fbd0ab8 2026-04-25 11:19:09 +00:00
Jade Ellis 3f862b58cb ci: Fix unstable builds for repo packages 2026-04-25 11:33:25 +01:00
Jade Ellis 046a6356f3 ci: Automaticallly upload release binaries 2026-04-25 11:17:43 +01:00
Jade Ellis 3af0240ff5 style: Fix clipy lint 2026-04-25 10:07:17 +01:00
ginger 5dcfff51cf chore: Admin announcement 2026-04-24 20:33:07 +00:00
Ginger b9989f1713 chore: Release 2026-04-24 15:21:47 -04:00
Ginger 1d3e3e7e62 chore: Update changelog 2026-04-24 15:21:40 -04:00
Jade Ellis 0adf3aa956 fix: Revert 7b1aabda9f
Yeah that didn't work sadly.
2026-04-24 16:22:46 +01:00
Jade Ellis 7b1aabda9f feat: Re-enable http3
This required the previous commit, and relies on
the included flag to make fat LTO builds
work correctly.
2026-04-24 14:51:11 +01:00
Jade Ellis e31c5997b7 fix: Explicitly set TLS backends
Dependency updates mean we have to set a custom TLS backend sooner.
Also some groundwork for being able to use aws-lc in future
2026-04-24 14:19:12 +01:00
Jade Ellis 7ca0d137c4 chore: Replace ring for sha256 with sha2 2026-04-24 12:56:05 +01:00
Jade Ellis 0344bf71d8 chore: Disable http3 by default
Unfortunately h3 requires aws-lc since the last version of reqwest.
aws-lc currently breaks the build.
2026-04-24 12:11:48 +01:00
Jade Ellis a07d3e24ea fix(deps): corrext aws-lc-rs dependencies and direct-tls 2026-04-24 10:06:47 +01:00
Jade Ellis 1bc7950748 fix: Update direct-tls server handle 2026-04-24 09:34:16 +01:00
tokii 0fd43ff6fa docs: Update Nomad deployment docs to include volume configuration changes 2026-04-23 20:07:53 +00:00
tokii 796136f1a6 docs: Update Nomad deployment docs for HTTPS and Traefik changes 2026-04-23 20:07:53 +00:00
tokii 447608985b docs: Add deployment documentation for Nomad 2026-04-23 20:07:53 +00:00
timedout 5f4cd47d88 fix: Add workaround for handling malformed PDUs
Signed-off-by: timedout <git@nexy7574.co.uk>
Reviewed-On: https://forgejo.ellis.link/continuwuation/continuwuity-sec/pulls/7
Reviewed-By: Jade Ellis <jade@ellis.link>
2026-04-23 20:48:11 +01:00
stratself a7244bdb68 docs(docker): Detailed port exposure docs for other reverse proxies 2026-04-23 19:47:46 +00:00
stratself 91f2900463 docs(docker): More compose cleanups
* Stringify and use long URLs for image names
* Use read-only docker socket in traefik mount
* Shorten some comments
2026-04-23 19:47:46 +00:00
stratself e44ae3bac9 docs(delegation): Add compose examples
Previous projects used split-domain examples, so it's good to add back
2026-04-23 19:47:46 +00:00
stratself b692f9e6e7 fix(docs): Fix one wrong config filename and title all the composes 2026-04-23 19:47:46 +00:00
stratself 695333fe5b chore: Renumber changelog PR and fix trailing whitespace 2026-04-23 19:47:31 +00:00
stratself bc7a6c148f fix(docs): Small wording fixes 2026-04-23 19:47:31 +00:00
ky-bean bd3944573b docs(docker): Add note for required config setting 2026-04-23 19:47:31 +00:00
ky-bean 21ac3c5a86 chore: Add news fragment for #1553 2026-04-23 19:47:31 +00:00
ky-bean 3976849b97 docs(docker): fix typos (psuedo=>pseudo, decleration=>declaration) 2026-04-23 19:47:31 +00:00
ky-bean a1e3619291 docs(docker): update wording, implement suggestions from @lveneris 2026-04-23 19:47:31 +00:00
ky-bean a92fc78a90 docs(docker): Detail how to access the server's console 2026-04-23 19:47:31 +00:00
ky-bean fc429ea564 docs: explain admin console for docker deployments 2026-04-23 19:47:31 +00:00
stratself 69c931e18a docs(generic): Highlight important /_continuwuity features + typofixes 2026-04-23 19:46:57 +00:00
stratself 284e0ce1e5 chore: Add changelog for #1677 2026-04-23 19:46:57 +00:00
stratself a13779a051 docs(generic): Remove Nix build section and further wording fixes
* Add spec link to well-known support endpoint
* Prioritize simpler "route everything" approach for other RProxies
2026-04-23 19:46:57 +00:00
stratself 7163714697 docs(generic): Fix links for CI binaries 2026-04-23 19:46:57 +00:00
stratself 3998a14c32 docs(generic): Rewrite sections on server initialization and testing
* Rename "You're done" to "Starting Your Server"
* Add instructions for initial registration token flow
* Shorten "How do I know it works" section
* Beautify "What's Next" section
2026-04-23 19:46:57 +00:00
stratself c79f2a3057 docs(generic): Fix router + reverse proxy + docker build sections
* Link docker builds to section in dev pages
* Delete old section on port forwarding
* Create new section on port exposing, near reverse proxy section
* Rewrite Other Reverse Proxies section to update specified routes
* Move reverse proxy software caveats into its own subsection
* Other wording and structure fixes and improvements
2026-04-23 19:46:57 +00:00
stratself 17837c51a0 docs(generic): Various fixes for consistency with other pages
* Use indirect URLs
* Change your.server.name to canonical example.com
* Put Getting help section in a tip admonition
* Remove statement on Caddy preference
* Clean up "What's next" section
2026-04-23 19:46:57 +00:00
stratself 99a7be0222 fix(docs): Correct ports for LiveKit TURN range 2026-04-23 19:46:16 +00:00
stratself 41ed2eb167 docs(livekit): Add/correct client discovery paths and extra fixes
* Use de facto unstasble path instead of the standard one
* Document both the unstable and well-known paths as being exposed by
  Continuwuity when set
* Make curling said path one of the Testing steps
* Removing confusing console `~$` prefix symbols, move commands and
  response into separate code blocks
* Number the MSCs
2026-04-23 19:46:16 +00:00
Renovate Bot 2b08460b16 chore(deps): update https://github.com/actions/setup-node digest to 48b55a0 2026-04-23 19:45:52 +00:00
Renovate Bot 4cf8f6e05b chore(deps): update https://github.com/taiki-e/install-action digest to 74e87cb 2026-04-23 19:45:46 +00:00
Jade Ellis ae37acb228 ci: Don't run clippy & tests if rust files haven't changed 2026-04-23 20:44:57 +01:00
Jade Ellis 10c3045f5f ci: Run Renovate on resolvematrix 2026-04-23 20:44:56 +01:00
Jade Ellis 8242718571 ci: Label PRs touching dependencies 2026-04-23 20:44:56 +01:00
Renovate Bot 03db067aab chore(deps): update ghcr.io/renovatebot/renovate docker tag to v43.140.0 2026-04-23 19:23:09 +00:00
Jade Ellis b28ddde1eb chore: Update lockfile 2026-04-23 20:04:05 +01:00
Jade Ellis 0134f69bf9 chore: Update incompatible dependenceis 2026-04-23 20:02:48 +01:00
Jade Ellis 15878371bf chore: Update reqwest 2026-04-23 18:53:25 +01:00
Getz Mikalsen 980bd475b6 feat: Add TLS options for LDAP (#1389)
Optional StartTLS for LDAP and add option to skip TLS verification.

Co-authored-by: Jade Ellis <jade@ellis.link>
Reviewed-on: https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1389
Reviewed-by: Jade Ellis <jade@ellis.link>
2026-04-23 17:39:25 +00:00
331 changed files with 9571 additions and 10020 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ runs:
- name: Install timelord-cli and git-warp-time
if: steps.check-binaries.outputs.need-install == 'true'
uses: https://github.com/taiki-e/install-action@a2352fc6ce487f030a3aa709482d57823eadfb37 # v2
uses: https://github.com/taiki-e/install-action@b5fddbb5361bce8a06fb168c9d403a6cc552b084 # v2
with:
tool: git-warp-time,timelord-cli@3.0.1
-2
View File
@@ -45,7 +45,6 @@
- [ ] I have [tested my contribution][c1t] (or proof-read it for documentation-only changes)
myself, if applicable. This includes ensuring code compiles.
- [ ] My commit messages follow the [commit message format][c1cm] and are descriptive.
- [ ] I have written a [news fragment][n1] for this PR, if applicable<!--(can be done after hitting open!)-->.
<!--
Notes on these requirements:
@@ -79,4 +78,3 @@
[c1pc]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#pre-commit-checks
[c1t]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#running-tests-locally
[c1cm]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#commit-messages
[n1]: https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
+3
View File
@@ -37,6 +37,9 @@ jobs:
if (file.startsWith('pkg/') || file.startsWith('nix/') || file === 'flake.nix' || file === 'flake.lock' || file.startsWith('docker/')) {
labelsToAdd.add('Meta/Packaging');
}
if (file === 'Cargo.lock') {
labelsToAdd.add('Dependencies');
}
}
if (labelsToAdd.size > 0) {
+1 -1
View File
@@ -96,7 +96,7 @@ jobs:
if [[ ${{ forge.ref_name }} =~ ^v+[0-9]\.+[0-9]\.+[0-9]$ ]]; then
# Use the "stable" component for tagged semver releases
COMPONENT="stable"
elif [[ ${{ forge.ref }} =~ ^refs/tags/^v+[0-9]\.+[0-9]\.+[0-9] ]]; then
elif [[ ${{ forge.ref_name }} =~ ^v+[0-9]\.+[0-9]\.+[0-9] ]]; then
# Use the "unstable" component for tagged semver pre-releases
COMPONENT="unstable"
else
+6 -2
View File
@@ -105,7 +105,7 @@ jobs:
RELEASE_SUFFIX=""
TAG_NAME="${{ github.ref_name }}"
# Extract version from tag (remove v prefix if present)
TAG_VERSION=$(echo "$TAG_NAME" | sed 's/^v//')
TAG_VERSION=$(echo "$TAG_NAME" | sed 's/^v//' | tr '-' '~')
# Create spec file with tag version
sed -e "s/^Version:.*$/Version: $TAG_VERSION/" \
@@ -270,9 +270,13 @@ jobs:
# Determine the group based on ref type and branch
if [[ "${{ github.ref }}" == "refs/tags/"* ]]; then
GROUP="stable"
# For tags, extract the tag name for version info
TAG_NAME="${{ github.ref_name }}"
if [[ "$TAG_NAME" == *"-"* ]]; then
GROUP="unstable"
else
GROUP="stable"
fi
elif [ "${{ github.ref_name }}" = "main" ]; then
GROUP="dev"
else
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
- name: Setup Node.js
if: steps.runner-env.outputs.node_major == '' || steps.runner-env.outputs.node_major < '20'
uses: https://github.com/actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: https://github.com/actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
steps:
- name: 📦 Setup Node.js
uses: https://github.com/actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: https://github.com/actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "22"
+23
View File
@@ -9,6 +9,7 @@ on:
permissions:
contents: read
pull-requests: read
jobs:
fast-checks:
@@ -40,10 +41,32 @@ jobs:
cargo +nightly fmt --all -- --check && \
echo "✅ Formatting check passed" || \
exit 1
check-changes:
name: Check changed files
runs-on: ubuntu-latest
outputs:
rust: ${{ steps.filter.outputs.rust }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Check for file changes
uses: https://github.com/dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
id: filter
with:
filters: |
rust:
- '**/*.rs'
- '**/Cargo.toml'
- '**/Cargo.lock'
clippy-and-tests:
name: Clippy and Cargo Tests
runs-on: ubuntu-latest
needs: check-changes
if: needs.check-changes.outputs.rust == 'true'
steps:
- name: Checkout repository
+22
View File
@@ -199,6 +199,28 @@ jobs:
registry_user: ${{ vars.BUILTIN_REGISTRY_USER || github.actor }}
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
release-binaries:
name: "Release Binaries"
runs-on: ubuntu-latest
needs:
- build-release
- build-maxperf
permissions:
contents: write
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Download binary artifacts
uses: forgejo/download-artifact@v4
with:
pattern: conduwuit*
path: binaries
merge-multiple: true
- name: Create Release and Upload
uses: https://github.com/softprops/action-gh-release@v2
with:
draft: true
files: binaries/*
mirror_images:
name: "Mirror Images"
runs-on: ubuntu-latest
+3 -3
View File
@@ -43,7 +43,7 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:43.111.0@sha256:da5fcac20c48d9792aac9c61fd234531bfa8df61263a39387cd8920263ca4768
image: ghcr.io/renovatebot/renovate:43.140.0@sha256:61303c28b10a491c559529fb6f41745850e4755a43a54c04c3ae6848d6eaf5cc
options: --tmpfs /tmp:exec
steps:
- name: Checkout
@@ -90,12 +90,12 @@ jobs:
RENOVATE_PLATFORM: forgejo
RENOVATE_ENDPOINT: ${{ github.server_url }}
RENOVATE_AUTODISCOVER: 'false'
RENOVATE_REPOSITORIES: '["${{ github.repository }}"]'
RENOVATE_REPOSITORIES: '["${{ github.repository }}", "continuwuation/resolvematrix"]'
RENOVATE_GIT_TIMEOUT: 60000
RENOVATE_REQUIRE_CONFIG: 'required'
RENOVATE_ONBOARDING: 'false'
# RENOVATE_ONBOARDING: 'false'
RENOVATE_INHERIT_CONFIG: 'true'
RENOVATE_GITHUB_TOKEN_WARN: 'false'
+1 -1
View File
@@ -24,7 +24,7 @@ repos:
- id: check-added-large-files
- repo: https://github.com/crate-ci/typos
rev: v1.45.1
rev: v1.46.1
hooks:
- id: typos
- id: typos
+17
View File
@@ -1,3 +1,20 @@
# Continuwuity 0.5.8 (2026-04-24)
## Features
- LDAP can now optionally be connected to using StartTLS, and you may unsafely skip verification. Contributed by @getz (#1389)
- Users will now be prevented from removing their email if the server is configured to require an email when registering an account.
## Bugfixes
- Fixed a situation where multiple email addresses could be associated with one user when that user changes their email address.
## Improved Documentation
- Updated config docs to state we support room version 12, and set it as default. Contributed by @ezera. (#1622)
- Improve instructions for generic deployments, removing unnecessary parts and documenting the new initial registration token flow. Contributed by @stratself (#1677)
# Continuwuity v0.5.7 (2026-04-17)
## Features
+1 -1
View File
@@ -1 +1 @@
Contributors are expected to follow the [Continuwuity Community Guidelines](continuwuity.org/community/guidelines).
Contributors are expected to follow the [Continuwuity Community Guidelines](https://continuwuity.org/community/guidelines).
Generated
+468 -560
View File
File diff suppressed because it is too large Load Diff
+52 -49
View File
@@ -12,7 +12,7 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "0.5.7"
version = "0.5.9"
[workspace.metadata.crane]
name = "conduwuit"
@@ -36,10 +36,13 @@ version = "0.3"
features = ["ffi", "std", "union"]
[workspace.dependencies.const-str]
version = "0.7.0"
version = "1.1.0"
[workspace.dependencies.ctor]
version = "0.10.0"
version = "0.13.0"
[workspace.dependencies.dtor]
version = "0.13.0"
[workspace.dependencies.cargo_toml]
version = "0.22"
@@ -47,9 +50,9 @@ default-features = false
features = ["features"]
[workspace.dependencies.toml]
version = "0.9.5"
version = "1.1.2"
default-features = false
features = ["parse"]
features = ["parse", "serde"]
[workspace.dependencies.sanitize-filename]
version = "0.6.0"
@@ -68,7 +71,7 @@ default-features = false
version = "0.1.3"
[workspace.dependencies.rand]
version = "0.10.0"
version = "0.10.1"
# Used for the http request / response body type for Ruma endpoints used with reqwest
[workspace.dependencies.bytes]
@@ -102,15 +105,18 @@ default-features = false
features = ["typed-header", "tracing", "cookie"]
[workspace.dependencies.axum-server]
version = "0.7.2"
version = "0.8.0"
default-features = false
# to listen on both HTTP and HTTPS if listening on TLS dierctly from conduwuit for complement or sytest
[workspace.dependencies.axum-server-dual-protocol]
version = "0.7"
# version = "0.7"
git = "https://github.com/vinchona/axum-server-dual-protocol.git"
rev = "ca6db055254255b74238673ce4135698e347d71c" # feat!: bump axum_server to 0.8.0
default-features = false
[workspace.dependencies.axum-client-ip]
version = "0.7"
version = "1.3"
[workspace.dependencies.tower]
version = "0.5.2"
@@ -134,13 +140,12 @@ features = [
[workspace.dependencies.rustls]
version = "0.23.25"
default-features = false
features = ["aws_lc_rs"]
[workspace.dependencies.reqwest]
version = "0.12.15"
version = "0.13.2"
default-features = false
features = [
"rustls-tls-native-roots",
"rustls-no-provider",
"socks",
"hickory-dns",
"http2",
@@ -159,7 +164,7 @@ features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde-saphyr]
version = "0.0.23"
version = "0.0.25"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
@@ -167,7 +172,7 @@ version = "1.1.0"
# Used for ruma wrapper
[workspace.dependencies.serde_html_form]
version = "0.2.6"
version = "0.4.0"
# Used for password hashing
[workspace.dependencies.argon2]
@@ -251,7 +256,7 @@ features = [
]
[workspace.dependencies.tokio-metrics]
version = "0.4.0"
version = "0.5.0"
[workspace.dependencies.libloading]
version = "0.9.0"
@@ -340,51 +345,51 @@ version = "0.1.88"
[workspace.dependencies.lru-cache]
version = "0.1.2"
[workspace.dependencies.assign]
version = "1.1.1"
# Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
#branch = "conduwuit-changes"
rev = "d00b51a8669b21689c4eb47fb81f3a8b27c3e371"
# version = "0.14.1"
git = "https://github.com/ruma/ruma.git"
rev = "3ecd80b92794d2d93f657a7b3db62d4be237526b"
features = [
"compat",
"rand",
"appservice-api-c",
"client-api",
"federation-api",
"markdown",
"push-gateway-api-c",
"unstable-exhaustive-types",
"state-res",
"rand",
"markdown",
"ring-compat",
"compat-upload-signatures",
"identifiers-validation",
"unstable-unspecified",
"compat-optional-txn-pdus",
"unstable-msc2448",
"unstable-msc2666",
"unstable-msc2867",
"unstable-msc2870",
"unstable-msc3026",
"unstable-msc3061",
"unstable-msc3814",
"unstable-msc3245",
"unstable-msc3266",
"unstable-msc3381", # polls
"unstable-msc3489", # beacon / live location
"unstable-msc3575",
"unstable-msc3930", # polls push rules
"unstable-msc3381",
"unstable-msc3489",
"unstable-msc3930",
"unstable-msc4075",
"unstable-msc4095",
"unstable-msc4121",
"unstable-msc4125",
"unstable-msc4155",
"unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4210", # remove legacy mentions
"unstable-msc4195",
"unstable-msc4203",
"unstable-msc4310",
"unstable-msc4373",
"unstable-msc4380",
"unstable-msc4143",
"unstable-msc4293",
"unstable-msc4406",
"unstable-msc4439",
"unstable-msc4466",
"unstable-extensible-events",
"unstable-pdu",
"unstable-msc4155",
"unstable-msc4143", # livekit well_known response
"unstable-msc4284",
"unstable-msc4439", # pgp_key in .well_known/matrix/support
]
[workspace.dependencies.rust-rocksdb]
@@ -429,14 +434,13 @@ features = ["http", "grpc-tonic", "trace", "logs", "metrics"]
# optional sentry metrics for crash/panic reporting
[workspace.dependencies.sentry]
version = "0.46.0"
version = "0.48.0"
default-features = false
features = [
"backtrace",
"contexts",
"debug-images",
"panic",
"rustls",
"tower",
"tower-http",
"tracing",
@@ -445,9 +449,9 @@ features = [
]
[workspace.dependencies.sentry-tracing]
version = "0.46.0"
version = "0.48.0"
[workspace.dependencies.sentry-tower]
version = "0.46.0"
version = "0.48.0"
# jemalloc usage
[workspace.dependencies.tikv-jemalloc-sys]
@@ -546,21 +550,16 @@ features = ["std"]
[workspace.dependencies.maplit]
version = "1.0.2"
[workspace.dependencies.ldap3]
version = "0.12.0"
default-features = false
features = ["sync", "tls-rustls", "rustls-provider"]
[workspace.dependencies.yansi]
version = "1.0.1"
[workspace.dependencies.askama]
version = "0.15.0"
version = "0.16.0"
[workspace.dependencies.lettre]
version = "0.11.19"
default-features = false
features = ["smtp-transport", "pool", "hostname", "builder", "rustls", "rustls-native-certs", "tokio1", "ring", "tokio1-rustls", "tracing", "serde"]
features = ["smtp-transport", "pool", "hostname", "builder", "rustls", "rustls-native-certs", "tokio1", "rustls-no-provider", "tokio1-rustls", "tracing", "serde"]
[workspace.dependencies.governor]
version = "0.10.4"
@@ -657,6 +656,10 @@ default-features = false
package = "conduwuit"
path = "src/main"
[workspace.dependencies.ruminuwuity]
package = "ruminuwuity"
path = "src/ruminuwuity"
###############################################################################
#
# Release profiles
-1
View File
@@ -1 +0,0 @@
Users will now be prevented from removing their email if the server is configured to require an email when registering an account.
+1
View File
@@ -0,0 +1 @@
The invite recipient's membership event is now included in invite stripped state, which should fix flaky invite display in some clients. Contributed by @ginger
+1
View File
@@ -0,0 +1 @@
Switched from Continuwuity's fork of Ruma back to upstream Ruma. Contributed by @ginger.
+1
View File
@@ -0,0 +1 @@
Added support for MSC4466, which allows clients to customize how changes to a user's global profile are propagated. Contributed by @ginger.
+1
View File
@@ -0,0 +1 @@
Removed support for guest user registration, a little-used and deprecated approach to room previews.
+1
View File
@@ -0,0 +1 @@
The deprecated `well_known.rtc_focus_server_urls` config option has been removed. MatrixRTC foci should be configured using the `matrix_rtc.foci` config option.
-1
View File
@@ -1 +0,0 @@
Fixed a situation where multiple email addresses could be associated with one user when that user changes their email address.
+1
View File
@@ -0,0 +1 @@
Add performance tuning documentation. Contributed by @stratself.
-1
View File
@@ -1 +0,0 @@
Updated config docs to state we support room version 12, and set it as default. Contributed by @ezera.
+1
View File
@@ -0,0 +1 @@
Explain accessing Continuwuity's server console when deployed via Docker.
+1
View File
@@ -0,0 +1 @@
Removed support for LDAP.
+1
View File
@@ -0,0 +1 @@
Clarified in the config that `max_request_size` affects federated media as well.
+1
View File
@@ -0,0 +1 @@
Added support for fallback encryption keys.
+1
View File
@@ -0,0 +1 @@
Fixed a bug that caused the server to drop events during processing if several events for the same room were sent in a singular transaction. Contributed by @nex.
+1
View File
@@ -0,0 +1 @@
Add `!admin users reject-all-invites` to clean invite spam
+1
View File
@@ -0,0 +1 @@
fix `!admin query account-data account-data-get` not returning the content
+1
View File
@@ -0,0 +1 @@
Fixed an issue where Continuwuity would only advertise support for the unstable endpoint for Mutual Rooms (MSC2666), despite only supporting the stable endpoint. Contributed by @Henry-Hiles (QuadRadical)
-2
View File
@@ -7,7 +7,6 @@
[global]
address = "0.0.0.0"
allow_device_name_federation = true
allow_guest_registration = true
allow_public_room_directory_over_federation = true
allow_registration = true
database_path = "/database"
@@ -32,7 +31,6 @@ rocksdb_log_level = "info"
rocksdb_max_log_files = 1
rocksdb_recovery_mode = 0
rocksdb_paranoid_file_checks = true
log_guest_registrations = false
allow_legacy_media = true
startup_netburst = true
startup_netburst_keep = -1
+1 -125
View File
@@ -291,6 +291,7 @@
#ip_lookup_strategy = 5
# Max request size for file uploads in bytes. Defaults to 20MB.
# Also limits incoming federated media.
#
#max_request_size = 20971520
@@ -573,18 +574,6 @@
#
#allow_public_room_directory_over_federation = false
# Allow guests/unauthenticated users to access TURN credentials.
#
# This is the equivalent of Synapse's `turn_allow_guests` config option.
# This allows any unauthenticated user to call the endpoint
# `/_matrix/client/v3/voip/turnServer`.
#
# It is unlikely you need to enable this as all major clients support
# authentication for this endpoint and prevents misuse of your TURN server
# from potential bots.
#
#turn_allow_guests = false
# Set this to true to lock down your server's public room directory and
# only allow admins to publish rooms to the room directory. Unpublishing
# is still allowed by all users with this enabled.
@@ -1282,21 +1271,6 @@
#
#brotli_compression = false
# Set to true to allow user type "guest" registrations. Some clients like
# Element attempt to register guest users automatically.
#
#allow_guest_registration = false
# Set to true to log guest registrations in the admin room. Note that
# these may be noisy or unnecessary if you're a public homeserver.
#
#log_guest_registrations = false
# Set to true to allow guest registrations/users to auto join any rooms
# specified in `auto_join_rooms`.
#
#allow_guests_auto_join_rooms = false
# Enable the legacy unauthenticated Matrix media repository endpoints.
# These endpoints consist of:
# - /_matrix/media/*/config
@@ -1900,16 +1874,6 @@
#
#support_pgp_key =
# **DEPRECATED**: Use `[global.matrix_rtc].foci` instead.
#
# A list of MatrixRTC foci URLs which will be served as part of the
# MSC4143 client endpoint at /.well-known/matrix/client.
#
# This option is deprecated and will be removed in a future release.
# Please migrate to the new `[global.matrix_rtc]` config section.
#
#rtc_focus_server_urls = []
[global.blurhashing]
# blurhashing x component, 4 is recommended by https://blurha.sh/
@@ -1945,94 +1909,6 @@
#
#foci = []
[global.ldap]
# Whether to enable LDAP login.
#
# example: "true"
#
#enable = false
# Whether to force LDAP authentication or authorize classical password
# login.
#
# example: "true"
#
#ldap_only = false
# URI of the LDAP server.
#
# example: "ldap://ldap.example.com:389"
#
#uri = ""
# Root of the searches.
#
# example: "ou=users,dc=example,dc=org"
#
#base_dn = ""
# Bind DN if anonymous search is not enabled.
#
# You can use the variable `{username}` that will be replaced by the
# entered username. In such case, the password used to bind will be the
# one provided for the login and not the one given by
# `bind_password_file`. Beware: automatically granting admin rights will
# not work if you use this direct bind instead of a LDAP search.
#
# example: "cn=ldap-reader,dc=example,dc=org" or
# "cn={username},ou=users,dc=example,dc=org"
#
#bind_dn = ""
# Path to a file on the system that contains the password for the
# `bind_dn`.
#
# The server must be able to access the file, and it must not be empty.
#
#bind_password_file = ""
# Search filter to limit user searches.
#
# You can use the variable `{username}` that will be replaced by the
# entered username for more complex filters.
#
# example: "(&(objectClass=person)(memberOf=matrix))"
#
#filter = "(objectClass=*)"
# Attribute to use to uniquely identify the user.
#
# example: "uid" or "cn"
#
#uid_attribute = "uid"
# Attribute containing the display name of the user.
#
# example: "givenName" or "sn"
#
#name_attribute = "givenName"
# Root of the searches for admin users.
#
# Defaults to `base_dn` if empty.
#
# example: "ou=admins,dc=example,dc=org"
#
#admin_base_dn = ""
# The LDAP search filter to find administrative users for continuwuity.
#
# If left blank, administrative state must be configured manually for each
# user.
#
# You can use the variable `{username}` that will be replaced by the
# entered username for more complex filters.
#
# example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
#
#admin_filter = ""
#[global.antispam]
#[global.antispam.meowlnir]
+4 -2
View File
@@ -17,12 +17,14 @@ ARG LLVM_VERSION=21
# Line one: compiler tools
# Line two: curl, for downloading binaries and wget because llvm.sh is broken with curl
# Line three: for xx-verify
# golang, cmake: For aws-lc-rs bindgen
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y \
pkg-config make jq \
wget curl git software-properties-common \
file
# golang cmake
# LLVM packages
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
@@ -48,7 +50,7 @@ EOF
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.18.1
ENV BINSTALL_VERSION=1.19.1
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
@@ -162,7 +164,7 @@ ENV CONDUWUIT_VERSION_EXTRA=$CONDUWUIT_VERSION_EXTRA
ENV CONTINUWUITY_VERSION_EXTRA=$CONTINUWUITY_VERSION_EXTRA
ARG RUST_PROFILE=release
ARG CARGO_FEATURES="default,http3"
ARG CARGO_FEATURES="default"
# Build the binary
RUN --mount=type=cache,target=/usr/local/cargo/registry \
+1 -1
View File
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.18.1
ENV BINSTALL_VERSION=1.19.1
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
+5
View File
@@ -8,6 +8,11 @@
"type": "file",
"name": "dns",
"label": "DNS tuning (recommended)"
},
{
"type": "file",
"name": "performance",
"label": "Performance tuning"
}
]
+54 -7
View File
@@ -50,8 +50,6 @@ # Defaults to members of the admin room if unset
# CONTINUWUITY_WELL_KNOWN__SERVER: matrix.example.com:443
```
## Reverse proxying well-known files to Continuwuity
After doing the steps above, Continuwuity will serve these 3 JSON files:
- `/.well-known/matrix/client`: for Client-Server discovery
@@ -60,9 +58,11 @@ ## Reverse proxying well-known files to Continuwuity
To enable full discovery, you will need to reverse proxy these paths from the base domain back to Continuwuity.
## Reverse proxying well-known files to Continuwuity
<details>
<summary>For Caddy</summary>
<summary>For **Caddy**</summary>
```
matrix.example.com:443 {
@@ -78,7 +78,7 @@ ## Reverse proxying well-known files to Continuwuity
<details>
<summary>For Traefik (via Docker labels)</summary>
<summary>For **Traefik** (via Docker labels)</summary>
```
services:
@@ -93,7 +93,10 @@ ## Reverse proxying well-known files to Continuwuity
</details>
Restart Continuwuity and your reverse proxy. Once that's done, visit these routes and check that the responses match the examples below:
For **Docker** users, consult the compose files in the [Appendix section](#docker-compose-examples).
After applying these changes, restart Continuwuity and your reverse proxy.Visit these routes and check that the responses match the examples below:
<details open>
@@ -153,9 +156,11 @@ ### Serving well-known files manually
## Troubleshooting
Check with the [Matrix Connectivity Tester][federation-tester] to see that it's working.
Check that other servers can connect to you.
Here are some tools that can help identify federation issues:
[federation-tester]: https://federationtester.mtrnord.blog/
- [Matrix Connectivity Tester](https://federationtester.mtrnord.blog/)
- [Matrix Federation Tester](https://federationtester.matrix.org/)
### Cannot log in with web clients
@@ -253,3 +258,45 @@ ## Related Documentation
- [Server-to-Server resolution](https://spec.matrix.org/v1.17/server-server-api/#resolving-server-names) (see this for more information on SRV records)
- [Client-to-Server resolution](https://spec.matrix.org/v1.17/client-server-api/#server-discovery)
- [MSC1929: Homeserver Admin Contact and Support page](https://github.com/matrix-org/matrix-spec-proposals/pull/1929)
## Appendix
### Docker Compose examples
The following Compose files are taken from [Docker instructions](../deploying/docker.mdx) and reconfigured to support split-domain delegation. Note the updated `CONTINUWUITY_WELL_KNOWN` variable and relevant changes in reverse proxy rules.
<details>
<summary>Caddy (using Caddyfile) - delegated.docker-compose.with-caddy.yml ([view raw](/advanced/delegated.docker-compose.with-caddy.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.with-caddy.yml"
```
</details>
<details>
<summary>Caddy (using labels) - delegated.docker-compose.with-caddy-labels.yml ([view raw](/advanced/delegated.docker-compose.with-caddy-labels.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.with-caddy-labels.yml"
```
</details>
<details>
<summary>Traefik (for existing setup) - delegated.docker-compose.for-traefik.yml ([view raw](/advanced/delegated.docker-compose.for-traefik.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.for-traefik.yml"
```
</details>
<details>
<summary>Traefik included - delegated.docker-compose.with-traefik.yml ([view raw](/advanced/delegated.docker-compose.with-traefik.yml))</summary>
```yaml file="../public/advanced/delegated.docker-compose.with-traefik.yml"
```
</details>
+135
View File
@@ -0,0 +1,135 @@
# Performance tuning
Continuwuity's default configs are suited for many typical setups and scales appropriately with the size of your hardware. However, there are many scenarios where additional modifications can be made to better utilize your server resources.
This page aims to outline various performance tweaks for Continuwuity and their effects. These adjustments are especially helpful for homeservers that join many large federated rooms or have many users, and it will become increasingly necessary as the Matrix network expands. As always, your mileage may vary according to your setup's specifics. If you have further discussions or recommendations, please share them in the community rooms.
## DNS tuning (recommended)
Please see the dedicated [DNS tuning guide](./dns.mdx).
## Cache capacities
If you have memory to spare, consider increasing the `cache_capacity_modifier` value to a larger number to allow more data to be stored in hot memory. This *significantly* speeds up many intensive operations (such as state resolutions) and decreases CPU usage and disk I/O. Start with a baseline of `cache_capacity_modifier = 2.0` and tune up until you are satisfied with RAM usage.
On the other hand, if your system doesn't have a lot of RAM, consider decreasing the cache capacity modifier to something smaller than `1.0` to avoid low-memory issues (at the cost of higher load on disk/CPU). This recommendation also works if your system has abnormally little RAM compared to the number of CPU cores (for example, 2GB RAM for 12 cores), as cache capacities scale according to number of available cores.
## Disabling some features
You can disable outgoing **typing notifications** and **read markers** to reduce strain on the CPU and network when actively participating in rooms.
```toml
# disables sending read receipts
allow_outgoing_read_receipts = false
# disables sending typing notifications
allow_outgoing_typing = false
```
Outgoing presence updates are also considered very expensive and have been disabled by default (`allow_outgoing_presence = false`). For more savings, you may wish to disable _all_ processing of presence entirely.
```toml title=continuwuity.toml
# disabling presence updates entirely
allow_local_presence = false
allow_incoming_presence = false
allow_outgoing_presence = false
```
## Tuning database compression
:::warning
These steps SHOULD be done **before** starting Continuwuity for the first time. While switching database compression midway through is theoretically possible, this has not been tested extensively in the wild.
:::
### Changing the compression algorithm
For reduced CPU usage at a tradeoff of increased storage space, consider deploying Continuwuity with the faster and less intensive `lz4` algorithm instead of `zstd` for rocksdb, and disable WAL compression entirely:
```toml
### in continuwuity.toml ###
rocksdb_compression_algo = "lz4"
rocksdb_wal_compression = "none"
```
This tweak can especially be helpful if you have an older or less performant CPU (e.g. a Raspberry Pi) and disk space to spare.
### Increasing bottommost layer compression (`zstd` only)
The bottommost layer of the database usually contains old and read-only data, so it is a suitable place for further compression. In Continuwuity, this is possible by setting `rocksdb_bottommost_compression = true` and tuning `rocksdb_bottommost_compression_level` to a more compact level than the default one used in `rocksdb_compression_level`. This tweak comes at a cost of increased CPU usage, but may prevent your database from growing too large in the long run.
For those using `zstd` compression, the compression level ranges from 1 to 22. An example like this could apply:
```toml
### in continuwuity.toml ###
rocksdb_compression_algo = "zstd"
rocksdb_compression_level = 32767 # magic number, translates to level 3 on zstd
rocksdb_bottommost_compression = true
rocksdb_bottommost_compression_level = 9 # level 9 on zstd
```
For `lz4` users, the default level (`-1`) is already the most compact. You can only further decrease it to favor compression speed over ratio.
Consult these documents for more information on compression tuning and levels:
- [Rocksdb compression documentation][rocksdb-compression]
- [Rocksdb default compression levels][rocksdb-compression-defaults]
- [Zstd manual][zstd-manual]
- [Lz4 manual][lz4-manual]
[rocksdb-compression]: https://github.com/facebook/rocksdb/wiki/Compression
[rocksdb-compression-defaults]: https://github.com/facebook/rocksdb/blob/main/include/rocksdb/options.h#L208-L217
[zstd-manual]: https://facebook.github.io/zstd/zstd_manual.html
[lz4-manual]: https://github.com/lz4/lz4/blob/release/doc/lz4_manual.html
## Other tweaks
### Using UNIX sockets
If your homeserver and reverse proxy live on the same machine, you may wish to expose Continuwuity on a UNIX socket instead of a port. This removes TCP overhead between the two programs.
<details>
<summary>Example config with Caddy</summary>
```toml
### in continuwuity.toml ###
# `address` and `port` has to be commented out first
#address = ["127.0.0.1", "::1"]
#port = 8008
unix_socket_path = "/run/continuwuity/continuwuity.sock"
```
```
### in your Caddyfile ###
https://matrix.example.com {
reverse_proxy unix//run/continuwuity/continuwuity.sock
# alternatively, use the http2-plaintext protocol
# reverse_proxy unix+h2c//run/continuwuity/continuwuity.sock
}
```
</details>
### Tuning your trusted servers
:::info Vet your trusted servers!
Trusted servers are your first point of contact when obtaining public keys from other servers, and they could theoretically impersonate other servers and cause significant harm to your deployment. Please thoroughly verify your trusted servers' credibility before adding them to your configuration.
:::
Trusted servers are queried sequentially in the order they are listed. If you have multiple trusted servers configured, put the faster ones first:
```toml
# Example config, using maintainers' recommended homeservers
trusted_servers = ["codestorm.net","starstruck.systems","unredacted.org","matrix.org"]
```
Avoid prioritising `matrix.org` as your primary trusted server, as it tends to be quite slow.
Some users have also reported that increasing `trusted_server_batch_size` has helped with faster joins for huge rooms. Start with doubling the default to `2048` until you find a suitable value.
### Enable HTTP/3 on your reverse proxy
Consider enabling the newer **HTTP/3** protocol for inbound connections to Continuwuity. In Caddy HTTP/3 is allowed by default, but you must expose port :443/**udp** on your firewall.
HTTP/3 can vastly improve Client-Server connections especially on unstable networks, as it reduces packet losses and latency from TCP head-of-line blocking, includes workarounds for network switching, and reduces connection establishment handshakes. Continuwuity also includes experimental _outbound_ HTTP/3 support in its Docker images, so connections between Continuwuity servers can benefit from this too.
+36 -8
View File
@@ -91,7 +91,7 @@ ### 3. Telling clients where to find LiveKit
To tell clients where to find LiveKit, you need to add the address of your `lk-jwt-service` to the `[global.matrix_rtc]` config section using the `foci` option.
The variable should be a list of servers serving as MatrixRTC endpoints. Clients discover these via the `/_matrix/client/v1/rtc/transports` endpoint (MSC4143).
The variable should be a list of servers serving as MatrixRTC endpoints. Replace the URL with the address you are deploying your instance of lk-jwt-service to:
```toml
[global.matrix_rtc]
@@ -100,7 +100,10 @@ ### 3. Telling clients where to find LiveKit
]
```
Remember to replace the URL with the address you are deploying your instance of lk-jwt-service to.
This will expose LiveKit information on the following endpoints for clients to discover:
- `/_matrix/client/unstable/org.matrix.msc4143/rtc/transports` (MSC4143 unstable, behind auth)
- `/.well-known/matrix/client` (fallback, not behind auth. Only enabled if `[global.well_known].client` is set)
### 4. Configure your Reverse Proxy
@@ -114,6 +117,7 @@ ### 4. Configure your Reverse Proxy
<details>
<summary>Example caddy config</summary>
```
livekit.example.com {
@@ -127,10 +131,12 @@ ### 4. Configure your Reverse Proxy
reverse_proxy 127.0.0.1:7880
}
```
</details>
<details>
<summary>Example nginx config</summary>
```
server {
server_name livekit.example.com;
@@ -167,16 +173,19 @@ ### 4. Configure your Reverse Proxy
'' close;
}
```
</details>
<details>
<summary>Example traefik router</summary>
```
# on LiveKit itself
traefik.http.routers.livekit.rule=Host(`livekit.example.com`)
# on the JWT service
traefik.http.routers.livekit-jwt.rule=Host(`livekit.example.com`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/healthz`) || PathPrefix(`/get_token`))
```
</details>
@@ -210,7 +219,7 @@ ### add these to livekit's docker-compose ###
### if you're using `network_mode: host`, you can skip this part
```
Recreate the LiveKit container (with `docker-compose up -d livekit`) to apply these changes. Remember to allow the new `3478/udp` and `50100:50200/udp` ports through your firewall.
Recreate the LiveKit container (with `docker-compose up -d livekit`) to apply these changes. Remember to allow the new `3478/udp` and `50300:50400/udp` ports through your firewall.
### Integration with an external TURN server
@@ -257,11 +266,25 @@ ## Testing
First, you will need an access token for your current login session. These can be found in your client's settings or obtained via [this website](https://timedout.uk/mxtoken.html).
Then, using that token, request another OpenID token for use with the lk-jwt-service:
Then, using that token, fetch the discovery endpoints for MatrixRTC services
```bash
~$ curl -X POST -H "Authorization: Bearer <session-access-token>" \
curl -X POST -H "Authorization: Bearer <session-access-token>" \
https://matrix.example.com/_matrix/client/unstable/org.matrix.msc4143/rtc/transports
```
In the output, you should see the LiveKit URL matching the one [configured above](#3-telling-clients-where-to-find-livekit).
With the same token, request another OpenID token for use with the lk-jwt-service:
```bash
curl -X POST -H "Authorization: Bearer <session-access-token>" \
https://matrix.example.com/_matrix/client/v3/user/@user:example.com/openid/request_token
```
You will see a response as below:
```json
{"access_token":"<openid_access_token>","token_type":"Bearer","matrix_server_name":"example.com","expires_in":3600}
```
@@ -296,10 +319,15 @@ ## Testing
```bash
~$ curl -X POST -d @payload.json https://livekit.example.com/get_token
```
The lk-jwt-service will, after checking against Continuwuity, answer with a `jwt` token to create a LiveKit media room:
```json
{"url":"wss://livekit.example.com","jwt":"a_really_really_long_string"}
```
The lk-jwt-service will, after checking against Continuwuity, answer with a `jwt` token to create a LiveKit media room. Use this token to test at the [LiveKit Connection Tester](https://livekit.io/connection-test). If everything works there, then you have set up LiveKit successfully!
Use this token to test at the [LiveKit Connection Tester](https://livekit.io/connection-test). If everything works there, then you have set up LiveKit successfully!
## Troubleshooting
@@ -363,8 +391,8 @@ ## Related Documentation
Specifications:
- [MatrixRTC proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
- [LiveKit proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4195)
- [MSC4143 - MatrixRTC proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)
- [MSC4195 - LiveKit proposal](https://github.com/matrix-org/matrix-spec-proposals/pull/4195)
Source code:
+5
View File
@@ -34,6 +34,11 @@
"name": "kubernetes",
"label": "Kubernetes"
},
{
"type": "file",
"name": "nomad",
"label": "Nomad"
},
{
"type": "file",
"name": "freebsd",
+24 -3
View File
@@ -148,7 +148,7 @@ #### For other reverse proxies
</details>
You will then need to point your reverse proxy towards Continuwuity at `127.0.0.1:8008`. See the [Other reverse proxies](generic.mdx#setting-up-the-reverse-proxy) section of the Generic page for further routing details.
See the [Other reverse proxies](generic.mdx#setting-up-the-reverse-proxy) section of the Generic page for further routing details.
### Starting Your Server
@@ -243,9 +243,30 @@ ### (Optional) Building Custom Images
[Building Docker Images](../development/index.mdx#building-docker-images)
section in the development documentation.
### Accessing the Server's Console
Before you can access the server's console and [send admin commands](../reference/admin/index.md) from the CLI, you will need to make the container interactive and allocate a pseudo-tty. Make sure you set `admin_console_automatic` to `true` in [the config](../reference/config.mdx) as well for Continuwuity to activate the CLI on startup.
For Docker Compose deployments this means adding `stdin_open: true` and `tty: true` to the container's declaration:
```yaml
services:
homeserver:
stdin_open: true
tty: true
# ...
```
If you choose to deploy via `docker run`, add the flags `-i`/`--interactive` and `-t`/`--tty` to the command.
From there you can access the server's console by running `docker attach <container-name>`, which will show the server's prompt `uwu> `. To exit `docker attach`, press `CTRL+p` then `CTRL+q`.
Note that using `CTRL+c` within `docker attach`'s context will forward the signal to the server, stopping it. See [Docker's reference][docker-attach-reference] for more information.
[docker-attach-reference]: https://docs.docker.com/reference/cli/docker/container/attach/
## Next steps
- For smooth federation, set up a caching resolver according to the [**DNS tuning guide**](../advanced/dns.mdx) (recommended)
- To set up Audio/Video communication, see the [**Calls**](../calls.mdx) page.
- If you want to set up an appservice, take a look at the [**Appservice
Guide**](../appservices.mdx).
- If you want to set up an appservice, take a look at the [**Appservice Guide**](../appservices.mdx).
+109 -105
View File
@@ -1,10 +1,12 @@
# Generic deployment documentation
> ### Getting help
>
> If you run into any problems while setting up Continuwuity, ask us in
> `#continuwuity:continuwuity.org` or [open an issue on
> Forgejo](https://forgejo.ellis.link/continuwuation/continuwuity/issues/new).
:::tip Getting help
If you run into any problems while setting up Continuwuity, ask us in
`#continuwuity:continuwuity.org` or [open an issue on
Forgejo][forgejo-new-issue].
:::
[forgejo-new-issue]: https://forgejo.ellis.link/continuwuation/continuwuity/issues/new
## Installing Continuwuity
@@ -15,17 +17,16 @@ ### Prebuilt binary
Prebuilt binaries are available from:
- **Tagged releases**: [Latest release page](https://forgejo.ellis.link/continuwuation/continuwuity/releases/latest)
- **Development builds**: CI artifacts from the `main` branch
(includes Debian/Ubuntu packages)
When browsing CI artifacts, `ci-bins` contains binaries organised
by commit hash, while `releases` contains tagged versions. Sort
by last modified date to find the most recent builds.
- **Tagged releases**: [see Release page][release-page]
- **Development builds**: CI artifacts from the `main` branch,
[see `release-image.yml` for details][release-image]
The binaries require jemalloc and io_uring on the host system. Currently
we can't cross-build static binaries - contributions are welcome here.
[release-page]: https://forgejo.ellis.link/continuwuation/continuwuity/releases/
[release-image]: https://forgejo.ellis.link/continuwuation/continuwuity/actions/?workflow=release-image.yml
#### Performance-optimised builds
For x86_64 systems with CPUs from the last ~15 years, use the
@@ -38,11 +39,12 @@ #### Performance-optimised builds
If you're using Docker instead, equivalent performance-optimised
images are available with the `-maxperf` suffix (e.g.
`forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf`).
These images use the `release-max-perf`
build profile with
[link-time optimisation (LTO)](https://doc.rust-lang.org/cargo/reference/profiles.html#lto)
These images use the `release-max-perf` build profile with
[link-time optimisation (LTO)][lto-rust-docs]
and, for amd64, target the haswell CPU architecture.
[lto-rust-docs]: https://doc.rust-lang.org/cargo/reference/profiles.html#lto
### Nix
Theres a Nix package defined in our flake, available for Linux and MacOS. Add continuwuity as an input to your flake, and use `inputs.continuwuity.packages.${system}.default` to get a working Continuwuity package.
@@ -55,7 +57,8 @@ ### Compiling
#### Using Docker
If you would like to build using docker, you can run the command `docker build -f ./docker/Dockerfile -t forgejo.ellis.link/continuwuation/continuwuity:main .` to compile continuwuity.
See the [Building Docker Images](../development/index.mdx#building-docker-images)
section in the development documentation.
#### Manual
@@ -69,7 +72,7 @@ ##### Dependencies
##### Build
You can build Continuwuity using `cargo build --release`.
You can now build Continuwuity using `cargo build --release`.
Continuwuity supports various optional features that can be enabled during compilation. Please see the Cargo.toml file for a comprehensive list, or ask in our rooms.
@@ -91,27 +94,6 @@ ## Adding a Continuwuity user
sudo useradd -r --shell /usr/bin/nologin --no-create-home continuwuity
```
## Forwarding ports in the firewall or the router
Matrix's default federation port is 8448, and clients must use port 443.
If you would like to use only port 443 or a different port, you will need to set up
delegation. Continuwuity has configuration options for delegation, or you can configure
your reverse proxy to manually serve the necessary JSON files for delegation
(see the `[global.well_known]` config section).
If Continuwuity runs behind a router or in a container and has a different public
IP address than the host system, you need to forward these public ports directly
or indirectly to the port mentioned in the configuration.
Note for NAT users: if you have trouble connecting to your server from inside
your network, check if your router supports "NAT
hairpinning" or "NAT loopback".
If your router does not support this feature, you need to research doing local
DNS overrides and force your Matrix DNS records to use your local IP internally.
This can be done at the host level using `/etc/hosts`. If you need this to be
on the network level, consider something like NextDNS or Pi-Hole.
## Setting up a systemd service
You can find an example unit for continuwuity below.
@@ -123,7 +105,7 @@ ## Setting up a systemd service
`/etc/rsyslog.conf` to allow color in logs.
If you are using a different `database_path` than the systemd unit's
configured default `/var/lib/conduwuit`, you need to add your path to the
configured default (`/var/lib/conduwuit`), you need to add your path to the
systemd unit's `ReadWritePaths=`. You can do this by either directly editing
`conduwuit.service` and reloading systemd, or by running `systemctl edit conduwuit.service`
and entering the following:
@@ -144,7 +126,9 @@ ### Example systemd Unit File
</details>
You can also [view the file on Foregejo](https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/pkg/conduwuit.service).
You can also [view the file on Foregejo][systemd-file].
[systemd-file]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/pkg/conduwuit.service
## Creating the Continuwuity configuration file
@@ -155,9 +139,7 @@ ## Creating the Continuwuity configuration file
**Please take a moment to read the config. You need to change at least the
server name.**
RocksDB is the only supported database backend.
## Setting the correct file permissions
### Setting the correct file permissions
If you are using a dedicated user for Continuwuity, you need to allow it to
read the configuration. To do this, run:
@@ -175,22 +157,29 @@ ## Setting the correct file permissions
sudo chmod 700 /var/lib/conduwuit/
```
## Setting up the Reverse Proxy
## Exposing ports in the firewall or the router
We recommend Caddy as a reverse proxy because it is trivial to use and handles TLS certificates, reverse proxy headers, etc. transparently with proper defaults.
For other software, please refer to their respective documentation or online guides.
Matrix's default federation port is **:8448**, and clients use port **:443**. You will need to
expose these ports on your firewall or router. If you use UFW, the commands to allow them
are: `ufw allow 8448/tcp` and `ufw allow 443/tcp`.
:::tip Alternative port/domain setups
If you would like to use only port 443, a different port, or a subdomain for the homeserver, you will need to set up `.well-known` delegation. Consult the `[global.well_known]` section of the config file, and the [**Delegation/Split-domain**](../advanced/delegation) page to learn more about these kinds of deployments.
:::
## Setting up the Reverse Proxy
### Caddy
After installing Caddy via your preferred method, create `/etc/caddy/conf.d/conduwuit_caddyfile`
and enter the following (substitute your actual server name):
Caddy is the recommended reverse proxy as it is easy to use, has good defaults,
and handle TLS certificates automatically. After installing Caddy via your preferred
method, create `/etc/caddy/conf.d/conduwuit_caddyfile` and enter the following
(substitute `example.com` with your actual server name):
```
your.server.name, your.server.name:8448 {
example.com, example.com:8448 {
# TCP reverse_proxy
reverse_proxy 127.0.0.1:6167
# UNIX socket
#reverse_proxy unix//run/conduwuit/conduwuit.sock
reverse_proxy 127.0.0.1:8008
}
```
@@ -202,51 +191,45 @@ ### Caddy
### Other Reverse Proxies
As we prefer our users to use Caddy, we do not provide configuration files for other proxies.
Normally, your reverse proxy should route everything from port :8448 and :443 back to Continuwuity.
You will need to reverse proxy everything under the following routes:
For more granular controls, you will need to proxy everything under these following routes:
- `/_matrix/` - core Matrix C-S and S-S APIs
- `/_conduwuit/` and/or `/_continuwuity/` - ad-hoc Continuwuity routes such as `/local_user_count` and
`/server_version`
- `/_matrix/` - core Matrix APIs, which includes:
- `/_matrix/federation` and `/_matrix/key` - core Server-Server APIs. These should be available on port :8448
- `/_matrix/client` - core Client-Server APIs. These should be available on port :443
- `/_conduwuit/` and `/_continuwuity/` - ad-hoc Continuwuity routes for password resets, email verification, and server details such as `/local_user_count` and `/server_version`.
You can optionally reverse proxy the following individual routes:
- `/.well-known/matrix/client` and `/.well-known/matrix/server` if using
Continuwuity to perform delegation (see the `[global.well_known]` config section)
- `/.well-known/matrix/support` if using Continuwuity to send the homeserver admin
contact and support page (formerly known as MSC1929)
- `/` if you would like to see `hewwo from conduwuit woof!` at the root
[contact and support page][well-known-support]
- `/` and `/_continuwuity/logo.svg` if you would like to see the Continuwuity landing page
See the following spec pages for more details on these files:
Refer to the respective software's documentation and online guides on how to do so.
- [`/.well-known/matrix/server`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixserver)
- [`/.well-known/matrix/client`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient)
- [`/.well-known/matrix/support`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixsupport)
[well-known-support]: https://spec.matrix.org/v1.18/client-server-api/#getwell-knownmatrixsupport
Examples of delegation:
#### Caveats for specific reverse proxies
- https://continuwuity.org/.well-known/matrix/server
- https://continuwuity.org/.well-known/matrix/client
- https://ellis.link/.well-known/matrix/server
- https://ellis.link/.well-known/matrix/client
For Apache and Nginx there are many examples available online.
Lighttpd is not supported as it appears to interfere with the `X-Matrix` Authorization
- Lighttpd is not supported as it appears to interfere with the `X-Matrix` Authorization
header, making federation non-functional. If you find a workaround, please share it so we can add it to this documentation.
If using Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent httpd from interfering with the `X-Matrix` header (note that Apache is not ideal as a general reverse proxy, so we discourage using it if alternatives are available).
- If using Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent httpd from interfering with the `X-Matrix` header (note that Apache is not ideal as a general reverse proxy, so we discourage using it if alternatives are available).
If using Nginx, you need to pass the request URI to Continuwuity using `$request_uri`, like this:
- If using Nginx, you need to pass the request URI to Continuwuity using `$request_uri`, like this:
- `proxy_pass http://127.0.0.1:6167$request_uri;`
- `proxy_pass http://127.0.0.1:6167;`
- `proxy_pass http://127.0.0.1:6167$request_uri;`
- `proxy_pass http://127.0.0.1:6167;`
Nginx users need to increase the `client_max_body_size` setting (default is 1M) to match the
`max_request_size` defined in conduwuit.toml.
Furthermore, Nginx users need to increase the `client_max_body_size` setting (default is 1M) to match the `max_request_size` defined in conduwuit.toml.
## You're done
## Starting Your Server
Now you can start Continuwuity with:
@@ -260,36 +243,57 @@ ## You're done
sudo systemctl enable conduwuit
```
## How do I know it works?
You can open [a Matrix client](https://matrix.org/ecosystem/clients), enter your
homeserver address, and try to register.
You can also use these commands as a quick health check (replace
`your.server.name`).
Check Continuwuity logs with the following command:
```bash
curl https://your.server.name/_conduwuit/server_version
# If using port 8448
curl https://your.server.name:8448/_conduwuit/server_version
# If federation is enabled
curl https://your.server.name:8448/_matrix/federation/v1/version
sudo journalctl -u conduwuit.service
```
- To check if your server can communicate with other homeservers, use the
[Matrix Federation Tester](https://federationtester.mtrnord.blog/). If you can
register but cannot join federated rooms, check your configuration and verify
that port 8448 is open and forwarded correctly.
If Continuwuity has successfully initialized, you'll see output as below.
```
In order to use your new homeserver, you need to create its
first user account.
Open your Matrix client of choice and register an account
on example.com using registration token x5keUZ811RqvLsNa .
Pick your own username and password!
```
You can then open [a Matrix client][matrix-clients],
enter your homeserver address, and try to register with the provided token.
By default, the first user is the instance's first admin. They will be added
to the `#admin:example.com` room and be able to [issue admin commands](../reference/admin/index.md).
[matrix-clients]: https://matrix.org/ecosystem/clients
## How do I know it works?
To check if your server can communicate with other homeservers,
use an external testing tool:
- [Matrix Connectivity Tester](https://federationtester.mtrnord.blog/)
- [Matrix Federation Tester](https://federationtester.matrix.org/)
If you can register your account but cannot join federated rooms, check your configuration
and verify that your federation endpoints are opened and forwarded correctly.
As a quick health check, you can also use these cURL commands:
```bash
curl https://example.com/_conduwuit/server_version
# If using port 8448
curl https://example.com:8448/_conduwuit/server_version
# If federation is enabled
curl https://example.com:8448/_matrix/federation/v1/version
# For client-server endpoints
curl https://example.com/_matrix/client/versions
```
## What's next?
### Audio/Video calls
For Audio/Video call functionality see the [Calls](../calls.md) page.
### Appservices
If you want to set up an appservice, take a look at the [Appservice
Guide](../appservices.md).
- For smooth federation, set up a caching resolver according to the [**DNS tuning guide**](../advanced/dns.mdx) (recommended)
- For Audio/Video call functionality see the [**Calls**](../calls.md) page.
- If you want to set up an appservice, take a look at the [**Appservice Guide**](../appservices.md).
+118
View File
@@ -0,0 +1,118 @@
# Continuwuity for Nomad
You can either pass the configuration as environment variables or mount a file containing the configuration from consul.
This given configuration assumes that you have a traefik reverse proxy running.
## Persistence
The database being a RockDB file, it is recommended to use a volume to persist the data.
The example below uses a volume, you need to configure the CSI driver on your cluster.
| Volume Name | Mount Path | Purpose |
|-------------|------------|---------|
| continuwuity-volume | `/var/lib/continuwuity` | Store the database |
| continuwuity-media-volume | `/var/lib/continuwuity/media` | Store uploaded media |
## Configuration
### Using environment variables
```hcl
job "continuwuity" {
datacenters = ["dc1"]
type = "service"
node_pool = "default"
group "continuwuity" {
count = 1
network {
port "http" {
static = 6167
}
}
service {
name = "continuwuity"
port = "http"
tags = [
"traefik.enable=true",
"traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))",
"traefik.http.routers.continuwuity.entrypoints=https",
"traefik.http.routers.continuwuity.tls=true",
"traefik.http.routers.continuwuity.tls.certresolver=letsencrypt",
"traefik.http.routers.continuwuity-http.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))",
"traefik.http.routers.continuwuity-http.entrypoints=http",
"traefik.http.routers.continuwuity-http.middlewares=continuwuity-redirect",
"traefik.http.middlewares.continuwuity-redirect.redirectscheme.scheme=https",
"traefik.http.middlewares.continuwuity-redirect.redirectscheme.permanent=true",
]
}
volume "continuwuity-volume" {
type = "csi"
read_only = false
source = "continuwuity-volume"
attachment_mode = "file-system"
access_mode = "single-node-writer"
per_alloc = false
}
volume "continuwuity-media-volume" {
type = "csi"
read_only = false
source = "continuwuity-media-volume"
attachment_mode = "file-system"
access_mode = "single-node-writer"
per_alloc = false
mount_options {
mount_flags = []
}
}
task "continuwuity" {
driver = "docker"
env {
CONTINUWUITY_SERVER_NAME = "matrix.example.com"
CONTINUWUITY_TRUSTED_SERVERS = "[\"matrix.org\", \"mozilla.org\"]"
CONTINUWUITY_ALLOW_REGISTRATION = false
CONTINUWUITY_ADDRESS = "0.0.0.0"
CONTINUWUITY_PORT = 6167
CONTINUWUITY_DATABASE_PATH = "/var/lib/continuwuity"
CONTINUWUITY_WELL_KNOWN = <<EOF
{
client=https://matrix.example.com,
server=matrix.example.com:443
}
EOF
}
config {
image = "forgejo.ellis.link/continuwuation/continuwuity:latest"
ports = ["http"]
}
volume_mount {
volume = "continuwuity-volume"
destination = "/var/lib/continuwuity"
}
volume_mount {
volume = "continuwuity-media-volume"
destination = "/var/lib/continuwuity/media"
}
}
}
}
```
### Using consul
```hcl
...
template {
data = <<EOF
{{key "config/continuwuity"}}
EOF
destination = "local/conduwuit.toml"
}
...
```
-2
View File
@@ -81,8 +81,6 @@ ## List of forked dependencies
All forked dependencies are maintained under the
[continuwuation organization on Forgejo](https://forgejo.ellis.link/continuwuation):
- [ruwuma][continuwuation-ruwuma] - Fork of [ruma/ruma][ruma] with various
performance improvements, more features and better client/server interop
- [rocksdb][continuwuation-rocksdb] - Fork of [facebook/rocksdb][rocksdb] via
[`@zaidoon1`][8] with liburing build fixes and GCC debug build fixes
- [jemallocator][continuwuation-jemallocator] - Fork of
@@ -6,10 +6,10 @@
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
},
{
"id": 11,
"mention_room": false,
"date": "2026-04-17",
"message": "[v0.5.7](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.7) is out! Email verification! Terms and Conditions! Deleting notification pushers! So much good stuff. Go grab the release and read the changelog!"
"id": 13,
"mention_room": true,
"date": "2026-05-08",
"message": "[v0.5.9](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.9) has been released, fixing a few low-severity federation-related vulnerabilities. It is recommended you read the changelog and update as soon as possible. There are no new features or other changes in this release, only related bugfixes. Deployments tracking the main branch should also update to the latest commit."
}
]
}
@@ -0,0 +1,43 @@
# Continuwuity - Behind Traefik Reverse Proxy
services:
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
#- ./continuwuity.toml:/etc/continuwuity.toml
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))"
- "traefik.http.routers.continuwuity.entrypoints=websecure" # your HTTPS entry point
- "traefik.http.routers.continuwuity.tls=true"
- "traefik.http.routers.continuwuity.service=continuwuity"
- "traefik.http.services.continuwuity.loadbalancer.server.port=8008"
# possibly, depending on your config:
# - "traefik.http.routers.continuwuity.tls.certresolver=letsencrypt"
environment:
CONTINUWUITY_SERVER_NAME: example.com # EDIT THIS
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008 # This must match with traefik's loadbalancer label
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
# Serve .well-known files to tell others to reach Continuwuity on port :443
CONTINUWUITY_WELL_KNOWN: |
{
client=https://matrix.example.com,
server=matrix.example.com:443
}
volumes:
db:
networks:
# This must match the network name that Traefik listens on
proxy:
external: true
@@ -0,0 +1,54 @@
# Continuwuity - With Caddy Labels
services:
caddy:
# This compose file uses caddy-docker-proxy as the reverse proxy for Continuwuity!
# For more info, visit https://github.com/lucaslorentz/caddy-docker-proxy
image: "docker.io/lucaslorentz/caddy-docker-proxy:ci-alpine"
ports:
- 80:80
- 443:443
environment:
- CADDY_INGRESS_NETWORKS=caddy
networks:
- caddy
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/data
restart: unless-stopped
labels:
caddy: example.com
caddy.reverse_proxy: /.well-known/matrix/* homeserver:8008
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
#- ./continuwuity.toml:/etc/continuwuity.toml
environment:
CONTINUWUITY_SERVER_NAME: example.com # EDIT THIS
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
# Serve .well-known files to tell others to reach Continuwuity on port :443
CONTINUWUITY_WELL_KNOWN: |
{
client=https://matrix.example.com,
server=matrix.example.com:443
}
networks:
- caddy
labels:
caddy: matrix.example.com
caddy.reverse_proxy: "{{upstreams 8008}}"
volumes:
db:
networks:
caddy:
@@ -0,0 +1,57 @@
# Continuwuity - Using Caddy Docker Image
services:
caddy:
image: "docker.io/caddy:latest"
ports:
- 80:80
- 443:443
networks:
- caddy
volumes:
- ./data:/data
restart: unless-stopped
configs:
- source: Caddyfile
target: /etc/caddy/Caddyfile
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
#- ./continuwuity.toml:/etc/continuwuity.toml
environment:
CONTINUWUITY_SERVER_NAME: example.com
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
## Serve .well-known files to tell others to reach Continuwuity on port :443
CONTINUWUITY_WELL_KNOWN: |
{
client=https://matrix.example.com,
server=matrix.example.com:443
}
networks:
- caddy
networks:
caddy:
volumes:
db:
configs:
Caddyfile:
content: |
https://matrix.example.com:443 {
reverse_proxy http://homeserver:8008
}
https://example.com:443 {
reverse_proxy /.well-known/matrix* http://homeserver:8008
}
@@ -0,0 +1,85 @@
# Continuwuity - With Traefik Reverse Proxy
services:
homeserver:
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
#- ./continuwuity.toml:/etc/continuwuity.toml
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))"
- "traefik.http.routers.continuwuity.entrypoints=websecure"
- "traefik.http.routers.continuwuity.tls.certresolver=letsencrypt"
- "traefik.http.services.continuwuity.loadbalancer.server.port=8008"
environment:
CONTINUWUITY_SERVER_NAME: example.com # EDIT THIS
CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity
CONTINUWUITY_ADDRESS: 0.0.0.0
CONTINUWUITY_PORT: 8008 # This must match with traefik's loadbalancer label
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
# Serve .well-known files to tell others to reach Continuwuity on port :443
CONTINUWUITY_WELL_KNOWN: |
{
client=https://matrix.example.com,
server=matrix.example.com:443
}
traefik:
image: "docker.io/traefik:latest"
container_name: "traefik"
restart: "unless-stopped"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "acme:/etc/traefik/acme"
labels:
- "traefik.enable=true"
# middleware redirect
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
# global redirect to https
- "traefik.http.routers.redirs.rule=hostregexp(`{host:.+}`)"
- "traefik.http.routers.redirs.entrypoints=web"
- "traefik.http.routers.redirs.middlewares=redirect-to-https"
environment:
TRAEFIK_LOG_LEVEL: DEBUG
TRAEFIK_ENTRYPOINTS_WEB: true
TRAEFIK_ENTRYPOINTS_WEB_ADDRESS: ":80"
TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_TO: websecure
TRAEFIK_ENTRYPOINTS_WEBSECURE: true
TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS: ":443"
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_TLS_CERTRESOLVER: letsencrypt
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT: true
# CHANGE THIS to desired email for ACME
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL: user@example.com
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE: true
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT: web
TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE: "/etc/traefik/acme/acme.json"
# Since Traefik 3.6.3, paths with certain "encoded characters" are now blocked by default; we need a couple, or else things *will* break
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSLASH: true
TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDHASH: true
TRAEFIK_PROVIDERS_DOCKER: true
TRAEFIK_PROVIDERS_DOCKER_ENDPOINT: "unix:///var/run/docker.sock"
TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT: false
volumes:
db:
acme:
networks:
proxy:
@@ -2,7 +2,7 @@
services:
homeserver:
image: forgejo.ellis.link/continuwuation/continuwuity:latest
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
@@ -38,7 +38,6 @@ volumes:
db:
networks:
# This is the network Traefik listens to, if your network has a different
# name, don't forget to change it here and in the docker-compose.override.yml
# This must match the network name that Traefik listens on
proxy:
external: true
@@ -1,10 +1,10 @@
# Continuwuity - Traefik Reverse Proxy Labels
# Continuwuity - Traefik Reverse Proxy Labels (override file)
services:
homeserver:
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy" # Change this to the name of your Traefik docker proxy network
- "traefik.docker.network=proxy" # Change this to the name of your Traefik docker proxy network
- "traefik.http.routers.to-continuwuity.rule=Host(`example.com`)" # Change to the address on which Continuwuity is hosted
- "traefik.http.routers.to-continuwuity.tls=true"
@@ -14,13 +14,10 @@ services:
# This must match with CONTINUWUITY_PORT (default: 8008)
- "traefik.http.services.to_continuwuity.loadbalancer.server.port=8008"
- "traefik.http.middlewares.cors-headers.headers.accessControlAllowOriginList=*"
- "traefik.http.middlewares.cors-headers.headers.accessControlAllowHeaders=Origin, X-Requested-With, Content-Type, Accept, Authorization"
- "traefik.http.middlewares.cors-headers.headers.accessControlAllowMethods=GET, POST, PUT, DELETE, OPTIONS"
# If you want to have your account on <DOMAIN>, but host Continuwuity on a subdomain,
# you can let it only handle the well known file on that domain instead
#- "traefik.http.routers.to-matrix-wellknown.rule=Host(`example.com`) && PathPrefix(`/.well-known/matrix`)"
# you can let it only handle the well known file on the base domain instead
#
# - "traefik.http.routers.to-matrix-wellknown.rule=Host(`example.com`) && PathPrefix(`/.well-known/matrix`)"
#- "traefik.http.routers.to-matrix-wellknown.tls=true"
#- "traefik.http.routers.to-matrix-wellknown.tls.certresolver=letsencrypt"
#- "traefik.http.routers.to-matrix-wellknown.middlewares=cors-headers@docker"
@@ -1,8 +1,10 @@
# Continuwuity - With Caddy Labels
services:
caddy:
# This compose file uses caddy-docker-proxy as the reverse proxy for Continuwuity!
# For more info, visit https://github.com/lucaslorentz/caddy-docker-proxy
image: lucaslorentz/caddy-docker-proxy:ci-alpine
image: "docker.io/lucaslorentz/caddy-docker-proxy:ci-alpine"
ports:
- 80:80
- 443:443
@@ -16,7 +18,7 @@ services:
restart: unless-stopped
homeserver:
image: forgejo.ellis.link/continuwuation/continuwuity:latest
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
@@ -1,6 +1,8 @@
# Continuwuity - Using Caddy Docker Image
services:
caddy:
image: docker.io/caddy:latest
image: "docker.io/caddy:latest"
ports:
- 80:80
- 443:443
@@ -15,7 +17,7 @@ services:
target: /etc/caddy/Caddyfile
homeserver:
image: forgejo.ellis.link/continuwuation/continuwuity:latest
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
@@ -37,7 +39,6 @@ services:
# server=example.com:443
# }
networks:
- caddy
@@ -48,8 +49,8 @@ volumes:
db:
configs:
dynamic.yml:
Caddyfile:
content: |
https://example.com, https://example.com:8448 {
https://example.com:443, https://example.com:8448 {
reverse_proxy http://homeserver:8008
}
@@ -1,8 +1,8 @@
# Continuwuity - Behind Traefik Reverse Proxy
# Continuwuity - With Traefik Reverse Proxy
services:
homeserver:
image: forgejo.ellis.link/continuwuation/continuwuity:latest
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
volumes:
@@ -32,14 +32,14 @@ services:
}
traefik:
image: "traefik:latest"
image: "docker.io/traefik:latest"
container_name: "traefik"
restart: "unless-stopped"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:z"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "acme:/etc/traefik/acme"
labels:
- "traefik.enable=true"
@@ -52,6 +52,7 @@ services:
- "traefik.http.routers.redirs.middlewares=redirect-to-https"
environment:
TRAEFIK_LOG_LEVEL: DEBUG
TRAEFIK_ENTRYPOINTS_WEB: true
TRAEFIK_ENTRYPOINTS_WEB_ADDRESS: ":80"
+13 -3
View File
@@ -1,12 +1,23 @@
# Continuwuity
# Continuwuity - Bare Configuration (for other reverse proxies)
services:
homeserver:
image: forgejo.ellis.link/continuwuation/continuwuity:latest
image: "forgejo.ellis.link/continuwuation/continuwuity:latest"
restart: unless-stopped
command: /sbin/conduwuit
ports:
# If your reverse proxy is on the host, use this
# and configure it to connect to `127.0.0.1:8008`
- 127.0.0.1:8008:8008
# If your reverse proxy is on another machine, use this
# and configure it to connect to <this-machine-ip>:8008
# - 8008:8008
# If your reverse proxy is a docker container on the same network,
# comment out the entire `ports` section, and configure it to connect to `continuwuity:8008`
volumes:
- db:/var/lib/continuwuity
- ./continuwuity-resolv.conf:/etc/resolv.conf # use custom resolvers rather than Docker's
@@ -26,6 +37,5 @@ services:
# server=example.com:443
# }
volumes:
db:
Generated
+15 -15
View File
@@ -3,11 +3,11 @@
"advisory-db": {
"flake": false,
"locked": {
"lastModified": 1775907537,
"narHash": "sha256-vbeLNgmsx1Z6TwnlDV0dKyeBCcon3UpkV9yLr/yc6HM=",
"lastModified": 1777645914,
"narHash": "sha256-P1T7QVQS13OvkXEuEhI91CLaQfyv6iqV9vW8IBLLDYg=",
"owner": "rustsec",
"repo": "advisory-db",
"rev": "d99f7b9eb81731bddebf80a355f8be7b2f8b1b28",
"rev": "d6ba1f7070ba91f45efe372d68eb648be67d0417",
"type": "github"
},
"original": {
@@ -18,11 +18,11 @@
},
"crane": {
"locked": {
"lastModified": 1775839657,
"narHash": "sha256-SPm9ck7jh3Un9nwPuMGbRU04UroFmOHjLP56T10MOeM=",
"lastModified": 1777335812,
"narHash": "sha256-bEg5xoAxAwsyfnGhkEX7RJViTIBIYPd8ISg4O1c0HFc=",
"owner": "ipetkov",
"repo": "crane",
"rev": "7cf72d978629469c4bd4206b95c402514c1f6000",
"rev": "5e0fb2f64edff2822249f21293b8304dedaaf676",
"type": "github"
},
"original": {
@@ -39,11 +39,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1775891769,
"narHash": "sha256-EOfVlTKw2n8w1uhfh46GS4hEGnQ7oWrIWQfIY6utIkI=",
"lastModified": 1777624102,
"narHash": "sha256-thSyElkje577x/kAbP72nHlfiFc1a+tCudskLPHXe9s=",
"owner": "nix-community",
"repo": "fenix",
"rev": "6fbc54dde15aee725bdc7aae5e478849685d5f56",
"rev": "4d81601e0b73f20d81d066754ad0e7d1e7f75a06",
"type": "github"
},
"original": {
@@ -89,11 +89,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1775710090,
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
"lastModified": 1777268161,
"narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"type": "github"
},
"original": {
@@ -132,11 +132,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1775843361,
"narHash": "sha256-j53ZgyDvmYf3Sjh1IPvvTjqa614qUfVQSzj59+MpzkY=",
"lastModified": 1777583169,
"narHash": "sha256-dVJ4+wrRKc8oIgp3rLOFSq1obt/sCKlXy3h47qof/w0=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "9eb97ea96d8400e8957ddd56702e962614296583",
"rev": "aa64e4828a2bbba44463c1229a81c748d3cce583",
"type": "github"
},
"original": {
+130 -151
View File
@@ -16,26 +16,24 @@
}
},
"node_modules/@emnapi/core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -47,7 +45,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -109,9 +106,9 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
"integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -128,13 +125,13 @@
}
},
"node_modules/@rsbuild/core": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.0-rc.1.tgz",
"integrity": "sha512-eqxtRlQiFSm/ibCNGiPj8ozsGSNK91NY+GksmPuTCPmWQExGtPqM1V+s13UYeWZS6fYbMRs7NlQKD896e0QkKA==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.5.tgz",
"integrity": "sha512-KajO50hbXb32S8MsyDh2f+xKcVeRy9Gfzdcy0JjpMLj22djHugly6jrGo7jH7ls9X6/TDcyCTncSuNK4+D2lTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/core": "2.0.0-rc.1",
"@rspack/core": "~2.0.2",
"@swc/helpers": "^0.5.21"
},
"bin": {
@@ -153,17 +150,17 @@
}
},
"node_modules/@rsbuild/plugin-react": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/@rsbuild/plugin-react/-/plugin-react-1.4.6.tgz",
"integrity": "sha512-LAT6xHlEyZKA0VjF/ph5d50iyG+WSmBx+7g98HNZUwb94VeeTMZFB8qVptTkbIRMss3BNKOXmHOu71Lhsh9oEw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@rsbuild/plugin-react/-/plugin-react-2.0.0.tgz",
"integrity": "sha512-/1gzt39EGUSFEqB83g46QoOwsgv172HI18i6au1b6lgIaX4sv9stuX4ijdHbHCp8PqYEq+MyQ99jIQMO6I+etg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/plugin-react-refresh": "^1.6.1",
"@rspack/plugin-react-refresh": "2.0.0",
"react-refresh": "^0.18.0"
},
"peerDependencies": {
"@rsbuild/core": "^1.0.0 || ^2.0.0-0"
"@rsbuild/core": "^2.0.0-0"
},
"peerDependenciesMeta": {
"@rsbuild/core": {
@@ -172,28 +169,28 @@
}
},
"node_modules/@rspack/binding": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.0-rc.1.tgz",
"integrity": "sha512-rhJqtbyiRPOjTAZW0xTZFbOrS5yP5yL1SF0DPE9kvFfzePz30IqjMDMxL0KuhkDZd/M1eUINJyoqd8NTbR9wHw==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.2.tgz",
"integrity": "sha512-0kZPplW9GWx8mfC6DfsaRY3QBIYPuUs42JfmSM6aSb8tMHZAXQeLeMB8M+h8i4SeI+aFtCgO6UuYGtyWf7+L+A==",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"@rspack/binding-darwin-arm64": "2.0.0-rc.1",
"@rspack/binding-darwin-x64": "2.0.0-rc.1",
"@rspack/binding-linux-arm64-gnu": "2.0.0-rc.1",
"@rspack/binding-linux-arm64-musl": "2.0.0-rc.1",
"@rspack/binding-linux-x64-gnu": "2.0.0-rc.1",
"@rspack/binding-linux-x64-musl": "2.0.0-rc.1",
"@rspack/binding-wasm32-wasi": "2.0.0-rc.1",
"@rspack/binding-win32-arm64-msvc": "2.0.0-rc.1",
"@rspack/binding-win32-ia32-msvc": "2.0.0-rc.1",
"@rspack/binding-win32-x64-msvc": "2.0.0-rc.1"
"@rspack/binding-darwin-arm64": "2.0.2",
"@rspack/binding-darwin-x64": "2.0.2",
"@rspack/binding-linux-arm64-gnu": "2.0.2",
"@rspack/binding-linux-arm64-musl": "2.0.2",
"@rspack/binding-linux-x64-gnu": "2.0.2",
"@rspack/binding-linux-x64-musl": "2.0.2",
"@rspack/binding-wasm32-wasi": "2.0.2",
"@rspack/binding-win32-arm64-msvc": "2.0.2",
"@rspack/binding-win32-ia32-msvc": "2.0.2",
"@rspack/binding-win32-x64-msvc": "2.0.2"
}
},
"node_modules/@rspack/binding-darwin-arm64": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.0-rc.1.tgz",
"integrity": "sha512-fYbeDDDg6QKZzXYt/J0/j0Qhr01wQLuISUsYnNhu5MLwdXVUSVcqz+CTqgF3d0EQVVn6FqLV63lbNRzUGfSq9g==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.2.tgz",
"integrity": "sha512-0o7lbgBBsDlICWdjIH0q3e0BsSco4GRiImHWVfZSVEG+q2+ykZJvSvYCVhPM1Co375Z0S3VMPa/8SjcY1FHwlw==",
"cpu": [
"arm64"
],
@@ -205,9 +202,9 @@
]
},
"node_modules/@rspack/binding-darwin-x64": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.0-rc.1.tgz",
"integrity": "sha512-MvXi9kr8xXn1y0PD1WI/4YphRNOdbykJjKdEsAG4JxEVoERmhIHOTwKvUqlejajizAwlVZcxQl/FacoPLsKN5Q==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.2.tgz",
"integrity": "sha512-tOwxZpoPlTlRs/w6UyUinXJ4TYRVHMlR7+eQxO1R3muKpixvhXQjtvoaY16HuFyTVky5F0IfOoWr3x9FEsgdLg==",
"cpu": [
"x64"
],
@@ -219,9 +216,9 @@
]
},
"node_modules/@rspack/binding-linux-arm64-gnu": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.0-rc.1.tgz",
"integrity": "sha512-j6WsHEwGSdUoiy4BsQBW0RjFl+MBzozdybSYhkiyVSoHlbm7CPt3XaaS3elH5YcwuLHORmVHPP91QhwWl9UFJg==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.2.tgz",
"integrity": "sha512-1ZD4YFhG1rmgqj+W8hfwHyKV8xDxGsc/3KgU0FwmiVEX7JfzhCkgBO/xlCG79kRKSrzuVzt4icO/G3cCKn0pag==",
"cpu": [
"arm64"
],
@@ -236,9 +233,9 @@
]
},
"node_modules/@rspack/binding-linux-arm64-musl": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.0-rc.1.tgz",
"integrity": "sha512-MPoZE0aS8oH+Wr0R5tIYch8gbUwYYf4LsiGdP6enMKMTrmpJyOVGlhPHVSwsrFgBg7fjTGOuxHuibtsvDUdLOQ==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.2.tgz",
"integrity": "sha512-/PtTkM/DsDLjeuXTmeJeRfbjCDbcL9jvoVgZrgxYFZ28y2cdLvbChbW9uigOzs5dQEs1CIBQXMTTj7KhdBTuQg==",
"cpu": [
"arm64"
],
@@ -253,9 +250,9 @@
]
},
"node_modules/@rspack/binding-linux-x64-gnu": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.0-rc.1.tgz",
"integrity": "sha512-gOlPCwtIg9GsFG/8ZdUyV5SyXDaGq2kmtXmyyFU7RO33MaalltNEBMf2hevRPj9z39eSzxwgJDonMOdx5Fo0Og==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.2.tgz",
"integrity": "sha512-bBjsZxMHRaPo6X9SokApm6ucs+UhXtAJFyJJyuk2BH4XJsLeCU9Dz1vMwioeohFbJUUeTASVPm6/BL+RhSaunw==",
"cpu": [
"x64"
],
@@ -270,9 +267,9 @@
]
},
"node_modules/@rspack/binding-linux-x64-musl": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.0-rc.1.tgz",
"integrity": "sha512-K6Swk1rfP4z4b6bp84NlikGlUWMOPpIWCtlPr/W0TWgc2C/cd844oHdoIu7WtmOH7y9AwB5UG2bWpgFAVwykCw==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.2.tgz",
"integrity": "sha512-HjlpInqzabDNkhVsUJpsHPqa9QYVWBViJoyWNjzXCAW0vKMDvwaphyUvokSinX8FGTlZi/sr5UEaHJo6XtQ35g==",
"cpu": [
"x64"
],
@@ -287,9 +284,9 @@
]
},
"node_modules/@rspack/binding-wasm32-wasi": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.0-rc.1.tgz",
"integrity": "sha512-aa9oUTqOb1QjwsHVlMr5sV+7mcBI4MLQ/xhFO2CIEcfVnJIPl8XpKUbDEgqMwcFlzcgzKmHg5cVmIvd82BLgow==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.2.tgz",
"integrity": "sha512-YaRYNFLJRpkGfYjSWR7n9f+nQKtrlmrrffpAn/blc2geHcRvXoBc5SCs1idPtsLhj7H9qWWhs7ucjyHy4csWFg==",
"cpu": [
"wasm32"
],
@@ -297,13 +294,15 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@napi-rs/wasm-runtime": "1.1.2"
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "1.1.4"
}
},
"node_modules/@rspack/binding-win32-arm64-msvc": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.0-rc.1.tgz",
"integrity": "sha512-+UxF0c7E9bE3siFbMHi+mmoeQJzcTKl1j3x+Y6MY/PJ3V70cU23wOaxMvmSsCyq2JNJBT2RCNZ9HaL+o3kReug==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.2.tgz",
"integrity": "sha512-d/3kTEKq+asLjRFPO96t+wfWiM7DLN76VQEPDD9bc1kdsZXlVJBuvyXfsgK8bbEvKplWXYcSsokhmEnuXrLOpg==",
"cpu": [
"arm64"
],
@@ -315,9 +314,9 @@
]
},
"node_modules/@rspack/binding-win32-ia32-msvc": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.0-rc.1.tgz",
"integrity": "sha512-gc0JdkdxSWo+o/b1qTCT6mZ3DrlGe32eW+Ps3xInxcG4UHjUG7hTDgFtOgVQ6VhQ8WMUXG+TQOz0CySVpYjsoQ==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.2.tgz",
"integrity": "sha512-161cWineq3RW+Jdm1FAfSpXeUtYWvhB3kAbm46vNT9h/YYz+spwsFMvveAZ1nsVSVL0IC5lDBGUte7yUAY8K2g==",
"cpu": [
"ia32"
],
@@ -329,9 +328,9 @@
]
},
"node_modules/@rspack/binding-win32-x64-msvc": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.0-rc.1.tgz",
"integrity": "sha512-Dnj0jthyVUikf65MGEyZy3akshtSmR1xsp/Xr0h/NWTo5JFWHKAFNYFE+jFfY0uzC8e4IDcLQLYoFomqV1DsEg==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.2.tgz",
"integrity": "sha512-y7Q0S1FE+OlkL5GMqLG0PwxrPw6E1r892KhGrGKE1Vdufe5YTEx6xTPxzZ+b7N2KPD7s9G1/iJmWHQxb1+Bjkg==",
"cpu": [
"x64"
],
@@ -343,13 +342,13 @@
]
},
"node_modules/@rspack/core": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.0-rc.1.tgz",
"integrity": "sha512-OIfkYn05/IWtVIdZ8Y/a0y/k4ipzqfApxIZqnJM59G/bGwQKMBrLHpOMGgV2Wmq1j9UMXzF7ZtsFMUbYBhFb9A==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.2.tgz",
"integrity": "sha512-VM3UHOo26uC+4QSqY5tU1ybI7KuXY5rTof8nhFOaBY9SYau0Smvr+hMSAPmrmHwknB6dXT8yaNVxrj7I+qxE1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/binding": "2.0.0-rc.1"
"@rspack/binding": "2.0.2"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
@@ -368,39 +367,36 @@
}
},
"node_modules/@rspack/plugin-react-refresh": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.6.2.tgz",
"integrity": "sha512-k+/VrfTNgo+KirjI6V+8CWRj6y+DH9jOUWv8JorYY4vKf/9xfnZ8xHzuB4iqCwTtoZl9YnxOaOuoyjJipc2tiQ==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-2.0.0.tgz",
"integrity": "sha512-Cf6CxBStNDJbiXMc/GmsvG1G8PRlUpa0MSfWsMTI+e8npzuTN/p8nwLs3shriBZOLciqgkSZpBtPTd10BLpj1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"error-stack-parser": "^2.1.4"
},
"peerDependencies": {
"react-refresh": ">=0.10.0 <1.0.0",
"webpack-hot-middleware": "2.x"
"@rspack/core": "^2.0.0-0",
"react-refresh": ">=0.10.0 <1.0.0"
},
"peerDependenciesMeta": {
"webpack-hot-middleware": {
"@rspack/core": {
"optional": true
}
}
},
"node_modules/@rspress/core": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.9.tgz",
"integrity": "sha512-cfbqqbWtdimrWIsfeyPnQOTKwJpdNLr8VnwLIL4JYC2ZcRq+xcInpszLXVpV86nONL6qI19usr2Or7uzZJ+ynA==",
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.11.tgz",
"integrity": "sha512-4YBOFmSMFv5GWrCa80qSIW8VxqZQQS/PknVq2r7Hb7kgfB38Fzciopn3hjb3hNwI4TTRbsi/Jev2HyRWD4bYAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@rsbuild/core": "2.0.0-rc.1",
"@rsbuild/plugin-react": "~1.4.6",
"@rspress/shared": "2.0.9",
"@rsbuild/core": "^2.0.5",
"@rsbuild/plugin-react": "~2.0.0",
"@rspress/shared": "2.0.11",
"@shikijs/rehype": "^4.0.2",
"@types/unist": "^3.0.3",
"@unhead/react": "^2.1.13",
"@unhead/react": "^2.1.15",
"body-scroll-lock": "4.0.0-beta.0",
"clsx": "2.1.1",
"copy-to-clipboard": "^3.3.3",
@@ -411,12 +407,12 @@
"mdast-util-mdxjs-esm": "^2.0.1",
"medium-zoom": "1.1.0",
"nprogress": "^0.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-lazy-with-preload": "^2.2.1",
"react-reconciler": "0.33.0",
"react-render-to-markdown": "19.0.1",
"react-router-dom": "^7.13.2",
"react-router-dom": "^7.15.0",
"rehype-external-links": "^3.0.0",
"rehype-raw": "^7.0.0",
"remark-cjk-friendly": "^2.0.1",
@@ -440,39 +436,39 @@
}
},
"node_modules/@rspress/plugin-client-redirects": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.9.tgz",
"integrity": "sha512-r2GyHzOSt8CeS4UIsy/cPM5Zotekt1JVQFmgOYGapvll5ktUlVcd77HLtXDbZjtpgtj0XlaMLrXueOpV2gsBoQ==",
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.11.tgz",
"integrity": "sha512-DI9vod5mGccg57c19CuFpN3mGP1FEEueOUnEUz1UHXSyXg9YTj+ox7Xla4jUUzAzoPVGiWSSsfbtCTwdoxAsbg==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rspress/core": "^2.0.9"
"@rspress/core": "^2.0.10"
}
},
"node_modules/@rspress/plugin-sitemap": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.9.tgz",
"integrity": "sha512-GTuXuySaeaazUZoUxdk2vZ8p0ehIgulPjCP9C7gDg6lIh5JGpUbcjG4def4tWHsxUoKp2rIwu/93bHwKb8T0Mw==",
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.11.tgz",
"integrity": "sha512-046LCHgbJXdaPipWB2SWMjZcAtIrOjXGZOD92xlTjhZ74D7Mk1Nod1MQdtOEoISWedcHdgpUVXMDbB1doKBpPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rspress/core": "^2.0.9"
"@rspress/core": "^2.0.10"
}
},
"node_modules/@rspress/shared": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.9.tgz",
"integrity": "sha512-G48n3pC7AVAR58pLqwClUCYj5Nt7ZgYEStR8VTBGFuPgXtzb3+KPfo/gz0hb6wxdKJ1cL5ohPsZ6EXqllu6lew==",
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.11.tgz",
"integrity": "sha512-7l5Pso4s597utJyisVEnd7n/40h053nfE8DwGQMeS8RLGtSwVgxFwNHsSrvQEGtFlLrg2aWWSITqnAVO1wfTew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rsbuild/core": "2.0.0-rc.1",
"@rsbuild/core": "^2.0.5",
"@shikijs/rehype": "^4.0.2",
"unified": "^11.0.5"
}
@@ -614,9 +610,9 @@
}
},
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -711,13 +707,13 @@
"license": "ISC"
},
"node_modules/@unhead/react": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.13.tgz",
"integrity": "sha512-gC48tNJ0UtbithkiKCc2WUlxbVVk5o171EtruS2w2hQUblfYFHzCPu2hljjT1e0tUHXXqN8EMv7mpxHddMB2sg==",
"version": "2.1.15",
"resolved": "https://registry.npmjs.org/@unhead/react/-/react-2.1.15.tgz",
"integrity": "sha512-5hfAaZ3XJq9JkspRzZdSPsMrXXA8v/SKiEOxZcN9L40o44byF/50bcQuOLgSSCAx8802mI5VG32KZXWTtsLu9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"unhead": "2.1.13"
"unhead": "2.1.15"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
@@ -972,16 +968,6 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/esast-util-from-estree": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz",
@@ -1413,9 +1399,9 @@
}
},
"node_modules/hookable": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.0.tgz",
"integrity": "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==",
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz",
"integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==",
"dev": true,
"license": "MIT"
},
@@ -2697,20 +2683,20 @@
"license": "MIT"
},
"node_modules/oniguruma-parser": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
"integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
"dev": true,
"license": "MIT"
},
"node_modules/oniguruma-to-es": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz",
"integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==",
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
"integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
"dev": true,
"license": "MIT",
"dependencies": {
"oniguruma-parser": "^0.12.1",
"oniguruma-parser": "^0.12.2",
"regex": "^6.1.0",
"regex-recursion": "^6.0.2"
}
@@ -2767,9 +2753,9 @@
}
},
"node_modules/react": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2777,16 +2763,16 @@
}
},
"node_modules/react-dom": {
"version": "19.2.5",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
"dev": true,
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.5"
"react": "^19.2.6"
}
},
"node_modules/react-lazy-with-preload": {
@@ -2836,9 +2822,9 @@
}
},
"node_modules/react-router": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz",
"integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.0.tgz",
"integrity": "sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2859,13 +2845,13 @@
}
},
"node_modules/react-router-dom": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz",
"integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.0.tgz",
"integrity": "sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"react-router": "7.14.0"
"react-router": "7.15.0"
},
"engines": {
"node": ">=20.0.0"
@@ -3218,13 +3204,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
"dev": true,
"license": "MIT"
},
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@@ -3311,9 +3290,9 @@
}
},
"node_modules/unhead": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.13.tgz",
"integrity": "sha512-jO9M1sI6b2h/1KpIu4Jeu+ptumLmUKboRRLxys5pYHFeT+lqTzfNHbYUX9bxVDhC1FBszAGuWcUVlmvIPsah8Q==",
"version": "2.1.15",
"resolved": "https://registry.npmjs.org/unhead/-/unhead-2.1.15.tgz",
"integrity": "sha512-MCt5T90mCWyr3Z6pUCdM9lVRXoMoVBlL7z7U4CYVIiaDiuzad/UCfLuMqz5MeNmpZUgoBCQnrucJimU7EZR+XA==",
"dev": true,
"license": "MIT",
"dependencies": {
+5 -1
View File
@@ -20,7 +20,11 @@ export default defineConfig({
'/deploying/docker-compose.for-traefik.yml',
'/deploying/docker-compose.with-traefik.yml',
`/deploying/docker-compose.override.yml`,
`/deploying/docker-compose.yml`
`/deploying/docker-compose.yml`,
'/advanced/delegated.docker-compose.with-caddy.yml',
'/advanced/delegated.docker-compose.with-caddy-labels.yml',
'/advanced/delegated.docker-compose.for-traefik.yml',
'/advanced/delegated.docker-compose.with-traefik.yml',
]
},
},
+2
View File
@@ -81,9 +81,11 @@ conduwuit-macros.workspace = true
conduwuit-service.workspace = true
const-str.workspace = true
ctor.workspace = true
dtor.workspace = true
futures.workspace = true
lettre.workspace = true
log.workspace = true
assign.workspace = true
ruma.workspace = true
serde_json.workspace = true
serde-saphyr.workspace = true
+1 -1
View File
@@ -7,7 +7,7 @@
#[implement(Context, params = "<'_>")]
pub(super) async fn check_all_users(&self) -> Result {
let timer = tokio::time::Instant::now();
let users = self.services.users.iter().collect::<Vec<_>>().await;
let users = self.services.users.stream().collect::<Vec<_>>().await;
let query_time = timer.elapsed();
let total = users.len();
+37 -24
View File
@@ -79,12 +79,14 @@ pub(super) async fn parse_pdu(&self) -> Result {
}
let string = self.body[1..self.body.len().saturating_sub(1)].join("\n");
let room_version_rules = RoomVersionId::V12.rules().unwrap();
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, &room_version_rules) {
| Err(e) => return Err!("Could not parse PDU JSON: {e:?}"),
| Ok(hash) => {
let event_id = OwnedEventId::parse(format!("${hash}"));
let event_id = EventId::parse(format!("${hash}"));
match serde_json::from_value::<PduEvent>(serde_json::to_value(value)?) {
| Err(e) => return Err!("EventId: {event_id:?}\nCould not parse event: {e}"),
| Ok(pdu) => write!(self, "EventId: {event_id:?}\n{pdu:#?}"),
@@ -119,7 +121,7 @@ pub(super) async fn get_pdu(&self, event_id: OwnedEventId) -> Result {
} else {
"PDU found in our database"
};
write!(self, "{msg}\n```json\n{text}\n```",)
write!(self, "{msg}\n```json\n{text}\n```")
},
}
.await
@@ -187,10 +189,7 @@ pub(super) async fn get_remote_pdu_list(&self, server: OwnedServerName, force: b
for event_id in list {
if force {
match self
.get_remote_pdu(event_id.to_owned(), server.clone())
.await
{
match self.get_remote_pdu(event_id.clone(), server.clone()).await {
| Err(e) => {
failed_count = failed_count.saturating_add(1);
self.services
@@ -205,7 +204,7 @@ pub(super) async fn get_remote_pdu_list(&self, server: OwnedServerName, force: b
},
}
} else {
self.get_remote_pdu(event_id.to_owned(), server.clone())
self.get_remote_pdu(event_id.clone(), server.clone())
.await?;
success_count = success_count.saturating_add(1);
}
@@ -237,10 +236,10 @@ pub(super) async fn get_remote_pdu(
match self
.services
.sending
.send_federation_request(&server, ruma::api::federation::event::get_event::v1::Request {
event_id: event_id.clone(),
include_unredacted_content: None,
})
.send_federation_request(
&server,
ruma::api::federation::event::get_event::v1::Request::new(event_id.clone()),
)
.await
{
| Err(e) => {
@@ -330,9 +329,9 @@ pub(super) async fn ping(&self, server: OwnedServerName) -> Result {
match self
.services
.sending
.send_federation_request(
.send_unauthenticated_request(
&server,
ruma::api::federation::discovery::get_server_version::v1::Request {},
ruma::api::federation::discovery::get_server_version::v1::Request::new(),
)
.await
{
@@ -361,7 +360,7 @@ pub(super) async fn force_device_list_updates(&self) -> Result {
self.services
.users
.stream()
.for_each(|user_id| self.services.users.mark_device_key_update(user_id))
.for_each(async |user_id| self.services.users.mark_device_key_update(&user_id).await)
.await;
write!(self, "Marked all devices for all users as having new keys to update").await
@@ -430,9 +429,16 @@ pub(super) async fn verify_json(&self) -> Result {
}
let string = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
let room_version_rules = RoomVersionId::V12.rules().unwrap();
match serde_json::from_str::<CanonicalJsonObject>(&string) {
| Err(e) => return Err!("Invalid json: {e}"),
| Ok(value) => match self.services.server_keys.verify_json(&value, None).await {
| Ok(value) => match self
.services
.server_keys
.verify_json(&value, &room_version_rules)
.await
{
| Err(e) => return Err!("Signature verification failed: {e}"),
| Ok(()) => write!(self, "Signature correct"),
},
@@ -445,9 +451,15 @@ pub(super) async fn verify_pdu(&self, event_id: OwnedEventId) -> Result {
use ruma::signatures::Verified;
let mut event = self.services.rooms.timeline.get_pdu_json(&event_id).await?;
let room_version_rules = RoomVersionId::V12.rules().unwrap();
event.remove("event_id");
let msg = match self.services.server_keys.verify_event(&event, None).await {
let msg = match self
.services
.server_keys
.verify_event(&event, &room_version_rules)
.await
{
| Err(e) => return Err(e),
| Ok(Verified::Signatures) => "signatures OK, but content hash failed (redaction).",
| Ok(Verified::All) => "signatures and hashes OK.",
@@ -544,16 +556,17 @@ pub(super) async fn force_set_room_state_from_server(
};
let room_version = self.services.rooms.state.get_room_version(&room_id).await?;
let room_version_rules = room_version.rules().unwrap();
let mut state: HashMap<u64, OwnedEventId> = HashMap::new();
let remote_state_response = self
.services
.sending
.send_federation_request(&server_name, get_room_state::v1::Request {
room_id: room_id.clone(),
event_id: at_event_id,
})
.send_federation_request(
&server_name,
get_room_state::v1::Request::new(at_event_id, room_id.clone()),
)
.await?;
for pdu in remote_state_response.pdus.clone() {
@@ -576,7 +589,7 @@ pub(super) async fn force_set_room_state_from_server(
for result in remote_state_response.pdus.iter().map(|pdu| {
self.services
.server_keys
.validate_and_add_event_id(pdu, &room_version)
.validate_and_add_event_id(pdu, &room_version_rules)
}) {
let Ok((event_id, value)) = result.await else {
continue;
@@ -608,7 +621,7 @@ pub(super) async fn force_set_room_state_from_server(
for result in remote_state_response.auth_chain.iter().map(|pdu| {
self.services
.server_keys
.validate_and_add_event_id(pdu, &room_version)
.validate_and_add_event_id(pdu, &room_version_rules)
}) {
let Ok((event_id, value)) = result.await else {
continue;
@@ -625,7 +638,7 @@ pub(super) async fn force_set_room_state_from_server(
.services
.rooms
.event_handler
.resolve_state(&room_id, &room_version, state)
.resolve_state(&room_id, &room_version_rules, state)
.await?;
info!("Compressing new room state");
+2 -6
View File
@@ -102,16 +102,12 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: OwnedUserId) -> Result
);
}
if !self.services.users.exists(&user_id).await {
return Err!("Remote user does not exist in our database.",);
}
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))
.then(async |room_id| get_room_info(self.services, &room_id).await)
.collect()
.await;
@@ -129,6 +125,6 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: OwnedUserId) -> Result
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms {user_id} shares with us ({num}):\n```\n{body}\n```",))
self.write_str(&format!("Rooms {user_id} shares with us ({num}):\n```\n{body}\n```"))
.await
}
+5 -4
View File
@@ -6,7 +6,8 @@
warn,
};
use conduwuit_service::media::Dim;
use ruma::{Mxc, OwnedEventId, OwnedMxcUri, OwnedServerName};
use ruma::{OwnedEventId, OwnedMxcUri, OwnedServerName};
use service::media::mxc::Mxc;
use crate::{admin_command, utils::parse_local_user_id};
@@ -261,7 +262,7 @@ pub(super) async fn delete_past_remote_media(
)
.await?;
self.write_str(&format!("Deleted {deleted_count} total files.",))
self.write_str(&format!("Deleted {deleted_count} total files."))
.await
}
@@ -271,7 +272,7 @@ pub(super) async fn delete_all_from_user(&self, username: String) -> Result {
let deleted_count = self.services.media.delete_from_user(&user_id).await?;
self.write_str(&format!("Deleted {deleted_count} total files.",))
self.write_str(&format!("Deleted {deleted_count} total files."))
.await
}
@@ -330,7 +331,7 @@ pub(super) async fn delete_all_from_server(
}
}
self.write_str(&format!("Deleted {deleted_count} total files.",))
self.write_str(&format!("Deleted {deleted_count} total files."))
.await
}
+5 -5
View File
@@ -16,8 +16,8 @@
use ruma::{
EventId,
events::{
relation::InReplyTo,
room::message::{Relation::Reply, RoomMessageEventContent},
relation::{InReplyTo, Reply},
room::message::{Relation, RoomMessageEventContent},
},
};
use service::{
@@ -38,6 +38,7 @@ pub(super) fn dispatch(services: Arc<Services>, command: CommandInput) -> Proces
}
#[tracing::instrument(skip_all, name = "admin", level = "info")]
#[allow(clippy::result_large_err)]
async fn handle_command(services: Arc<Services>, command: CommandInput) -> ProcessorResult {
AssertUnwindSafe(Box::pin(process_command(services, &command)))
.catch_unwind()
@@ -277,9 +278,8 @@ fn reply(
mut content: RoomMessageEventContent,
reply_id: Option<&EventId>,
) -> RoomMessageEventContent {
content.relates_to = reply_id.map(|event_id| Reply {
in_reply_to: InReplyTo { event_id: event_id.to_owned() },
});
content.relates_to =
reply_id.map(|event_id| Relation::Reply(Reply::new(InReplyTo::new(event_id.to_owned()))));
content
}
+13 -3
View File
@@ -1,7 +1,8 @@
use clap::Subcommand;
use conduwuit::Result;
use conduwuit_database::Deserialized as _;
use futures::StreamExt;
use ruma::{OwnedRoomId, OwnedUserId};
use ruma::{OwnedRoomId, OwnedUserId, exports::serde::Serialize};
use crate::{admin_command, admin_command_dispatch};
@@ -58,13 +59,22 @@ async fn account_data_get(
room_id: Option<OwnedRoomId>,
) -> Result {
let timer = tokio::time::Instant::now();
let results = self
let result = self
.services
.account_data
.get_raw(room_id.as_deref(), &user_id, &kind)
.await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"))
let json = serde_json::to_string_pretty(&match room_id {
| None => result
.deserialized::<ruma::serde::Raw<ruma::events::AnyGlobalAccountDataEvent>>()?
.serialize(serde_json::value::Serializer)?,
| Some(_) => result
.deserialized::<ruma::serde::Raw<ruma::events::AnyRoomAccountDataEvent>>()?
.serialize(serde_json::value::Serializer)?,
})?;
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{json}\n```"))
.await
}
+2 -2
View File
@@ -50,7 +50,7 @@ async fn destinations_cache(&self, server_name: Option<OwnedServerName>) -> Resu
while let Some((name, CachedDest { dest, host, expire })) = destinations.next().await {
if let Some(server_name) = server_name.as_ref() {
if name != server_name {
if name != *server_name {
continue;
}
}
@@ -76,7 +76,7 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
overrides.next().await
{
if let Some(server_name) = server_name.as_ref() {
if name != server_name {
if name != *server_name {
continue;
}
}
+1 -2
View File
@@ -41,7 +41,6 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Context<'_>)
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -54,7 +53,7 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Context<'_>)
.rooms
.alias
.all_local_aliases()
.map(|(room_id, alias)| (room_id.to_owned(), alias.to_owned()))
.map(|(room_id, alias)| (room_id, alias.to_owned()))
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
-8
View File
@@ -101,7 +101,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.room_servers(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -118,7 +117,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.server_rooms(&server)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -135,7 +133,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -152,7 +149,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.local_users_in_room(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -169,7 +165,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.active_local_users_in_room(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -212,7 +207,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.room_useroncejoined(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -229,7 +223,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.room_members_invited(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -276,7 +269,6 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.rooms_joined(&user_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
+1 -18
View File
@@ -15,10 +15,6 @@ pub enum UsersCommand {
IterUsers2,
PasswordHash {
user_id: OwnedUserId,
},
ListDevices {
user_id: OwnedUserId,
},
@@ -104,7 +100,6 @@ async fn get_shared_rooms(&self, user_a: OwnedUserId, user_b: OwnedUserId) -> Re
.rooms
.state_cache
.get_shared_rooms(&user_a, &user_b)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -217,8 +212,7 @@ async fn iter_users2(&self) -> Result {
let result: Vec<_> = self.services.users.stream().collect().await;
let result: Vec<_> = result
.into_iter()
.map(ruma::UserId::as_bytes)
.map(String::from_utf8_lossy)
.map(|user_id| String::from_utf8_lossy(user_id.as_bytes()).into_owned())
.collect();
let query_time = timer.elapsed();
@@ -237,16 +231,6 @@ async fn count_users(&self) -> Result {
.await
}
#[admin_command]
async fn password_hash(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result = self.services.users.password_hash(&user_id).await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command]
async fn list_devices(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
@@ -254,7 +238,6 @@ async fn list_devices(&self, user_id: OwnedUserId) -> Result {
.services
.users
.all_device_ids(&user_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await;
+3 -3
View File
@@ -3,7 +3,7 @@
use clap::Subcommand;
use conduwuit::{Err, Result};
use futures::StreamExt;
use ruma::{OwnedRoomAliasId, OwnedRoomId};
use ruma::{OwnedRoomAliasId, OwnedRoomId, RoomAliasId};
use crate::Context;
@@ -52,7 +52,7 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) ->
| RoomAliasCommand::Which { ref room_alias_localpart } => {
let room_alias_str =
format!("#{}:{}", room_alias_localpart, services.globals.server_name());
let room_alias = match OwnedRoomAliasId::parse(room_alias_str) {
let room_alias = match RoomAliasId::parse(room_alias_str) {
| Ok(alias) => alias,
| Err(err) => {
return Err!("Failed to parse alias: {err}");
@@ -139,7 +139,7 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) ->
.rooms
.alias
.all_local_aliases()
.map(|(room_id, localpart)| (room_id.into(), localpart.into()))
.map(|(room_id, localpart)| (room_id, localpart.into()))
.collect::<Vec<(OwnedRoomId, String)>>()
.await;
+4 -4
View File
@@ -22,14 +22,14 @@ pub(super) async fn list_rooms(
.metadata
.iter_ids()
.filter_map(|room_id| async move {
(!exclude_disabled || !self.services.rooms.metadata.is_disabled(room_id).await)
(!exclude_disabled || !self.services.rooms.metadata.is_disabled(&room_id).await)
.then_some(room_id)
})
.filter_map(|room_id| async move {
(!exclude_banned || !self.services.rooms.metadata.is_banned(room_id).await)
(!exclude_banned || !self.services.rooms.metadata.is_banned(&room_id).await)
.then_some(room_id)
})
.then(|room_id| get_room_info(self.services, room_id))
.then(async |room_id| get_room_info(self.services, &room_id).await)
.then(|(room_id, total_members, name)| async move {
let local_members: Vec<_> = self
.services
@@ -72,7 +72,7 @@ pub(super) async fn list_rooms(
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms ({}):\n```\n{body}\n```", rooms.len(),))
self.write_str(&format!("Rooms ({}):\n```\n{body}\n```", rooms.len()))
.await
}
+2 -2
View File
@@ -43,7 +43,7 @@ pub(super) async fn process(command: RoomDirectoryCommand, context: &Context<'_>
.rooms
.directory
.public_rooms()
.then(|room_id| get_room_info(services, room_id))
.then(async |room_id| get_room_info(services, &room_id).await)
.collect()
.await;
@@ -67,7 +67,7 @@ pub(super) async fn process(command: RoomDirectoryCommand, context: &Context<'_>
.join("\n");
context
.write_str(&format!("Rooms (page {page}):\n```\n{body}\n```",))
.write_str(&format!("Rooms (page {page}):\n```\n{body}\n```"))
.await
},
}
+1 -2
View File
@@ -46,7 +46,6 @@ async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> R
.then(|| self.services.globals.user_is_local(user_id))
.unwrap_or(true)
})
.map(ToOwned::to_owned)
.filter_map(|user_id| async move {
Some((
self.services
@@ -67,7 +66,7 @@ async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> R
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("{num} Members in Room \"{room_name}\":\n```\n{body}\n```",))
self.write_str(&format!("{num} Members in Room \"{room_name}\":\n```\n{body}\n```"))
.await
}
+10 -14
View File
@@ -71,7 +71,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
debug!("Room specified is a room ID, banning room ID");
room_id.to_owned()
room_id.clone()
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
| Ok(room_alias) => room_alias,
@@ -89,7 +89,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
locally, if not using get_alias_helper to fetch room ID remotely"
);
match self.services.rooms.alias.resolve_alias(room_alias).await {
match self.services.rooms.alias.resolve_alias(&room_alias).await {
| Ok((room_id, servers)) => {
debug!(
%room_id,
@@ -116,7 +116,6 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
@@ -140,7 +139,6 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.for_each(|local_alias| async move {
self.services
.rooms
@@ -205,7 +203,7 @@ async fn ban_list_of_rooms(&self) -> Result {
},
};
room_ids.push(room_id.to_owned());
room_ids.push(room_id.clone());
}
if room_alias_or_id.is_room_alias_id() {
@@ -215,7 +213,7 @@ async fn ban_list_of_rooms(&self) -> Result {
.services
.rooms
.alias
.resolve_local_alias(room_alias)
.resolve_local_alias(&room_alias)
.await
{
| Ok(room_id) => room_id,
@@ -229,7 +227,7 @@ async fn ban_list_of_rooms(&self) -> Result {
.services
.rooms
.alias
.resolve_alias(room_alias)
.resolve_alias(&room_alias)
.await
{
| Ok((room_id, servers)) => {
@@ -284,7 +282,6 @@ async fn ban_list_of_rooms(&self) -> Result {
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
@@ -309,7 +306,6 @@ async fn ban_list_of_rooms(&self) -> Result {
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.for_each(|local_alias| async move {
self.services
.rooms
@@ -348,9 +344,9 @@ 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.rooms.metadata.ban_room(&room_id, false);
room_id.to_owned()
room_id.clone()
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
| Ok(room_alias) => room_alias,
@@ -372,7 +368,7 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
.services
.rooms
.alias
.resolve_local_alias(room_alias)
.resolve_local_alias(&room_alias)
.await
{
| Ok(room_id) => room_id,
@@ -382,7 +378,7 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
room ID over federation"
);
match self.services.rooms.alias.resolve_alias(room_alias).await {
match self.services.rooms.alias.resolve_alias(&room_alias).await {
| Ok((room_id, servers)) => {
debug!(
%room_id,
@@ -453,6 +449,6 @@ async fn list_banned_rooms(&self, no_details: bool) -> Result {
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```",))
self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```"))
.await
}
+2 -2
View File
@@ -159,8 +159,8 @@ pub(super) async fn list_features(&self) -> Result {
let mut enabled_features = conduwuit::info::introspection::ENABLED_FEATURES
.lock()
.expect("locked")
.iter()
.flat_map(|(_, f)| f.iter())
.values()
.flat_map(|f| f.iter())
.collect::<Vec<_>>();
enabled_features.sort_unstable();
+171 -107
View File
@@ -4,28 +4,26 @@
};
use api::client::{
full_user_deactivate, join_room_by_id_helper, leave_room, recreate_push_rules_and_return,
remote_leave_room,
full_user_deactivate, leave_room, recreate_push_rules_and_return, remote_leave_room,
};
use conduwuit::{
Err, Result, debug_warn, error, info,
matrix::{Event, pdu::PduBuilder},
matrix::{Event, pdu::PartialPdu},
utils::{self, ReadyExt},
warn,
};
use futures::{FutureExt, StreamExt};
use lettre::Address;
use ruma::{
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, UserId,
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, ServerName,
UserId, assign,
events::{
RoomAccountDataEventType, StateEventType,
room::{
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
redaction::RoomRedactionEventContent,
},
RoomAccountDataEventType,
room::{power_levels::RoomPowerLevelsEventContent, redaction::RoomRedactionEventContent},
tag::{TagEvent, TagEventContent, TagInfo},
},
};
use service::users::HashedPassword;
use crate::{
admin_command, get_room_info,
@@ -41,7 +39,7 @@ pub(super) async fn list_users(&self) -> Result {
.services
.users
.list_local_users()
.map(ToString::to_string)
.map(|id| id.as_str().to_owned())
.collect()
.await;
@@ -72,7 +70,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
// Create user
self.services
.users
.create(&user_id, Some(password.as_str()), None)
.create(&user_id, Some(HashedPassword::new(&password)?))
.await?;
// Default to pretty displayname
@@ -103,11 +101,12 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
ruma::events::GlobalAccountDataEventType::PushRules
.to_string()
.into(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: ruma::push::Ruleset::server_default(&user_id),
},
})?,
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent::new(
ruma::events::push_rules::PushRulesEventContent::new(
ruma::push::Ruleset::server_default(&user_id),
),
))
.unwrap(),
)
.await?;
@@ -135,18 +134,20 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
}
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,
)
.await
match self
.services
.rooms
.membership
.join_room(
&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(),
],
)
.await
{
| Ok(_response) => {
info!("Automatically joined room {room} for user {user_id}");
@@ -275,24 +276,25 @@ pub(super) async fn reset_password(
let new_password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
match self
.services
self.services
.users
.set_password(&user_id, Some(new_password.as_str()))
.await
{
| Err(e) => return Err!("Couldn't reset the password for user {user_id}: {e}"),
| Ok(()) => {
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
},
}
.set_password(&user_id, Some(HashedPassword::new(&new_password)?));
self.write_str(&format!(
"Successfully reset the password for user {user_id}: `{new_password}`"
))
.await?;
if logout {
self.services
.users
.all_device_ids(&user_id)
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
.for_each(async |device_id| {
self.services
.users
.remove_device(&user_id, &device_id)
.await;
})
.await;
write!(self, "\nAll existing sessions have been logged out.").await?;
}
@@ -427,6 +429,82 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
.await
}
#[admin_command]
pub(super) async fn list_invited_rooms(&self, user_id: String) -> Result {
// Validate user id
let user_id = parse_local_user_id(self.services, &user_id)?;
let mut rooms: Vec<((OwnedRoomId, u64, String), Result<OwnedUserId>)> = self
.services
.rooms
.state_cache
.rooms_invited(&user_id)
.then(async |(room_id, _)| {
let sender = self
.services
.rooms
.state_cache
.invite_sender(&user_id, &room_id)
.await;
(get_room_info(self.services, &room_id).await, sender)
})
.collect()
.await;
if rooms.is_empty() {
return Err!("User is not invited to any rooms.");
}
rooms.sort_by_key(|r| r.0.1);
rooms.reverse();
let body = rooms
.iter()
.map(|((id, members, name), sender)| match sender {
| Ok(user_id) =>
format!("{id}\tInviter: {user_id}\tMembers: {members}\tName: {name}"),
| Err(_) => format!("{id}\tMembers: {members}\tName: {name}"),
})
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms {user_id} is Invited to ({}):\n```\n{body}\n```", rooms.len()))
.await
}
#[admin_command]
pub(super) async fn reject_all_invites(&self, user_id: String) -> Result {
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
let fails = self
.services
.rooms
.state_cache
.rooms_invited(&user_id)
.filter_map(async |(room_id, _)| {
match leave_room(self.services, &user_id, &room_id, None).await {
| Err(ref e) => {
warn!(%user_id, "Failed to leave {room_id}: {e}");
Some(())
},
| Ok(()) => None,
}
})
.count()
.await;
if fails > 0 {
return Err!("{fails} invites could not be rejected");
}
self.write_str("Successfully rejected all invites.").await
}
#[admin_command]
pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result {
// Validate user id
@@ -437,7 +515,7 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result {
.rooms
.state_cache
.rooms_joined(&user_id)
.then(|room_id| get_room_info(self.services, room_id))
.then(async |room_id| get_room_info(self.services, &room_id).await)
.collect()
.await;
@@ -454,7 +532,7 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result {
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms {user_id} Joined ({}):\n```\n{body}\n```", rooms.len(),))
self.write_str(&format!("Rooms {user_id} Joined ({}):\n```\n{body}\n```", rooms.len()))
.await
}
@@ -506,7 +584,7 @@ pub(super) async fn force_join_list_of_local_users(
.rooms
.state_cache
.room_members(&room_id)
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
.ready_any(|user_id| server_admins.contains(&user_id))
.await
{
return Err!("There is not a single server admin in the room.",);
@@ -552,15 +630,12 @@ pub(super) async fn force_join_list_of_local_users(
let mut successful_joins: usize = 0;
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,
)
.await
match self
.services
.rooms
.membership
.join_room(&user_id, &room_id, Some(String::from(BULK_JOIN_REASON)), &servers)
.await
{
| Ok(_res) => {
successful_joins = successful_joins.saturating_add(1);
@@ -620,7 +695,7 @@ pub(super) async fn force_join_all_local_users(
.rooms
.state_cache
.room_members(&room_id)
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
.ready_any(|user_id| server_admins.contains(&user_id))
.await
{
return Err!("There is not a single server admin in the room.",);
@@ -633,19 +708,15 @@ pub(super) async fn force_join_all_local_users(
.services
.users
.list_local_users()
.map(UserId::to_owned)
.collect::<Vec<_>>()
.await
{
match join_room_by_id_helper(
self.services,
user_id,
&room_id,
Some(String::from(BULK_JOIN_REASON)),
&servers,
&None,
)
.await
match self
.services
.rooms
.membership
.join_room(user_id, &room_id, Some(String::from(BULK_JOIN_REASON)), &servers)
.await
{
| Ok(_res) => {
successful_joins = successful_joins.saturating_add(1);
@@ -682,9 +753,13 @@ 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).await?;
self.services
.rooms
.membership
.join_room(&user_id, &room_id, None, &servers)
.await?;
self.write_str(&format!("{user_id} has been joined to {room_id}.",))
self.write_str(&format!("{user_id} has been joined to {room_id}."))
.await
}
@@ -716,7 +791,7 @@ pub(super) async fn force_leave_room(
.boxed()
.await?;
self.write_str(&format!("{user_id} has left {room_id}.",))
self.write_str(&format!("{user_id} has left {room_id}."))
.await
}
@@ -730,42 +805,34 @@ pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAli
"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.rooms.state.mutex.lock(room_id.as_str()).await;
let room_power_levels: Option<RoomPowerLevelsEventContent> = self
let mut room_power_levels = self
.services
.rooms
.state_accessor
.room_state_get_content(&room_id, &StateEventType::RoomPowerLevels, "")
.await
.ok();
.get_room_power_levels(&room_id)
.await;
let user_can_demote_self = room_power_levels
.as_ref()
.is_some_and(|power_levels_content| {
RoomPowerLevels::from(power_levels_content.clone())
.user_can_change_user_power_level(&user_id, &user_id)
}) || self
.services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
let user_can_demote_self =
room_power_levels.user_can_change_user_power_level(&user_id, &user_id);
if !user_can_demote_self {
return Err!("User is not allowed to modify their own power levels in the room.",);
}
let mut power_levels_content = room_power_levels.unwrap_or_default();
power_levels_content.users.remove(&user_id);
room_power_levels.users.remove(&user_id);
let event_id = self
.services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &power_levels_content),
PartialPdu::state(
String::new(),
&RoomPowerLevelsEventContent::try_from(room_power_levels)
.expect("PLs should be valid for room version"),
),
&user_id,
Some(&room_id),
&state_lock,
@@ -793,7 +860,7 @@ pub(super) async fn make_user_admin(&self, user_id: String) -> Result {
.boxed()
.await?;
self.write_str(&format!("{user_id} has been granted admin privileges.",))
self.write_str(&format!("{user_id} has been granted admin privileges."))
.await
}
@@ -811,9 +878,7 @@ pub(super) async fn put_room_tag(
.account_data
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
.await
.unwrap_or(TagEvent {
content: TagEventContent { tags: BTreeMap::new() },
});
.unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new())));
tags_event
.content
@@ -850,9 +915,7 @@ pub(super) async fn delete_room_tag(
.account_data
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
.await
.unwrap_or(TagEvent {
content: TagEventContent { tags: BTreeMap::new() },
});
.unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new())));
tags_event.content.tags.remove(&tag.clone().into());
@@ -882,9 +945,7 @@ pub(super) async fn get_room_tags(&self, user_id: String, room_id: OwnedRoomId)
.account_data
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
.await
.unwrap_or(TagEvent {
content: TagEventContent { tags: BTreeMap::new() },
});
.unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new())));
self.write_str(&format!("```\n{:#?}\n```", tags_event.content.tags))
.await
@@ -921,19 +982,19 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
.rooms
.state
.mutex
.lock(&event.room_id_or_hash())
.lock(event.room_id_or_hash().as_str())
.await;
self.services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
PartialPdu {
redacts: Some(event.event_id().to_owned()),
..PduBuilder::timeline(&RoomRedactionEventContent {
..PartialPdu::timeline(&assign!(RoomRedactionEventContent::new_v1(), {
redacts: Some(event.event_id().to_owned()),
reason: Some(reason),
})
}))
},
event.sender(),
Some(&event.room_id_or_hash()),
@@ -963,7 +1024,7 @@ pub(super) async fn force_leave_remote_room(
.resolve_with_servers(
&room_id,
if let Some(v) = via.clone() {
Some(vec![OwnedServerName::parse(v)?])
Some(vec![ServerName::parse(v)?])
} else {
None
},
@@ -976,7 +1037,7 @@ pub(super) async fn force_leave_remote_room(
);
let mut vias: HashSet<OwnedServerName> = HashSet::new();
if let Some(via) = via {
vias.insert(OwnedServerName::parse(via)?);
vias.insert(ServerName::parse(via)?);
}
for server in vias_raw {
vias.insert(server);
@@ -1051,7 +1112,12 @@ pub(super) async fn logout(&self, user_id: String) -> Result {
self.services
.users
.all_device_ids(&user_id)
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
.for_each(async |device_id| {
self.services
.users
.remove_device(&user_id, &device_id)
.await;
})
.await;
self.write_str(&format!("User {user_id} has been logged out from all devices."))
.await
@@ -1129,11 +1195,9 @@ pub(super) async fn get_user_by_email(&self, email: String) -> Result {
match self.services.threepid.get_localpart_for_email(&email).await {
| Some(localpart) => {
let user_id = OwnedUserId::parse(format!(
"@{localpart}:{}",
self.services.globals.server_name()
))
.unwrap();
let user_id =
UserId::parse(format!("@{localpart}:{}", self.services.globals.server_name()))
.unwrap();
self.write_str(&format!("{email} belongs to {user_id}."))
.await
+11
View File
@@ -160,6 +160,17 @@ pub enum UserCommand {
#[clap(alias = "list")]
ListUsers,
/// Lists all the rooms (local and remote) that the specified user is
/// invited to
ListInvitedRooms {
user_id: String,
},
/// Manually make a user reject all current invites
RejectAllInvites {
user_id: String,
},
/// Lists all the rooms (local and remote) that the specified user is
/// joined in
ListJoinedRooms {
+1 -1
View File
@@ -48,7 +48,7 @@ pub(crate) fn parse_local_user_id(services: &Services, user_id: &str) -> Result<
Ok(user_id)
}
/// Parses user ID that is an active (not guest or deactivated) local user
/// Parses user ID that is an active (not deactivated) local user
pub(crate) async fn parse_active_local_user_id(
services: &Services,
user_id: &str,
+3 -7
View File
@@ -29,10 +29,6 @@ gzip_compression = [
"conduwuit-service/gzip_compression",
"reqwest/gzip",
]
http3 = [
"conduwuit-core/http3",
"conduwuit-service/http3",
]
io_uring = [
"conduwuit-service/io_uring",
]
@@ -52,9 +48,6 @@ jemalloc_stats = [
"conduwuit-core/jemalloc_stats",
"conduwuit-service/jemalloc_stats",
]
ldap = [
"conduwuit-service/ldap"
]
release_max_log_level = [
"conduwuit-core/release_max_log_level",
"conduwuit-service/release_max_log_level",
@@ -81,6 +74,7 @@ conduwuit-macros.workspace = true
conduwuit-service.workspace = true
const-str.workspace = true
ctor.workspace = true
dtor.workspace = true
futures.workspace = true
hmac.workspace = true
http.workspace = true
@@ -92,7 +86,9 @@ lettre.workspace = true
log.workspace = true
rand.workspace = true
reqwest.workspace = true
assign.workspace = true
ruma.workspace = true
ruminuwuity.workspace = true
serde_html_form.workspace = true
serde_json.workspace = true
serde.workspace = true
+4 -7
View File
@@ -1,10 +1,8 @@
use axum::extract::State;
use conduwuit::{Err, Result, info, utils::ReadyExt, warn};
use futures::{FutureExt, StreamExt};
use ruma::{
OwnedRoomAliasId, continuwuity_admin_api::rooms,
events::room::message::RoomMessageEventContent,
};
use ruma::{OwnedRoomAliasId, events::room::message::RoomMessageEventContent};
use ruminuwuity::admin::continuwuity::rooms;
use crate::{Ruma, client::leave_room};
@@ -36,7 +34,6 @@ pub(crate) async fn ban_room(
.rooms
.state_cache
.room_members(&body.room_id)
.map(ToOwned::to_owned)
.ready_filter(|user| services.globals.user_is_local(user))
.boxed();
let mut evicted = Vec::new();
@@ -63,9 +60,9 @@ pub(crate) async fn ban_room(
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.collect()
.await;
for alias in &aliases {
info!("Removing alias {} for banned room {}", alias, body.room_id);
services
+4 -3
View File
@@ -1,7 +1,8 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use futures::StreamExt;
use ruma::{OwnedRoomId, continuwuity_admin_api::rooms};
use ruma::OwnedRoomId;
use ruminuwuity::admin::continuwuity::rooms;
use crate::Ruma;
@@ -22,8 +23,8 @@ pub(crate) async fn list_rooms(
.metadata
.iter_ids()
.filter_map(|room_id| async move {
if !services.rooms.metadata.is_banned(room_id).await {
Some(room_id.to_owned())
if !services.rooms.metadata.is_banned(&room_id).await {
Some(room_id.clone())
} else {
None
}
+51 -84
View File
@@ -1,15 +1,15 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Event, Result, err, info,
pdu::PduBuilder,
Err, Result, err, info,
pdu::PartialPdu,
utils::{ReadyExt, stream::BroadbandExt},
};
use conduwuit_service::Services;
use futures::{FutureExt, StreamExt};
use lettre::{Address, message::Mailbox};
use ruma::{
OwnedRoomId, OwnedUserId, UserId,
OwnedRoomId, UserId,
api::client::{
account::{
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
@@ -18,17 +18,15 @@
},
uiaa::{AuthFlow, AuthType},
},
events::{
StateEventType,
room::{
member::{MembershipState, RoomMemberEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
assign,
events::room::{
member::{MembershipState, RoomMemberEventContent},
power_levels::RoomPowerLevelsEventContent,
},
};
use service::{mailer::messages, uiaa::Identity};
use service::{mailer::messages, uiaa::Identity, users::HashedPassword};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::Ruma;
pub(crate) mod register;
@@ -48,7 +46,7 @@
#[tracing::instrument(skip_all, fields(%client), name = "register_available", level = "info")]
pub(crate) async fn get_register_available_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_username_availability::v3::Request>,
) -> Result<get_username_availability::v3::Response> {
// Validate user id
@@ -87,7 +85,7 @@ pub(crate) async fn get_register_available_route(
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
Ok(get_username_availability::v3::Response { available: true })
Ok(get_username_availability::v3::Response::new(true))
}
/// # `POST /_matrix/client/r0/account/password`
@@ -110,7 +108,7 @@ pub(crate) async fn get_register_available_route(
#[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")]
pub(crate) async fn change_password_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<change_password::v3::Request>,
) -> Result<change_password::v3::Response> {
let identity = if let Some(ref user_id) = body.sender_user {
@@ -143,7 +141,7 @@ pub(crate) async fn change_password_route(
.await?
};
let sender_user = OwnedUserId::parse(format!(
let sender_user = UserId::parse(format!(
"@{}:{}",
identity.localpart.expect("localpart should be known"),
services.globals.server_name()
@@ -152,8 +150,7 @@ pub(crate) async fn change_password_route(
services
.users
.set_password(&sender_user, Some(&body.new_password))
.await?;
.set_password(&sender_user, Some(HashedPassword::new(&body.new_password)?));
if body.logout_devices {
// Logout all devices except the current one
@@ -161,7 +158,7 @@ pub(crate) async fn change_password_route(
.users
.all_device_ids(&sender_user)
.ready_filter(|id| *id != body.sender_device())
.for_each(|id| services.users.remove_device(&sender_user, id))
.for_each(async |id| services.users.remove_device(&sender_user, &id).await)
.await;
// Remove all pushers except the ones associated with this session
@@ -175,8 +172,8 @@ pub(crate) async fn change_password_route(
.get_pusher_device(&pushkey)
.await
.ok()
.filter(|pusher_device| pusher_device != body.sender_device())
.is_some()
.as_ref()
.is_some_and(|pusher_device| pusher_device != body.sender_device())
.then_some(pushkey)
})
.for_each(async |pushkey| {
@@ -194,7 +191,7 @@ pub(crate) async fn change_password_route(
.await;
}
Ok(change_password::v3::Response {})
Ok(change_password::v3::Response::new())
}
/// # `POST /_matrix/client/v3/account/password/email/requestToken`
@@ -215,7 +212,7 @@ pub(crate) async fn request_password_change_token_via_email_route(
};
let user_id =
OwnedUserId::parse(format!("@{localpart}:{}", services.globals.server_name())).unwrap();
UserId::parse(format!("@{localpart}:{}", services.globals.server_name())).unwrap();
let display_name = services.users.displayname(&user_id).await.ok();
let session = services
@@ -241,21 +238,12 @@ pub(crate) async fn request_password_change_token_via_email_route(
///
/// Note: Also works for Application Services
pub(crate) async fn whoami_route(
State(services): State<crate::State>,
State(_): State<crate::State>,
body: Ruma<whoami::v3::Request>,
) -> Result<whoami::v3::Response> {
let is_guest = services
.users
.is_deactivated(body.sender_user())
.await
.map_err(|_| {
err!(Request(Forbidden("Application service has not registered this user.")))
})? && body.appservice_info.is_none();
Ok(whoami::v3::Response {
user_id: body.sender_user().to_owned(),
device_id: body.sender_device.clone(),
is_guest,
})
Ok(assign!(whoami::v3::Response::new(body.sender_user().to_owned(), false), {
device_id: body.sender_device,
}))
}
/// # `POST /_matrix/client/r0/account/deactivate`
@@ -272,7 +260,7 @@ pub(crate) async fn whoami_route(
#[tracing::instrument(skip_all, fields(%client), name = "deactivate", level = "info")]
pub(crate) async fn deactivate_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<deactivate::v3::Request>,
) -> Result<deactivate::v3::Response> {
// Authentication for this endpoint is technically optional,
@@ -310,9 +298,7 @@ pub(crate) async fn deactivate_route(
.await;
}
Ok(deactivate::v3::Response {
id_server_unbind_result: ThirdPartyIdRemovalStatus::Success,
})
Ok(deactivate::v3::Response::new(ThirdPartyIdRemovalStatus::Success))
}
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
@@ -330,7 +316,7 @@ pub(crate) async fn check_registration_token_validity(
.await
.is_some();
Ok(check_registration_token_validity::v1::Response { valid })
Ok(check_registration_token_validity::v1::Response::new(valid))
}
/// Runs through all the deactivation steps:
@@ -354,13 +340,7 @@ pub async fn full_user_deactivate(
.await;
}
services
.users
.all_profile_keys(user_id)
.ready_for_each(|(profile_key, _)| {
services.users.set_profile_key(user_id, &profile_key, None);
})
.await;
services.users.clear_profile(user_id).await;
services
.pusher
@@ -372,62 +352,49 @@ pub async fn full_user_deactivate(
// TODO: Rescind all user invites
let mut pdu_queue: Vec<(PduBuilder, &OwnedRoomId)> = Vec::new();
let mut pdu_queue: Vec<(PartialPdu, &OwnedRoomId)> = Vec::new();
for room_id in all_joined_rooms {
let room_power_levels = services
.rooms
.state_accessor
.room_state_get_content::<RoomPowerLevelsEventContent>(
room_id,
&StateEventType::RoomPowerLevels,
"",
)
.await
.ok();
.get_room_power_levels(room_id)
.await;
let user_can_demote_self =
room_power_levels
.as_ref()
.is_some_and(|power_levels_content| {
RoomPowerLevels::from(power_levels_content.clone())
.user_can_change_user_power_level(user_id, user_id)
}) || services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
room_power_levels.user_can_change_user_power_level(user_id, user_id);
if user_can_demote_self {
let mut power_levels_content = room_power_levels.unwrap_or_default();
if user_can_demote_self
&& let Ok(mut power_levels_content) =
RoomPowerLevelsEventContent::try_from(room_power_levels)
{
power_levels_content.users.remove(user_id);
let pl_evt = PduBuilder::state(String::new(), &power_levels_content);
let pl_evt = PartialPdu::state(String::new(), &power_levels_content);
pdu_queue.push((pl_evt, room_id));
}
// Leave the room
pdu_queue.push((
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
avatar_url: None,
blurhash: None,
membership: MembershipState::Leave,
displayname: None,
join_authorized_via_users_server: None,
reason: None,
is_direct: None,
third_party_invite: None,
redact_events: None,
}),
PartialPdu::state(
user_id.to_string(),
&RoomMemberEventContent::new(MembershipState::Leave),
),
room_id,
));
// TODO: Redact all messages sent by the user in the room
}
super::update_all_rooms(services, pdu_queue, user_id)
.boxed()
.await;
for (pdu, room_id) in pdu_queue {
let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;
let _ = services
.rooms
.timeline
.build_and_append_pdu(pdu, user_id, Some(room_id.as_ref()), &state_lock)
.await;
}
for room_id in all_joined_rooms {
services.rooms.state_cache.forget(room_id, user_id);
}
+78 -171
View File
@@ -1,7 +1,7 @@
use std::{collections::HashMap, fmt::Write};
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, debug_info, error, info,
utils::{self},
@@ -10,23 +10,26 @@
use conduwuit_service::Services;
use futures::{FutureExt, StreamExt};
use lettre::{Address, message::Mailbox};
use register::RegistrationKind;
use ruma::{
OwnedUserId, UserId,
api::client::{
account::{
register::{self, LoginType},
register::{self, LoginType, RegistrationKind},
request_registration_token_via_email,
},
uiaa::{AuthFlow, AuthType},
},
events::{GlobalAccountDataEventType, room::message::RoomMessageEventContent},
assign,
events::{
GlobalAccountDataEventType, push_rules::PushRulesEvent,
room::message::RoomMessageEventContent,
},
push,
};
use serde_json::value::RawValue;
use service::mailer::messages;
use service::{mailer::messages, users::HashedPassword};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::Ruma;
const RANDOM_USER_ID_LENGTH: usize = 10;
@@ -38,24 +41,17 @@
/// You can use [`GET
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
/// html) to check if the user id is valid and available.
///
/// - Only works if registration is enabled
/// - If type is guest: ignores all parameters except
/// initial_device_display_name
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
/// - If type is not guest and no username is given: Always fails after UIAA
/// check
/// - Creates a new account and populates it with default account data
/// - If `inhibit_login` is false: Creates a device and returns device id and
/// access_token
#[allow(clippy::doc_markdown)]
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
pub(crate) async fn register_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<register::v3::Request>,
) -> Result<register::v3::Response> {
let is_guest = body.kind == RegistrationKind::Guest;
if body.kind != RegistrationKind::User {
return Err!(Request(GuestAccessForbidden("Guests may not register on this server.")));
}
let emergency_mode_enabled = services.config.emergency_password.is_some();
// Allow registration if it's enabled in the config file or if this is the first
@@ -64,69 +60,19 @@ pub(crate) async fn register_route(
services.config.allow_registration || services.firstrun.is_first_run();
if !allow_registration && body.appservice_info.is_none() {
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
| (Some(username), Some(device_display_name)) => {
info!(
%is_guest,
user = %username,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
},
| (Some(username), _) => {
info!(
%is_guest,
user = %username,
"Rejecting registration attempt as registration is disabled"
);
},
| (_, Some(device_display_name)) => {
info!(
%is_guest,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
},
| (None, _) => {
info!(
%is_guest,
"Rejecting registration attempt as registration is disabled"
);
},
}
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
}
if is_guest && !services.config.allow_guest_registration {
info!(
"Guest registration disabled, rejecting guest registration attempt, initial device \
name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
?body.username,
?body.initial_device_display_name,
"Rejecting registration attempt as registration is disabled"
);
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
}
// forbid guests from registering if there is not a real admin user yet. give
// generic user error.
if is_guest && services.firstrun.is_first_run() {
warn!(
"Guest account attempted to register before a real admin user has been registered, \
rejecting registration. Guest's initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
}
// Appeservices and guests get to skip auth
let skip_auth = body.appservice_info.is_some() || is_guest;
let identity = if skip_auth {
// Appservices and guests have no identity
let identity = if body.appservice_info.is_some() {
// Appservices can skip auth
None
} else {
// Perform UIAA to determine the user's identity
@@ -153,13 +99,9 @@ pub(crate) async fn register_route(
}
});
let user_id = determine_registration_user_id(
&services,
supplied_username,
is_guest,
emergency_mode_enabled,
)
.await?;
let user_id =
determine_registration_user_id(&services, supplied_username, emergency_mode_enabled)
.await?;
if body.body.login_type == Some(LoginType::ApplicationService) {
// For appservice logins, make sure that the user ID is in the appservice's
@@ -183,10 +125,16 @@ pub(crate) async fn register_route(
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
let password = if is_guest { None } else { body.password.as_deref() };
let password = if body.appservice_info.is_some() {
None
} else if let Some(password) = body.password.as_deref() {
Some(HashedPassword::new(password)?)
} else {
return Err!(Request(InvalidParam("A password must be provided")));
};
// Create user
services.users.create(&user_id, password, None).await?;
services.users.create(&user_id, password).await?;
// Set an initial display name
let mut displayname = user_id.localpart().to_owned();
@@ -209,24 +157,18 @@ pub(crate) async fn register_route(
None,
&user_id,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: push::Ruleset::server_default(&user_id),
},
})?,
&serde_json::to_value(PushRulesEvent::new(
push::Ruleset::server_default(&user_id).into(),
))
.expect("should be able to serialize push rules"),
)
.await?;
// Generate new device id if the user didn't specify one
let no_device = body.inhibit_login
|| body
.appservice_info
.as_ref()
.is_some_and(|aps| aps.registration.device_management);
let (token, device) = if !no_device {
// Don't create a device for inhibited logins
let device_id = if is_guest { None } else { body.device_id.clone() }
let (token, device) = if !body.inhibit_login {
let device_id = body
.device_id
.clone()
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
// Generate new token for the device
@@ -243,12 +185,14 @@ pub(crate) async fn register_route(
Some(client.to_string()),
)
.await?;
debug_info!(%user_id, %device_id, "User account was created");
(Some(new_token), Some(device_id))
} else {
// Don't create a device for inhibited logins
(None, None)
};
debug_info!(%user_id, ?device, "User account was created");
// If the user registered with an email, associate it with their account.
if let Some(identity) = identity
&& let Some(email) = identity.email
@@ -265,8 +209,7 @@ pub(crate) async fn register_route(
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
// log in conduit admin channel if a non-guest user registered
if body.appservice_info.is_none() && !is_guest {
if body.appservice_info.is_none() {
if !device_display_name.is_empty() {
let notice = format!(
"New user \"{user_id}\" registered on this server from IP {client} and device \
@@ -287,65 +230,32 @@ pub(crate) async fn register_route(
}
}
// log in conduit admin channel if a guest registered
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
debug_info!("New guest user \"{user_id}\" registered on this server.");
// Make the first user to register an administrator and disable first-run mode.
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
if !device_display_name.is_empty() {
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
"Guest user \"{user_id}\" with device display name \
\"{device_display_name}\" registered on this server from IP {client}"
))
.await;
}
} else {
#[allow(clippy::collapsible_else_if)]
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
"Guest user \"{user_id}\" with no device display name registered on \
this server from IP {client}",
))
.await;
}
}
}
if !is_guest {
// Make the first user to register an administrator and disable first-run mode.
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
// If the registering user was not the first and we're suspending users on
// register, suspend them.
if !was_first_user && services.config.suspend_on_register {
// Note that we can still do auto joins for suspended users
// If the registering user was not the first and we're suspending users on
// register, suspend them.
if !was_first_user && services.config.suspend_on_register {
// Note that we can still do auto joins for suspended users
services
.users
.suspend_account(&user_id, &services.globals.server_user)
.await;
// And send an @room notice to the admin room, to prompt admins to review the
// new user and ideally unsuspend them if deemed appropriate.
if services.server.config.admin_room_notices {
services
.users
.suspend_account(&user_id, &services.globals.server_user)
.await;
// And send an @room notice to the admin room, to prompt admins to review the
// new user and ideally unsuspend them if deemed appropriate.
if services.server.config.admin_room_notices {
services
.admin
.send_loud_message(RoomMessageEventContent::text_plain(format!(
"User {user_id} has been suspended as they are not the first user on \
this server. Please review and unsuspend them if appropriate."
)))
.await
.ok();
}
.admin
.send_loud_message(RoomMessageEventContent::text_plain(format!(
"User {user_id} has been suspended as they are not the first user on this \
server. Please review and unsuspend them if appropriate."
)))
.await
.ok();
}
}
if body.appservice_info.is_none()
&& !services.server.config.auto_join_rooms.is_empty()
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
{
if body.appservice_info.is_none() && !services.server.config.auto_join_rooms.is_empty() {
for room in &services.server.config.auto_join_rooms {
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
error!(
@@ -368,16 +278,17 @@ 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()],
&body.appservice_info,
)
.boxed()
.await
match services
.rooms
.membership
.join_room(
&user_id,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
)
.boxed()
.await
{
| Err(e) => {
// don't return this error so we don't fail registrations
@@ -393,13 +304,12 @@ pub(crate) async fn register_route(
}
}
Ok(register::v3::Response {
Ok(assign!(register::v3::Response::new(user_id), {
access_token: token,
user_id,
device_id: device,
refresh_token: None,
expires_in: None,
})
}))
}
/// Determine which flows and parameters should be presented when
@@ -514,12 +424,9 @@ async fn create_registration_uiaa_session(
async fn determine_registration_user_id(
services: &Services,
supplied_username: Option<String>,
is_guest: bool,
emergency_mode_enabled: bool,
) -> Result<OwnedUserId> {
if let Some(supplied_username) = supplied_username
&& !is_guest
{
if let Some(supplied_username) = supplied_username {
// The user gets to pick their username. Do some validation to make sure it's
// acceptable.
@@ -572,7 +479,7 @@ async fn determine_registration_user_id(
Ok(user_id)
} else {
// The user is a guest or didn't specify a username. Generate a username for
// The user didn't specify a username. Generate a username for
// them.
loop {
+2 -6
View File
@@ -141,9 +141,7 @@ pub(crate) async fn delete_3pid_route(
let sender_user = body.sender_user();
if body.medium != Medium::Email {
return Ok(delete_3pid::v3::Response {
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
});
return Ok(delete_3pid::v3::Response::new(ThirdPartyIdRemovalStatus::NoSupport));
}
if !services.threepid.email_requirement().may_remove() {
@@ -159,7 +157,5 @@ pub(crate) async fn delete_3pid_route(
return Err!(Request(ThreepidNotFound("Your account has no associated email.")));
}
Ok(delete_3pid::v3::Response {
id_server_unbind_result: ThirdPartyIdRemovalStatus::Success,
})
Ok(delete_3pid::v3::Response::new(ThirdPartyIdRemovalStatus::Success))
}
+6 -9
View File
@@ -7,10 +7,7 @@
get_global_account_data, get_room_account_data, set_global_account_data,
set_room_account_data,
},
events::{
AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent,
RoomAccountDataEventType,
},
events::{AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent},
serde::Raw,
};
use serde::Deserialize;
@@ -40,7 +37,7 @@ pub(crate) async fn set_global_account_data_route(
)
.await?;
Ok(set_global_account_data::v3::Response {})
Ok(set_global_account_data::v3::Response::new())
}
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
@@ -65,7 +62,7 @@ pub(crate) async fn set_room_account_data_route(
)
.await?;
Ok(set_room_account_data::v3::Response {})
Ok(set_room_account_data::v3::Response::new())
}
/// # `GET /_matrix/client/r0/user/{userId}/account_data/{type}`
@@ -87,7 +84,7 @@ pub(crate) async fn get_global_account_data_route(
.await
.map_err(|_| err!(Request(NotFound("Data not found."))))?;
Ok(get_global_account_data::v3::Response { account_data: account_data.content })
Ok(get_global_account_data::v3::Response::new(account_data.content))
}
/// # `GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
@@ -109,7 +106,7 @@ pub(crate) async fn get_room_account_data_route(
.await
.map_err(|_| err!(Request(NotFound("Data not found."))))?;
Ok(get_room_account_data::v3::Response { account_data: account_data.content })
Ok(get_room_account_data::v3::Response::new(account_data.content))
}
async fn set_account_data(
@@ -119,7 +116,7 @@ async fn set_account_data(
event_type_s: &str,
data: &RawJsonValue,
) -> Result {
if event_type_s == RoomAccountDataEventType::FullyRead.to_cow_str() {
if event_type_s == "m.fully_read" {
return Err!(Request(BadJson(
"This endpoint cannot be used for marking a room as fully read (setting \
m.fully_read)"
+1 -1
View File
@@ -1,7 +1,7 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use futures::future::{join, join3};
use ruma::api::client::admin::{get_suspended, set_suspended};
use ruminuwuity::admin::{get_suspended, set_suspended};
use crate::Ruma;
+7 -4
View File
@@ -1,6 +1,9 @@
use axum::extract::State;
use conduwuit::{Err, Result, err};
use ruma::api::{appservice::ping, client::appservice::request_ping};
use ruma::{
api::{appservice::ping, client::appservice::request_ping},
assign,
};
use crate::Ruma;
@@ -40,12 +43,12 @@ pub(crate) async fn appservice_ping(
.sending
.send_appservice_request(
appservice_info.registration.clone(),
ping::send_ping::v1::Request {
assign!(ping::send_ping::v1::Request::new(), {
transaction_id: body.transaction_id.clone(),
},
}),
)
.await?
.expect("We already validated if an appservice URL exists above");
Ok(request_ping::v1::Response { duration: timer.elapsed() })
Ok(request_ping::v1::Response::new(timer.elapsed()))
}
+29 -33
View File
@@ -3,7 +3,6 @@
use axum::extract::State;
use conduwuit::{Err, Result, err};
use conduwuit_service::Services;
use futures::{FutureExt, future::try_join};
use ruma::{
UInt, UserId,
api::client::backup::{
@@ -28,7 +27,7 @@ pub(crate) async fn create_backup_version_route(
.key_backups
.create_backup(body.sender_user(), &body.algorithm)?;
Ok(create_backup_version::v3::Response { version })
Ok(create_backup_version::v3::Response::new(version))
}
/// # `PUT /_matrix/client/r0/room_keys/version/{version}`
@@ -44,7 +43,7 @@ pub(crate) async fn update_backup_version_route(
.update_backup(body.sender_user(), &body.version, &body.algorithm)
.await?;
Ok(update_backup_version::v3::Response {})
Ok(update_backup_version::v3::Response::new())
}
/// # `GET /_matrix/client/r0/room_keys/version`
@@ -60,9 +59,9 @@ pub(crate) async fn get_latest_backup_info_route(
.await
.map_err(|_| err!(Request(NotFound("Key backup does not exist."))))?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &version).await?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &version).await;
Ok(get_latest_backup_info::v3::Response { algorithm, count, etag, version })
Ok(get_latest_backup_info::v3::Response::new(algorithm, count, etag, version))
}
/// # `GET /_matrix/client/v3/room_keys/version/{version}`
@@ -80,14 +79,9 @@ pub(crate) async fn get_backup_info_route(
err!(Request(NotFound("Key backup does not exist at version {:?}", body.version)))
})?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(get_backup_info::v3::Response {
algorithm,
count,
etag,
version: body.version.clone(),
})
Ok(get_backup_info::v3::Response::new(algorithm, count, etag, body.version.clone()))
}
/// # `DELETE /_matrix/client/r0/room_keys/version/{version}`
@@ -105,7 +99,7 @@ pub(crate) async fn delete_backup_version_route(
.delete_backup(body.sender_user(), &body.version)
.await;
Ok(delete_backup_version::v3::Response {})
Ok(delete_backup_version::v3::Response::new())
}
/// # `PUT /_matrix/client/r0/room_keys/keys`
@@ -140,9 +134,9 @@ pub(crate) async fn add_backup_keys_route(
}
}
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(add_backup_keys::v3::Response { count, etag })
Ok(add_backup_keys::v3::Response::new(etag, count))
}
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}`
@@ -175,9 +169,9 @@ pub(crate) async fn add_backup_keys_for_room_route(
.await?;
}
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(add_backup_keys_for_room::v3::Response { count, etag })
Ok(add_backup_keys_for_room::v3::Response::new(etag, count))
}
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
@@ -275,9 +269,9 @@ pub(crate) async fn add_backup_keys_for_session_route(
.await?;
}
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(add_backup_keys_for_session::v3::Response { count, etag })
Ok(add_backup_keys_for_session::v3::Response::new(etag, count))
}
/// # `GET /_matrix/client/r0/room_keys/keys`
@@ -292,7 +286,7 @@ pub(crate) async fn get_backup_keys_route(
.get_all(body.sender_user(), &body.version)
.await;
Ok(get_backup_keys::v3::Response { rooms })
Ok(get_backup_keys::v3::Response::new(rooms))
}
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}`
@@ -307,7 +301,7 @@ pub(crate) async fn get_backup_keys_for_room_route(
.get_room(body.sender_user(), &body.version, &body.room_id)
.await;
Ok(get_backup_keys_for_room::v3::Response { sessions })
Ok(get_backup_keys_for_room::v3::Response::new(sessions))
}
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
@@ -325,7 +319,7 @@ pub(crate) async fn get_backup_keys_for_session_route(
err!(Request(NotFound(debug_error!("Backup key not found for this user's session."))))
})?;
Ok(get_backup_keys_for_session::v3::Response { key_data })
Ok(get_backup_keys_for_session::v3::Response::new(key_data))
}
/// # `DELETE /_matrix/client/r0/room_keys/keys`
@@ -340,9 +334,9 @@ pub(crate) async fn delete_backup_keys_route(
.delete_all_keys(body.sender_user(), &body.version)
.await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(delete_backup_keys::v3::Response { count, etag })
Ok(delete_backup_keys::v3::Response::new(etag, count))
}
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}`
@@ -357,9 +351,9 @@ pub(crate) async fn delete_backup_keys_for_room_route(
.delete_room_keys(body.sender_user(), &body.version, &body.room_id)
.await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(delete_backup_keys_for_room::v3::Response { count, etag })
Ok(delete_backup_keys_for_room::v3::Response::new(etag, count))
}
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
@@ -374,22 +368,24 @@ pub(crate) async fn delete_backup_keys_for_session_route(
.delete_room_key(body.sender_user(), &body.version, &body.room_id, &body.session_id)
.await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(delete_backup_keys_for_session::v3::Response { count, etag })
Ok(delete_backup_keys_for_session::v3::Response::new(etag, count))
}
async fn get_count_etag(
services: &Services,
sender_user: &UserId,
version: &str,
) -> Result<(UInt, String)> {
let count = services
) -> (UInt, String) {
let count: UInt = services
.key_backups
.count_keys(sender_user, version)
.map(TryInto::try_into);
.await
.try_into()
.expect("number of keys should fit into a UInt");
let etag = services.key_backups.get_etag(sender_user, version).map(Ok);
let etag = services.key_backups.get_etag(sender_user, version).await;
Ok(try_join(count, etag).await?)
(count, etag)
}
+13 -12
View File
@@ -5,8 +5,11 @@
use ruma::{
RoomVersionId,
api::client::discovery::get_capabilities::{
self, Capabilities, GetLoginTokenCapability, RoomVersionStability,
RoomVersionsCapability, ThirdPartyIdChangesCapability,
self,
v3::{
Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability,
ThirdPartyIdChangesCapability,
},
},
};
use serde_json::json;
@@ -25,19 +28,17 @@ pub(crate) async fn get_capabilities_route(
Server::available_room_versions().collect();
let mut capabilities = Capabilities::default();
capabilities.room_versions = RoomVersionsCapability {
capabilities.room_versions = RoomVersionsCapability::new(
services.server.config.default_room_version.clone(),
available,
default: services.server.config.default_room_version.clone(),
};
);
// Only allow 3pid changes if SMTP is configured
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability {
enabled: services.threepid.email_requirement().may_change(),
};
capabilities.thirdparty_id_changes =
ThirdPartyIdChangesCapability::new(services.threepid.email_requirement().may_change());
capabilities.get_login_token = GetLoginTokenCapability {
enabled: services.server.config.login_via_existing_session,
};
capabilities.get_login_token =
GetLoginTokenCapability::new(services.server.config.login_via_existing_session);
// MSC4133 capability
capabilities.set("uk.tcpip.msc4133.profile_fields", json!({"enabled": true}))?;
@@ -56,5 +57,5 @@ pub(crate) async fn get_capabilities_route(
capabilities.set("uk.timedout.msc4323", json!({"suspend": true, "lock": false}))?;
}
Ok(get_capabilities::v3::Response { capabilities })
Ok(get_capabilities::v3::Response::new(capabilities))
}
+5 -3
View File
@@ -12,7 +12,9 @@
FutureExt, StreamExt, TryFutureExt, TryStreamExt,
future::{OptionFuture, join, join3, try_join3},
};
use ruma::{OwnedEventId, UserId, api::client::context::get_context, events::StateEventType};
use ruma::{
OwnedEventId, UserId, api::client::context::get_context, assign, events::StateEventType,
};
use crate::{
Ruma,
@@ -213,7 +215,7 @@ pub(crate) async fn get_context_route(
.collect()
.await;
Ok(get_context::v3::Response {
Ok(assign!(get_context::v3::Response::new(), {
event: base_event.map(at!(1)).map(Event::into_format),
start: events_before
@@ -243,5 +245,5 @@ pub(crate) async fn get_context_route(
.collect(),
state,
})
}))
}
+18 -18
View File
@@ -1,11 +1,15 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, at};
use futures::StreamExt;
use ruma::api::client::dehydrated_device::{
delete_dehydrated_device::unstable as delete_dehydrated_device,
get_dehydrated_device::unstable as get_dehydrated_device, get_events::unstable as get_events,
put_dehydrated_device::unstable as put_dehydrated_device,
use ruma::{
api::client::dehydrated_device::{
delete_dehydrated_device::unstable as delete_dehydrated_device,
get_dehydrated_device::unstable as get_dehydrated_device,
get_events::unstable as get_events,
put_dehydrated_device::unstable as put_dehydrated_device,
},
assign,
};
use crate::Ruma;
@@ -18,7 +22,7 @@
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn put_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<put_dehydrated_device::Request>,
) -> Result<put_dehydrated_device::Response> {
let sender_user = body
@@ -33,7 +37,7 @@ pub(crate) async fn put_dehydrated_device_route(
.set_dehydrated_device(sender_user, body.body)
.await?;
Ok(put_dehydrated_device::Response { device_id })
Ok(put_dehydrated_device::Response::new(device_id))
}
/// # `DELETE /_matrix/client/../dehydrated_device`
@@ -42,7 +46,7 @@ pub(crate) async fn put_dehydrated_device_route(
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn delete_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<delete_dehydrated_device::Request>,
) -> Result<delete_dehydrated_device::Response> {
let sender_user = body.sender_user();
@@ -51,7 +55,7 @@ pub(crate) async fn delete_dehydrated_device_route(
services.users.remove_device(sender_user, &device_id).await;
Ok(delete_dehydrated_device::Response { device_id })
Ok(delete_dehydrated_device::Response::new(device_id))
}
/// # `GET /_matrix/client/../dehydrated_device`
@@ -60,17 +64,14 @@ pub(crate) async fn delete_dehydrated_device_route(
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn get_dehydrated_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_dehydrated_device::Request>,
) -> Result<get_dehydrated_device::Response> {
let sender_user = body.sender_user();
let device = services.users.get_dehydrated_device(sender_user).await?;
Ok(get_dehydrated_device::Response {
device_id: device.device_id,
device_data: device.device_data,
})
Ok(get_dehydrated_device::Response::new(device.device_id, device.device_data))
}
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
@@ -79,7 +80,7 @@ pub(crate) async fn get_dehydrated_device_route(
#[tracing::instrument(skip_all, fields(%client))]
pub(crate) async fn get_dehydrated_events_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_events::Request>,
) -> Result<get_events::Response> {
let sender_user = body.sender_user();
@@ -114,8 +115,7 @@ pub(crate) async fn get_dehydrated_events_route(
.collect()
.await;
Ok(get_events::Response {
events,
Ok(assign!(get_events::Response::new(events), {
next_batch: next_batch.as_ref().map(ToString::to_string),
})
}))
}
+25 -48
View File
@@ -1,5 +1,5 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, debug, err, utils};
use futures::StreamExt;
use ruma::{
@@ -25,7 +25,7 @@ pub(crate) async fn get_devices_route(
.collect()
.await;
Ok(get_devices::v3::Response { devices })
Ok(get_devices::v3::Response::new(devices))
}
/// # `GET /_matrix/client/r0/devices/{deviceId}`
@@ -41,7 +41,7 @@ pub(crate) async fn get_device_route(
.await
.map_err(|_| err!(Request(NotFound("Device not found."))))?;
Ok(get_device::v3::Response { device })
Ok(get_device::v3::Response::new(device))
}
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
@@ -50,7 +50,7 @@ pub(crate) async fn get_device_route(
#[tracing::instrument(skip_all, fields(%client), name = "update_device", level = "debug")]
pub(crate) async fn update_device_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<update_device::v3::Request>,
) -> Result<update_device::v3::Response> {
let sender_user = body.sender_user();
@@ -73,19 +73,16 @@ pub(crate) async fn update_device_route(
.update_device_metadata(sender_user, &body.device_id, &device)
.await?;
Ok(update_device::v3::Response {})
Ok(update_device::v3::Response::new())
},
| Err(_) => {
let Some(appservice) = appservice else {
return Err!(Request(NotFound("Device not found.")));
};
if !appservice.registration.device_management {
return Err!(Request(NotFound("Device not found.")));
}
debug!(
"Creating new device for {sender_user} from appservice {} as MSC4190 is enabled \
and device ID does not exist",
"Creating new device for {sender_user} from appservice {} as device ID does not \
exist",
appservice.registration.id
);
@@ -102,7 +99,7 @@ pub(crate) async fn update_device_route(
)
.await?;
return Ok(update_device::v3::Response {});
return Ok(update_device::v3::Response::new());
},
}
}
@@ -124,39 +121,28 @@ pub(crate) async fn delete_device_route(
let sender_user = body.sender_user();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
);
services
.users
.remove_device(sender_user, &body.device_id)
.await;
return Ok(delete_device::v3::Response {});
// Appservices get to skip UIAA for this endpoint
if appservice.is_none() {
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
}
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
services
.users
.remove_device(sender_user, &body.device_id)
.await;
Ok(delete_device::v3::Response {})
Ok(delete_device::v3::Response::new())
}
/// # `POST /_matrix/client/v3/delete_devices`
///
/// Deletes the given list of devices.
///
/// - Requires UIAA to verify user password unless from an appservice with
/// MSC4190 enabled.
/// - Requires UIAA to verify user password.
///
/// For each device:
/// - Invalidates access token
@@ -171,27 +157,18 @@ pub(crate) async fn delete_devices_route(
let sender_user = body.sender_user();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
);
for device_id in &body.devices {
services.users.remove_device(sender_user, device_id).await;
}
return Ok(delete_devices::v3::Response {});
// Appservices get to skip UIAA for this endpoint
if appservice.is_none() {
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
}
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
for device_id in &body.devices {
services.users.remove_device(sender_user, device_id).await;
}
Ok(delete_devices::v3::Response {})
Ok(delete_devices::v3::Response::new())
}
+54 -154
View File
@@ -1,23 +1,16 @@
use std::iter::once;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Event, Result, RoomVersion, err, info,
Err, Result, err, info,
utils::{
TryFutureExtExt,
math::Expected,
result::FlatOk,
stream::{ReadyExt, WidebandExt},
},
};
use conduwuit_service::Services;
use futures::{
FutureExt, StreamExt, TryFutureExt,
future::{join, join4, join5},
};
use futures::StreamExt;
use ruma::{
OwnedRoomId, RoomId, ServerName, UInt, UserId,
RoomId, ServerName, UInt, UserId,
api::{
client::{
directory::{
@@ -28,15 +21,9 @@
},
federation,
},
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
events::{
StateEventType,
room::{
create::RoomCreateEventContent,
join_rules::{JoinRule, RoomJoinRulesEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
},
assign,
directory::{Filter, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
events::StateEventType,
uint,
};
use tokio::join;
@@ -51,7 +38,7 @@
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms", level = "info")]
pub(crate) async fn get_public_rooms_filtered_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_public_rooms_filtered::v3::Request>,
) -> Result<get_public_rooms_filtered::v3::Response> {
if let Some(server) = &body.server {
@@ -87,7 +74,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms", level = "info")]
pub(crate) async fn get_public_rooms_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_public_rooms::v3::Request>,
) -> Result<get_public_rooms::v3::Response> {
if let Some(server) = &body.server {
@@ -109,12 +96,11 @@ pub(crate) async fn get_public_rooms_route(
err!(Request(Unknown(warn!(?body.server, "Failed to return /publicRooms: {e}"))))
})?;
Ok(get_public_rooms::v3::Response {
chunk: response.chunk,
Ok(assign!(get_public_rooms::v3::Response::new(response.chunk), {
prev_batch: response.prev_batch,
next_batch: response.next_batch,
total_room_count_estimate: response.total_room_count_estimate,
})
}))
}
/// # `PUT /_matrix/client/r0/directory/list/room/{roomId}`
@@ -123,7 +109,7 @@ pub(crate) async fn get_public_rooms_route(
#[tracing::instrument(skip_all, fields(%client), name = "room_directory", level = "info")]
pub(crate) async fn set_room_visibility_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<set_room_visibility::v3::Request>,
) -> Result<set_room_visibility::v3::Response> {
let sender_user = body.sender_user();
@@ -136,16 +122,6 @@ pub(crate) async fn set_room_visibility_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if services
.users
.is_deactivated(sender_user)
.await
.unwrap_or(false)
&& body.appservice_info.is_none()
{
return Err!(Request(Forbidden("Guests cannot publish to room directories")));
}
if !user_can_publish_room(&services, sender_user, &body.room_id).await? {
return Err!(Request(Forbidden("User is not allowed to publish this room")));
}
@@ -197,7 +173,7 @@ pub(crate) async fn set_room_visibility_route(
},
}
Ok(set_room_visibility::v3::Response {})
Ok(set_room_visibility::v3::Response::new())
}
/// # `GET /_matrix/client/r0/directory/list/room/{roomId}`
@@ -212,13 +188,13 @@ pub(crate) async fn get_room_visibility_route(
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 {
room::Visibility::Public
} else {
room::Visibility::Private
},
})
let visibility = if services.rooms.directory.is_public_room(&body.room_id).await {
room::Visibility::Public
} else {
room::Visibility::Private
};
Ok(get_room_visibility::v3::Response::new(visibility))
}
pub(crate) async fn get_public_rooms_filtered_helper(
@@ -236,24 +212,24 @@ pub(crate) async fn get_public_rooms_filtered_helper(
.sending
.send_federation_request(
other_server,
federation::directory::get_public_rooms_filtered::v1::Request {
assign!(federation::directory::get_public_rooms_filtered::v1::Request::new(), {
limit,
since: since.map(ToOwned::to_owned),
filter: Filter {
filter: assign!(Filter::new(), {
generic_search_term: filter.generic_search_term.clone(),
room_types: filter.room_types.clone(),
},
}),
room_network: RoomNetwork::Matrix,
},
}),
)
.await?;
return Ok(get_public_rooms_filtered::v3::Response {
return Ok(assign!(get_public_rooms_filtered::v3::Response::new(), {
chunk: response.chunk,
prev_batch: response.prev_batch,
next_batch: response.next_batch,
total_room_count_estimate: response.total_room_count_estimate,
});
}));
}
// Use limit or else 10, with maximum 100
@@ -284,16 +260,24 @@ pub(crate) async fn get_public_rooms_filtered_helper(
.rooms
.directory
.public_rooms()
.map(ToOwned::to_owned)
.wide_then(|room_id| public_rooms_chunk(services, room_id))
.ready_filter_map(|chunk| {
.wide_then(async |room_id| {
let summary = services
.rooms
.summary
.build_local_room_summary(&room_id)
.await
.expect("room in public room directory should exist");
summary.into()
})
.ready_filter_map(|chunk: PublicRoomsChunk| {
if !filter.room_types.is_empty() && !filter.room_types.contains(&RoomTypeFilter::from(chunk.room_type.clone())) {
return None;
}
if let Some(query) = filter.generic_search_term.as_ref().map(|q| q.to_lowercase()) {
if let Some(name) = &chunk.name {
if name.as_str().to_lowercase().contains(&query) {
if name.to_lowercase().contains(&query) {
return Some(chunk);
}
}
@@ -320,7 +304,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
.collect()
.await;
all_rooms.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members));
all_rooms.sort_by_key(|r| std::cmp::Reverse(r.num_joined_members));
let total_room_count_estimate = UInt::try_from(all_rooms.len())
.unwrap_or_else(|_| uint!(0))
@@ -335,12 +319,12 @@ pub(crate) async fn get_public_rooms_filtered_helper(
.ge(&limit)
.then_some(format!("n{}", num_since.expected_add(limit)));
Ok(get_public_rooms_filtered::v3::Response {
Ok(assign!(get_public_rooms_filtered::v3::Response::new(), {
chunk,
prev_batch,
next_batch,
total_room_count_estimate,
})
}))
}
/// Checks whether the given user ID is allowed to publish the target room to
@@ -356,109 +340,25 @@ async fn user_can_publish_room(
// Server admins can always publish to their own room directory.
return Ok(true);
}
let (create_event, room_version, power_levels_content) = join!(
services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, ""),
let (room_version, room_creators, power_levels) = join!(
services.rooms.state.get_room_version(room_id),
services
.rooms
.state_accessor
.room_state_get_content::<RoomPowerLevelsEventContent>(
room_id,
&StateEventType::RoomPowerLevels,
""
)
services.rooms.state_accessor.get_room_creators(room_id),
services.rooms.state_accessor.get_room_power_levels(room_id),
);
let room_version = room_version
.as_ref()
.map_err(|_| err!(Request(NotFound("Unknown room"))))?;
let create_event = create_event.map_err(|_| err!(Request(NotFound("Unknown room"))))?;
if RoomVersion::new(room_version)
.expect("room version must be supported")
let room_version_rules = room_version.rules().unwrap();
if room_version_rules
.authorization
.explicitly_privilege_room_creators
&& room_creators.contains(user_id)
{
let create_content: RoomCreateEventContent =
serde_json::from_str(create_event.content().get())
.map_err(|_| err!(Database("Invalid event content for m.room.create")))?;
let is_creator = create_content
.additional_creators
.unwrap_or_default()
.into_iter()
.chain(once(create_event.sender().to_owned()))
.any(|sender| sender == user_id);
if is_creator {
return Ok(true);
}
}
match power_levels_content.map(RoomPowerLevels::from) {
| Ok(pl) => Ok(pl.user_can_send_state(user_id, StateEventType::RoomCanonicalAlias)),
| Err(e) =>
if e.is_not_found() {
Ok(create_event.sender() == user_id)
} else {
Err!(Database("Invalid event content for m.room.power_levels: {e}"))
},
}
}
async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> PublicRoomsChunk {
let name = services.rooms.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();
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::Knock => "knock".into(),
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
| _ => "invite".into(),
});
let guest_can_join = services.rooms.state_accessor.guest_can_join(&room_id);
let num_joined_members = services.rooms.state_cache.room_joined_count(&room_id);
let (
(avatar_url, canonical_alias, guest_can_join, join_rule, name),
(num_joined_members, room_type, topic, world_readable),
) = join(
join5(avatar_url, canonical_alias, guest_can_join, join_rule, name),
join4(num_joined_members, room_type, topic, world_readable),
)
.boxed()
.await;
PublicRoomsChunk {
avatar_url: avatar_url.into_option().unwrap_or_default().url,
canonical_alias,
guest_can_join,
join_rule: join_rule.unwrap_or_default(),
name,
num_joined_members: num_joined_members
.map(TryInto::try_into)
.map(Result::ok)
.flat_ok()
.unwrap_or_else(|| uint!(0)),
room_id,
room_type,
topic,
world_readable,
return Ok(true);
}
Ok(power_levels.user_can_send_state(user_id, StateEventType::RoomCanonicalAlias))
}
+99 -118
View File
@@ -5,25 +5,23 @@
use axum::extract::State;
use conduwuit::{
Err, Error, Result, debug, debug_warn, err,
result::NotFound,
utils::{IterStream, stream::WidebandExt},
Err, Result, debug, debug_warn, err,
result::FlatOk,
utils::{IterStream, TryFutureExtExt, stream::WidebandExt},
};
use conduwuit_service::{Services, users::parse_master_key};
use futures::{StreamExt, stream::FuturesUnordered};
use ruma::{
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
api::{
client::{
error::ErrorKind,
keys::{
claim_keys, get_key_changes, get_keys, upload_keys,
upload_signatures::{self},
upload_signing_keys,
},
client::keys::{
claim_keys, get_key_changes, get_keys, upload_keys,
upload_signatures::{self},
upload_signing_keys,
},
federation,
},
assign,
encryption::CrossSigningKey,
serde::Raw,
};
@@ -66,6 +64,27 @@ pub(crate) async fn upload_keys_route(
.await?;
}
for (key_id, fallback_key) in &body.fallback_keys {
if fallback_key
.deserialize()
.inspect_err(|e| {
debug_warn!(
%key_id,
?fallback_key,
"Invalid one time key JSON submitted by client, skipping: {e}"
);
})
.is_err()
{
continue;
}
services
.users
.add_fallback_key(sender_user, sender_device, key_id, fallback_key, false)
.await?;
}
if let Some(device_keys) = &body.device_keys {
let deser_device_keys = device_keys.deserialize().map_err(|e| {
err!(Request(BadJson(debug_warn!(
@@ -115,12 +134,12 @@ pub(crate) async fn upload_keys_route(
}
}
Ok(upload_keys::v3::Response {
one_time_key_counts: services
.users
.count_one_time_keys(sender_user, sender_device)
.await,
})
let one_time_key_counts = services
.users
.count_one_time_keys(sender_user, sender_device)
.await;
Ok(upload_keys::v3::Response::new(one_time_key_counts))
}
/// # `POST /_matrix/client/r0/keys/query`
@@ -174,7 +193,7 @@ pub(crate) async fn upload_signing_keys_route(
) -> Result<upload_signing_keys::v3::Response> {
let sender_user = body.sender_user();
match check_for_new_keys(
if uiaa_needed_to_upload_keys(
services,
sender_user,
body.self_signing_key.as_ref(),
@@ -182,25 +201,11 @@ pub(crate) async fn upload_signing_keys_route(
body.master_key.as_ref(),
)
.await
.inspect_err(|e| debug!(?e))
{
| Ok(exists) => {
if let Some(result) = exists {
// No-op, they tried to reupload the same set of keys
// (lost connection for example)
return Ok(result);
}
debug!(
"Skipping UIA in accordance with MSC3967, the user didn't have any existing keys"
);
// Some of the keys weren't found, so we let them upload
},
| _ => {
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
},
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
}
services
@@ -214,77 +219,56 @@ pub(crate) async fn upload_signing_keys_route(
)
.await?;
Ok(upload_signing_keys::v3::Response {})
Ok(upload_signing_keys::v3::Response::new())
}
async fn check_for_new_keys(
async fn uiaa_needed_to_upload_keys(
services: crate::State,
user_id: &UserId,
self_signing_key: Option<&Raw<CrossSigningKey>>,
user_signing_key: Option<&Raw<CrossSigningKey>>,
master_signing_key: Option<&Raw<CrossSigningKey>>,
) -> Result<Option<upload_signing_keys::v3::Response>> {
debug!("checking for existing keys");
let mut empty = false;
if let Some(master_signing_key) = master_signing_key {
let (key, value) = parse_master_key(user_id, master_signing_key)?;
let result = services
.users
.get_master_key(None, user_id, &|_| true)
.await;
if result.is_not_found() {
empty = true;
} else {
let existing_master_key = result?;
let (existing_key, existing_value) = parse_master_key(user_id, &existing_master_key)?;
if existing_key != key || existing_value != value {
return Err!(Request(Forbidden(
"Tried to change an existing master key, UIA required"
)));
}
}
}
if let Some(user_signing_key) = user_signing_key {
let key = services.users.get_user_signing_key(user_id).await;
if key.is_not_found() && !empty {
return Err!(Request(Forbidden(
"Tried to update an existing user signing key, UIA required"
)));
}
if !key.is_not_found() {
let existing_signing_key = key?.deserialize()?;
if existing_signing_key != user_signing_key.deserialize()? {
return Err!(Request(Forbidden(
"Tried to change an existing user signing key, UIA required"
)));
}
}
}
if let Some(self_signing_key) = self_signing_key {
let key = services
) -> bool {
let (self_signing_key, user_signing_key, master_signing_key) = (
self_signing_key.map(Raw::deserialize).flat_ok(),
user_signing_key.map(Raw::deserialize).flat_ok(),
master_signing_key.map(Raw::deserialize).flat_ok(),
);
let (existing_self_signing_key, existing_user_signing_key, existing_master_signing_key) = futures::join!(
services
.users
.get_self_signing_key(None, user_id, &|_| true)
.await;
if key.is_not_found() && !empty {
debug!(?key);
return Err!(Request(Forbidden(
"Tried to add a new signing key independently from the master key"
)));
}
if !key.is_not_found() {
let existing_signing_key = key?.deserialize()?;
if existing_signing_key != self_signing_key.deserialize()? {
return Err!(Request(Forbidden(
"Tried to update an existing self signing key, UIA required"
)));
}
}
}
if empty {
return Ok(None);
}
.ok(),
services.users.get_user_signing_key(user_id).ok(),
services.users.get_master_key(None, user_id, &|_| true).ok(),
);
Ok(Some(upload_signing_keys::v3::Response {}))
let (existing_self_signing_key, existing_user_signing_key, existing_master_signing_key) = (
existing_self_signing_key
.as_ref()
.map(Raw::deserialize)
.flat_ok(),
existing_user_signing_key
.as_ref()
.map(Raw::deserialize)
.flat_ok(),
existing_master_signing_key
.as_ref()
.map(Raw::deserialize)
.flat_ok(),
);
if let Some(existing_master_signing_key) = existing_master_signing_key {
// If a master key exists, UIAA is required if any of the keys are different.
master_signing_key != Some(existing_master_signing_key)
|| user_signing_key != existing_user_signing_key
|| self_signing_key != existing_self_signing_key
} else {
// If no master key exists, UIAA is not required.
false
}
}
/// # `POST /_matrix/client/r0/keys/signatures/upload`
@@ -343,7 +327,7 @@ pub(crate) async fn upload_signatures_route(
}
}
Ok(upload_signatures::v3::Response { failures: BTreeMap::new() })
Ok(upload_signatures::v3::Response::new())
}
/// # `POST /_matrix/client/r0/keys/changes`
@@ -363,18 +347,17 @@ pub(crate) async fn get_key_changes_route(
let from = body
.from
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?;
.map_err(|_| err!(Request(InvalidParam("Invalid `from`."))))?;
let to = body
.to
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?;
.map_err(|_| err!(Request(InvalidParam("Invalid `to`."))))?;
device_list_updates.extend(
services
.users
.keys_changed(sender_user, Some(from), Some(to))
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
@@ -385,18 +368,18 @@ pub(crate) async fn get_key_changes_route(
device_list_updates.extend(
services
.users
.room_keys_changed(room_id, Some(from), Some(to))
.room_keys_changed(&room_id, Some(from), Some(to))
.map(|(user_id, _)| user_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
}
Ok(get_key_changes::v3::Response {
changed: device_list_updates.into_iter().collect(),
left: Vec::new(), // TODO
})
Ok(get_key_changes::v3::Response::new(
device_list_updates.into_iter().collect(),
// TODO
vec![],
))
}
pub(crate) async fn get_keys_helper<F>(
@@ -433,10 +416,10 @@ pub(crate) async fn get_keys_helper<F>(
let mut devices = services.users.all_device_ids(user_id).boxed();
while let Some(device_id) = devices.next().await {
if let Ok(mut keys) = services.users.get_device_keys(user_id, device_id).await {
if let Ok(mut keys) = services.users.get_device_keys(user_id, &device_id).await {
let metadata = services
.users
.get_device_metadata(user_id, device_id)
.get_device_metadata(user_id, &device_id)
.await
.map_err(|_| {
err!(Database("all_device_keys contained nonexistent device."))
@@ -445,7 +428,7 @@ pub(crate) async fn get_keys_helper<F>(
add_unsigned_device_display_name(&mut keys, metadata, include_display_names)
.map_err(|_| err!(Database("invalid device keys in database")))?;
container.insert(device_id.to_owned(), keys);
container.insert(device_id.clone(), keys);
}
}
@@ -506,8 +489,7 @@ pub(crate) async fn get_keys_helper<F>(
device_keys_input_fed.insert(user_id.to_owned(), keys.clone());
}
let request =
federation::keys::get_keys::v1::Request { device_keys: device_keys_input_fed };
let request = federation::keys::get_keys::v1::Request::new(device_keys_input_fed);
let response = tokio::time::timeout(
timeout,
services.sending.send_federation_request(server, request),
@@ -561,13 +543,13 @@ pub(crate) async fn get_keys_helper<F>(
}
}
Ok(get_keys::v3::Response {
Ok(assign!(get_keys::v3::Response::new(), {
failures,
device_keys,
master_keys,
self_signing_keys,
user_signing_keys,
})
}))
}
fn add_unsigned_device_display_name(
@@ -576,7 +558,8 @@ fn add_unsigned_device_display_name(
include_display_names: bool,
) -> serde_json::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::<serde_json::Map<String, serde_json::Value>>()?;
let unsigned = object.entry("unsigned").or_insert_with(|| json!({}));
if let serde_json::Value::Object(unsigned_object) = unsigned {
@@ -642,9 +625,7 @@ pub(crate) async fn claim_keys_helper(
timeout,
services.sending.send_federation_request(
server,
federation::keys::claim_keys::v1::Request {
one_time_keys: one_time_keys_input_fed,
},
federation::keys::claim_keys::v1::Request::new(one_time_keys_input_fed),
),
)
.await
@@ -667,5 +648,5 @@ pub(crate) async fn claim_keys_helper(
}
}
Ok(claim_keys::v3::Response { failures, one_time_keys })
Ok(assign!(claim_keys::v3::Response::new(one_time_keys), { failures: failures }))
}
+42 -87
View File
@@ -1,7 +1,7 @@
use std::time::Duration;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, err,
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
@@ -9,11 +9,11 @@
use conduwuit_core::error;
use conduwuit_service::{
Services,
media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta, MXC_LENGTH},
media::{Dim, FileMeta, MXC_LENGTH},
};
use reqwest::Url;
use ruma::{
Mxc, UserId,
UserId,
api::client::{
authenticated_media::{
get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
@@ -21,7 +21,9 @@
},
media::create_content,
},
assign,
};
use service::media::mxc::Mxc;
use crate::Ruma;
@@ -30,9 +32,9 @@ pub(crate) async fn get_media_config_route(
State(services): State<crate::State>,
_body: Ruma<get_media_config::v1::Request>,
) -> Result<get_media_config::v1::Response> {
Ok(get_media_config::v1::Response {
upload_size: ruma_from_usize(services.server.config.max_request_size),
})
Ok(get_media_config::v1::Response::new(ruma_from_usize(
services.server.config.max_request_size,
)))
}
/// # `POST /_matrix/media/v3/upload`
@@ -49,7 +51,7 @@ pub(crate) async fn get_media_config_route(
)]
pub(crate) async fn create_content_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<create_content::v3::Request>,
) -> Result<create_content::v3::Response> {
let user = body.sender_user();
@@ -82,10 +84,9 @@ pub(crate) async fn create_content_route(
.flatten()
});
Ok(create_content::v3::Response {
content_uri: mxc.to_string().into(),
Ok(assign!(create_content::v3::Response::new(mxc.to_string().into()), {
blurhash: blurhash.flatten(),
})
}))
}
/// # `GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}`
@@ -99,7 +100,7 @@ pub(crate) async fn create_content_route(
)]
pub(crate) async fn get_content_thumbnail_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_content_thumbnail::v1::Request>,
) -> Result<get_content_thumbnail::v1::Response> {
let user = body.sender_user();
@@ -114,7 +115,7 @@ pub(crate) async fn get_content_thumbnail_route(
content,
content_type,
content_disposition,
} = match fetch_thumbnail(&services, &mxc, user, body.timeout_ms, &dim).await {
} = match fetch_thumbnail_meta(&services, &mxc, user, body.timeout_ms, &dim).await {
| Ok(meta) => meta,
| Err(conduwuit::Error::Io(e)) => match e.kind() {
| std::io::ErrorKind::NotFound =>
@@ -128,13 +129,14 @@ pub(crate) async fn get_content_thumbnail_route(
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching thumbnail."))),
};
Ok(get_content_thumbnail::v1::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
let content_disposition =
make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
Ok(get_content_thumbnail::v1::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
content_disposition,
})
))
}
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}`
@@ -148,7 +150,7 @@ pub(crate) async fn get_content_thumbnail_route(
)]
pub(crate) async fn get_content_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_content::v1::Request>,
) -> Result<get_content::v1::Response> {
let user = body.sender_user();
@@ -161,7 +163,7 @@ pub(crate) async fn get_content_route(
content,
content_type,
content_disposition,
} = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await {
} = match fetch_file_meta(&services, &mxc, user, body.timeout_ms).await {
| Ok(meta) => meta,
| Err(conduwuit::Error::Io(e)) => match e.kind() {
| std::io::ErrorKind::NotFound => return Err!(Request(NotFound("Media not found."))),
@@ -174,13 +176,14 @@ pub(crate) async fn get_content_route(
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching file."))),
};
Ok(get_content::v1::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
let content_disposition =
make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
Ok(get_content::v1::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
content_disposition,
})
))
}
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}`
@@ -194,7 +197,7 @@ pub(crate) async fn get_content_route(
)]
pub(crate) async fn get_content_as_filename_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_content_as_filename::v1::Request>,
) -> Result<get_content_as_filename::v1::Response> {
let user = body.sender_user();
@@ -208,7 +211,7 @@ pub(crate) async fn get_content_as_filename_route(
content,
content_type,
content_disposition,
} = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await {
} = match fetch_file_meta(&services, &mxc, user, body.timeout_ms).await {
| Ok(meta) => meta,
| Err(conduwuit::Error::Io(e)) => match e.kind() {
| std::io::ErrorKind::NotFound => return Err!(Request(NotFound("Media not found."))),
@@ -221,13 +224,17 @@ pub(crate) async fn get_content_as_filename_route(
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching file."))),
};
Ok(get_content_as_filename::v1::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
let content_disposition = make_content_disposition(
content_disposition.as_ref(),
content_type.as_deref(),
Some(&body.filename),
);
Ok(get_content_as_filename::v1::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
content_disposition,
})
))
}
/// # `GET /_matrix/client/v1/media/preview_url`
@@ -241,7 +248,7 @@ pub(crate) async fn get_content_as_filename_route(
)]
pub(crate) async fn get_media_preview_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_media_preview::v1::Request>,
) -> Result<get_media_preview::v1::Response> {
let sender_user = body.sender_user();
@@ -278,58 +285,6 @@ pub(crate) async fn get_media_preview_route(
})
}
async fn fetch_thumbnail(
services: &Services,
mxc: &Mxc<'_>,
user: &UserId,
timeout_ms: Duration,
dim: &Dim,
) -> Result<FileMeta> {
let FileMeta {
content,
content_type,
content_disposition,
} = fetch_thumbnail_meta(services, mxc, user, timeout_ms, dim).await?;
let content_disposition = Some(make_content_disposition(
content_disposition.as_ref(),
content_type.as_deref(),
None,
));
Ok(FileMeta {
content,
content_type,
content_disposition,
})
}
async fn fetch_file(
services: &Services,
mxc: &Mxc<'_>,
user: &UserId,
timeout_ms: Duration,
filename: Option<&str>,
) -> Result<FileMeta> {
let FileMeta {
content,
content_type,
content_disposition,
} = fetch_file_meta(services, mxc, user, timeout_ms).await?;
let content_disposition = Some(make_content_disposition(
content_disposition.as_ref(),
content_type.as_deref(),
filename,
));
Ok(FileMeta {
content,
content_type,
content_disposition,
})
}
async fn fetch_thumbnail_meta(
services: &Services,
mxc: &Mxc<'_>,
+80 -62
View File
@@ -1,20 +1,21 @@
#![allow(deprecated)]
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, err,
utils::{content_disposition::make_content_disposition, math::ruma_from_usize},
};
use conduwuit_service::media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta};
use conduwuit_service::media::{CORP_CROSS_ORIGIN, Dim, FileMeta};
use reqwest::Url;
use ruma::{
Mxc,
api::client::media::{
create_content, get_content, get_content_as_filename, get_content_thumbnail,
get_media_config, get_media_preview,
},
assign,
};
use service::media::mxc::Mxc;
use crate::{Ruma, RumaResponse, client::create_content_route};
@@ -25,9 +26,9 @@ pub(crate) async fn get_media_config_legacy_route(
State(services): State<crate::State>,
_body: Ruma<get_media_config::v3::Request>,
) -> Result<get_media_config::v3::Response> {
Ok(get_media_config::v3::Response {
upload_size: ruma_from_usize(services.server.config.max_request_size),
})
Ok(get_media_config::v3::Response::new(ruma_from_usize(
services.server.config.max_request_size,
)))
}
/// # `GET /_matrix/media/v1/config`
@@ -52,7 +53,7 @@ pub(crate) async fn get_media_config_legacy_legacy_route(
#[tracing::instrument(skip_all, fields(%client), name = "url_preview_legacy", level = "debug")]
pub(crate) async fn get_media_preview_legacy_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_media_preview::v3::Request>,
) -> Result<get_media_preview::v3::Response> {
let sender_user = body.sender_user();
@@ -94,10 +95,10 @@ pub(crate) async fn get_media_preview_legacy_route(
/// Returns URL preview.
pub(crate) async fn get_media_preview_legacy_legacy_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_media_preview::v3::Request>,
) -> Result<RumaResponse<get_media_preview::v3::Response>> {
get_media_preview_legacy_route(State(services), InsecureClientIp(client), body)
get_media_preview_legacy_route(State(services), ClientIp(client), body)
.await
.map(RumaResponse)
}
@@ -114,10 +115,10 @@ pub(crate) async fn get_media_preview_legacy_legacy_route(
/// - Media will be saved in the media/ directory
pub(crate) async fn create_content_legacy_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<create_content::v3::Request>,
) -> Result<RumaResponse<create_content::v3::Response>> {
create_content_route(State(services), InsecureClientIp(client), body)
create_content_route(State(services), ClientIp(client), body)
.await
.map(RumaResponse)
}
@@ -133,7 +134,7 @@ pub(crate) async fn create_content_legacy_route(
#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
pub(crate) async fn get_content_legacy_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_content::v3::Request>,
) -> Result<get_content::v3::Response> {
let mxc = Mxc {
@@ -153,13 +154,16 @@ pub(crate) async fn get_content_legacy_route(
None,
);
Ok(get_content::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
Ok(assign!(
get_content::v3::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
}
))
},
| _ =>
if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
@@ -177,13 +181,16 @@ pub(crate) async fn get_content_legacy_route(
None,
);
Ok(get_content::v3::Response {
file: response.file,
content_type: response.content_type,
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
Ok(assign!(
get_content::v3::Response::new(
response.file,
response.content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
}
))
} else {
Err!(Request(NotFound("Media not found.")))
},
@@ -205,10 +212,10 @@ pub(crate) async fn get_content_legacy_route(
#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
pub(crate) async fn get_content_legacy_legacy_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_content::v3::Request>,
) -> Result<RumaResponse<get_content::v3::Response>> {
get_content_legacy_route(State(services), InsecureClientIp(client), body)
get_content_legacy_route(State(services), ClientIp(client), body)
.await
.map(RumaResponse)
}
@@ -224,7 +231,7 @@ pub(crate) async fn get_content_legacy_legacy_route(
#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
pub(crate) async fn get_content_as_filename_legacy_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_content_as_filename::v3::Request>,
) -> Result<get_content_as_filename::v3::Response> {
let mxc = Mxc {
@@ -244,13 +251,15 @@ pub(crate) async fn get_content_as_filename_legacy_route(
Some(&body.filename),
);
Ok(get_content_as_filename::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
Ok(assign!(get_content_as_filename::v3::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
}
))
},
| _ =>
if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
@@ -268,13 +277,16 @@ pub(crate) async fn get_content_as_filename_legacy_route(
None,
);
Ok(get_content_as_filename::v3::Response {
content_disposition: Some(content_disposition),
content_type: response.content_type,
file: response.file,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
Ok(assign!(
get_content_as_filename::v3::Response::new(
response.file,
response.content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
}
))
} else {
Err!(Request(NotFound("Media not found.")))
},
@@ -295,10 +307,10 @@ pub(crate) async fn get_content_as_filename_legacy_route(
/// seconds
pub(crate) async fn get_content_as_filename_legacy_legacy_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_content_as_filename::v3::Request>,
) -> Result<RumaResponse<get_content_as_filename::v3::Response>> {
get_content_as_filename_legacy_route(State(services), InsecureClientIp(client), body)
get_content_as_filename_legacy_route(State(services), ClientIp(client), body)
.await
.map(RumaResponse)
}
@@ -314,7 +326,7 @@ pub(crate) async fn get_content_as_filename_legacy_legacy_route(
#[tracing::instrument(skip_all, fields(%client), name = "media_thumbnail_get_legacy", level = "debug")]
pub(crate) async fn get_content_thumbnail_legacy_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_content_thumbnail::v3::Request>,
) -> Result<get_content_thumbnail::v3::Response> {
let mxc = Mxc {
@@ -335,13 +347,16 @@ pub(crate) async fn get_content_thumbnail_legacy_route(
None,
);
Ok(get_content_thumbnail::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
Ok(assign!(
get_content_thumbnail::v3::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
}
))
},
| _ =>
if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
@@ -359,13 +374,16 @@ pub(crate) async fn get_content_thumbnail_legacy_route(
None,
);
Ok(get_content_thumbnail::v3::Response {
file: response.file,
content_type: response.content_type,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
Ok(assign!(
get_content_thumbnail::v3::Response::new(
response.file,
response.content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
}
))
} else {
Err!(Request(NotFound("Media not found.")))
},
@@ -386,10 +404,10 @@ pub(crate) async fn get_content_thumbnail_legacy_route(
/// seconds
pub(crate) async fn get_content_thumbnail_legacy_legacy_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<get_content_thumbnail::v3::Request>,
) -> Result<RumaResponse<get_content_thumbnail::v3::Response>> {
get_content_thumbnail_legacy_route(State(services), InsecureClientIp(client), body)
get_content_thumbnail_legacy_route(State(services), ClientIp(client), body)
.await
.map(RumaResponse)
}
+10 -20
View File
@@ -1,7 +1,8 @@
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
use ruma::{
api::client::membership::ban_user,
assign,
events::room::member::{MembershipState, RoomMemberEventContent},
};
@@ -24,30 +25,19 @@ pub(crate) async fn ban_user_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let 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.rooms.state.mutex.lock(body.room_id.as_str()).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,
redact_events: body.redact_events,
..current_member_content
}),
PartialPdu::state(
body.user_id.to_string(),
&assign!(RoomMemberEventContent::new(MembershipState::Ban), {
reason: body.reason.clone(),
redact_events: body.redact_events,
}),
),
sender_user,
Some(&body.room_id),
&state_lock,
+61 -48
View File
@@ -1,19 +1,20 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
matrix::{event::gen_event_id_canonical_json, pdu::PartialPdu},
warn,
};
use futures::FutureExt;
use ruma::{
RoomId, UserId,
api::{client::membership::invite_user, federation::membership::create_invite},
events::{
invite_permission_config::FilterLevel,
room::member::{MembershipState, RoomMemberEventContent},
api::{
client::membership::invite_user::{self, v3::InviteUserId},
federation::membership::create_invite,
},
events::room::member::{MembershipState, RoomMemberEventContent},
};
use ruminuwuity::invite_permission_config::FilterLevel;
use service::Services;
use super::banned_room_check;
@@ -25,7 +26,7 @@
#[tracing::instrument(skip_all, fields(%client), name = "invite", level = "info")]
pub(crate) async fn invite_user_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<invite_user::v3::Request>,
) -> Result<invite_user::v3::Response> {
let sender_user = body.sender_user();
@@ -51,7 +52,11 @@ pub(crate) async fn invite_user_route(
.await?;
match &body.recipient {
| invite_user::v3::InvitationRecipient::UserId { user_id: recipient_user } => {
| invite_user::v3::InvitationRecipient::UserId(InviteUserId {
user_id: recipient_user,
reason,
..
}) => {
let sender_filter_level = services
.users
.invite_filter_level(recipient_user, sender_user)
@@ -59,7 +64,7 @@ pub(crate) async fn invite_user_route(
if !matches!(sender_filter_level, FilterLevel::Allow) {
// drop invites if the sender has the recipient filtered
return Ok(invite_user::v3::Response {});
return Ok(invite_user::v3::Response::new());
}
if let Ok(target_user_membership) = services
@@ -95,13 +100,13 @@ pub(crate) async fn invite_user_route(
sender_user,
recipient_user,
&body.room_id,
body.reason.clone(),
reason.clone(),
false,
)
.boxed()
.await?;
Ok(invite_user::v3::Response {})
Ok(invite_user::v3::Response::new())
},
| _ => {
Err!(Request(NotFound("User not found.")))
@@ -141,25 +146,28 @@ pub(crate) async fn invite_helper(
let (pdu, pdu_json, invite_room_state) = {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent {
avatar_url: services.users.avatar_url(recipient_user).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
};
let mut content = RoomMemberEventContent::new(MembershipState::Invite);
content.displayname = services.users.displayname(recipient_user).await.ok();
content.avatar_url = services.users.avatar_url(recipient_user).await.ok();
content.is_direct = Some(is_direct);
content.reason = reason;
let (pdu, pdu_json) = services
.rooms
.timeline
.create_hash_and_sign_event(
PduBuilder::state(recipient_user.to_string(), &content),
PartialPdu::state(recipient_user.to_string(), &content),
sender_user,
Some(room_id),
&state_lock,
)
.await?;
let invite_room_state = services.rooms.state.summary_stripped(&pdu, room_id).await;
let invite_room_state = services
.rooms
.state
.summary_stripped(&pdu, room_id, recipient_user)
.await;
drop(state_lock);
@@ -168,32 +176,39 @@ pub(crate) async fn invite_helper(
let room_version_id = services.rooms.state.get_room_version(room_id).await?;
let mut request = create_invite::v2::Request::new(
room_id.to_owned(),
(*pdu.event_id).to_owned(),
room_version_id.clone(),
services
.sending
.convert_to_outgoing_federation_event(pdu_json.clone())
.await,
invite_room_state,
);
request.via = services
.rooms
.state_cache
.servers_route_via(room_id)
.await
.ok();
let response = services
.sending
.send_federation_request(recipient_user.server_name(), create_invite::v2::Request {
room_id: room_id.to_owned(),
event_id: (*pdu.event_id).to_owned(),
room_version: room_version_id.clone(),
event: services
.sending
.convert_to_outgoing_federation_event(pdu_json.clone())
.await,
invite_room_state,
via: services
.rooms
.state_cache
.servers_route_via(room_id)
.await
.ok(),
})
.send_federation_request(recipient_user.server_name(), request)
.await?;
// We do not add the event_id field to the pdu here because of signature and
// hashes checks
let (event_id, value) = gen_event_id_canonical_json(&response.event, &room_version_id)
.map_err(|e| {
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
})?;
let (event_id, value) = gen_event_id_canonical_json(
&response.event,
&room_version_id
.rules()
.expect("room version should have defined rules"),
)
.map_err(|e| {
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
})?;
if pdu.event_id != event_id {
return Err!(Request(BadJson(warn!(
@@ -229,20 +244,18 @@ pub(crate) async fn invite_helper(
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent {
displayname: services.users.displayname(recipient_user).await.ok(),
avatar_url: services.users.avatar_url(recipient_user).await.ok(),
blurhash: services.users.blurhash(recipient_user).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
};
let mut content = RoomMemberEventContent::new(MembershipState::Invite);
content.displayname = services.users.displayname(recipient_user).await.ok();
content.avatar_url = services.users.avatar_url(recipient_user).await.ok();
content.blurhash = services.users.blurhash(recipient_user).await.ok();
content.is_direct = Some(is_direct);
content.reason = reason;
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(recipient_user.to_string(), &content),
PartialPdu::state(recipient_user.to_string(), &content),
sender_user,
Some(room_id),
&state_lock,
+24 -715
View File
@@ -1,60 +1,18 @@
use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, debug, debug_info, debug_warn, err, error, info, is_true,
matrix::{
StateKey,
event::{gen_event_id, gen_event_id_canonical_json},
pdu::{PduBuilder, PduEvent},
state_res,
},
Err, Result, debug,
result::FlatOk,
trace,
utils::{
self, shuffle,
stream::{IterStream, ReadyExt},
to_canonical_object,
},
warn,
utils::{shuffle, stream::IterStream},
};
use futures::{FutureExt, StreamExt, TryFutureExt};
use futures::{FutureExt, StreamExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
RoomVersionId, UserId,
api::{
client::{
error::ErrorKind,
membership::{join_room_by_id, join_room_by_id_or_alias},
},
federation::{self},
},
canonical_json::to_canonical_value,
events::{
StateEventType,
room::{
join_rules::JoinRule,
member::{MembershipState, RoomMemberEventContent},
},
},
OwnedRoomId, OwnedServerName, OwnedUserId, UserId,
api::client::membership::{join_room_by_id, join_room_by_id_or_alias},
};
use service::{
Services,
appservice::RegistrationInfo,
rooms::{
state::RoomMutexGuard,
state_compressor::{CompressedState, HashSetCompressStateEvent},
timeline::pdu_fits,
},
};
use tokio::join;
use super::{banned_room_check, validate_remote_member_event_stub};
use crate::{
Ruma,
server::{select_authorising_user, user_can_perform_restricted_join},
};
use super::banned_room_check;
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
///
@@ -67,7 +25,7 @@
#[tracing::instrument(skip_all, fields(%client), name = "join", level = "info")]
pub(crate) async fn join_room_by_id_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<join_room_by_id::v3::Request>,
) -> Result<join_room_by_id::v3::Response> {
let sender_user = body.sender_user();
@@ -89,7 +47,6 @@ pub(crate) async fn join_room_by_id_route(
.rooms
.state_cache
.servers_invite_via(&body.room_id)
.map(ToOwned::to_owned)
.collect()
.await;
@@ -115,16 +72,14 @@ pub(crate) async fn join_room_by_id_route(
shuffle(&mut servers);
let servers = deprioritize(servers, &services.config.deprioritize_joins_through_servers);
join_room_by_id_helper(
&services,
sender_user,
&body.room_id,
body.reason.clone(),
&servers,
&body.appservice_info,
)
.boxed()
.await
let room_id = services
.rooms
.membership
.join_room(sender_user, &body.room_id, body.reason.clone(), &servers)
.boxed()
.await?;
Ok(join_room_by_id::v3::Response::new(room_id))
}
/// # `POST /_matrix/client/r0/join/{roomIdOrAlias}`
@@ -139,11 +94,10 @@ pub(crate) async fn join_room_by_id_route(
#[tracing::instrument(skip_all, fields(%client), name = "join", level = "info")]
pub(crate) async fn join_room_by_id_or_alias_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
ClientIp(client): ClientIp,
body: Ruma<join_room_by_id_or_alias::v3::Request>,
) -> Result<join_room_by_id_or_alias::v3::Response> {
let sender_user = body.sender_user();
let appservice_info = &body.appservice_info;
let body = &body.body;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
@@ -169,7 +123,6 @@ pub(crate) async fn join_room_by_id_or_alias_route(
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
@@ -210,11 +163,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
)
.await?;
let addl_via_servers = services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned);
let addl_via_servers = services.rooms.state_cache.servers_invite_via(&room_id);
let addl_state_servers = services
.rooms
@@ -227,7 +176,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
.iter()
.map(|event| event.get_field("sender"))
.filter_map(FlatOk::flat_ok)
.map(|user: &UserId| user.server_name().to_owned())
.map(|user: OwnedUserId| user.server_name().to_owned())
.stream()
.chain(addl_via_servers)
.collect()
@@ -243,654 +192,14 @@ pub(crate) async fn join_room_by_id_or_alias_route(
};
let servers = deprioritize(servers, &services.config.deprioritize_joins_through_servers);
let join_room_response = join_room_by_id_helper(
&services,
sender_user,
&room_id,
body.reason.clone(),
&servers,
appservice_info,
)
.boxed()
.await?;
Ok(join_room_by_id_or_alias::v3::Response { room_id: join_room_response.room_id })
}
pub async fn join_room_by_id_helper(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
appservice_info: &Option<RegistrationInfo>,
) -> Result<join_room_by_id::v3::Response> {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let user_is_guest = services
.users
.is_deactivated(sender_user)
.await
.unwrap_or(false)
&& appservice_info.is_none();
if user_is_guest && !services.rooms.state_accessor.guest_can_join(room_id).await {
return Err!(Request(Forbidden("Guests are not allowed to join this room")));
}
if services
let room_id = services
.rooms
.state_cache
.is_joined(sender_user, room_id)
.await
{
debug_warn!("{sender_user} is already joined in {room_id}");
return Ok(join_room_by_id::v3::Response { room_id: room_id.into() });
}
if let Err(e) = services
.antispam
.user_may_join_room(
sender_user.to_owned(),
room_id.to_owned(),
services
.rooms
.state_cache
.is_invited(sender_user, room_id)
.await,
)
.await
{
warn!("Antispam prevented user {} from joining room {}: {}", sender_user, room_id, e);
return Err!(Request(Forbidden("You are not allowed to join this room.")));
}
let server_in_room = services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await;
// Only check our known membership if we're already in the room.
// See: https://forgejo.ellis.link/continuwuation/continuwuity/issues/855
let membership = if server_in_room {
services
.rooms
.state_accessor
.get_member(room_id, sender_user)
.await
} else {
debug!("Ignoring local state for join {room_id}, we aren't in the room yet.");
Ok(RoomMemberEventContent::new(MembershipState::Leave))
};
if let Ok(m) = membership {
if m.membership == MembershipState::Ban {
debug_warn!("{sender_user} is banned from {room_id} but attempted to join");
// TODO: return reason
return Err!(Request(Forbidden("You are banned from the room.")));
}
}
if !server_in_room && servers.is_empty() {
return Err!(Request(NotFound(
"No servers were provided to assist in joining the room remotely, and we are not \
already participating in the room."
)));
}
if services.antispam.check_all_joins() {
if let Err(e) = services
.antispam
.meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned())
.await
{
warn!("Antispam prevented user {} from joining room {}: {}", sender_user, room_id, e);
return Err!(Request(Forbidden("Antispam rejected join request.")));
}
}
if server_in_room {
join_room_by_id_helper_local(services, sender_user, room_id, reason, servers, state_lock)
.boxed()
.await?;
} else {
// Ask a remote server if we are not participating in this room
join_room_by_id_helper_remote(
services,
sender_user,
room_id,
reason,
servers,
state_lock,
)
.membership
.join_room(sender_user, &room_id, body.reason.clone(), &servers)
.boxed()
.await?;
}
Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
}
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_remote", level = "info")]
async fn join_room_by_id_helper_remote(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
state_lock: RoomMutexGuard,
) -> Result {
info!("Joining {room_id} over federation.");
let (make_join_response, remote_server) =
make_join_request(services, sender_user, room_id, servers).await?;
info!("make_join finished");
let room_version_id = make_join_response.room_version.unwrap_or(RoomVersionId::V1);
if !services.server.supported_room_version(&room_version_id) {
// How did we get here?
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut join_event_stub: CanonicalJsonObject =
serde_json::from_str(make_join_response.event.get()).map_err(|e| {
err!(BadServerResponse(warn!(
"Invalid make_join event json received from server: {e:?}"
)))
})?;
let join_authorized_via_users_server = {
use RoomVersionId::*;
if !matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
join_event_stub
.get("content")
.map(|s| {
s.as_object()?
.get("join_authorised_via_users_server")?
.as_str()
})
.and_then(|s| OwnedUserId::try_from(s.unwrap_or_default()).ok())
} else {
None
}
};
join_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
join_event_stub.insert(
"content".to_owned(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
join_authorized_via_users_server: join_authorized_via_users_server.clone(),
..RoomMemberEventContent::new(MembershipState::Join)
})
.expect("event is valid, we just created it"),
);
// We keep the "event_id" in the pdu only in v1 or
// v2 rooms
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
join_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
// Add event_id back
join_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let mut join_event = join_event_stub;
info!("Asking {remote_server} for send_join in room {room_id}");
let send_join_request = federation::membership::create_join_event::v2::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
omit_members: false,
pdu: services
.sending
.convert_to_outgoing_federation_event(join_event.clone())
.await,
};
let send_join_response = match services
.sending
.send_synapse_request(&remote_server, send_join_request)
.await
{
| Ok(response) => response,
| Err(e) => {
error!("send_join failed: {e}");
return Err(e);
},
};
info!("send_join finished");
if join_authorized_via_users_server.is_some() {
if let Some(signed_raw) = &send_join_response.room_state.event {
debug_info!(
"There is a signed event with join_authorized_via_users_server. This room is \
probably using restricted joins. Adding signature to our event"
);
let (signed_event_id, signed_value) =
gen_event_id_canonical_json(signed_raw, &room_version_id).map_err(|e| {
err!(Request(BadJson(warn!(
"Could not convert event to canonical JSON: {e}"
))))
})?;
if signed_event_id != event_id {
return Err!(Request(BadJson(warn!(
%signed_event_id, %event_id,
"Server {remote_server} sent event with wrong event ID"
))));
}
match signed_value["signatures"]
.as_object()
.ok_or_else(|| {
err!(BadServerResponse(warn!(
"Server {remote_server} sent invalid signatures type"
)))
})
.and_then(|e| {
e.get(remote_server.as_str()).ok_or_else(|| {
err!(BadServerResponse(warn!(
"Server {remote_server} did not send its signature for a restricted \
room"
)))
})
}) {
| Ok(signature) => {
join_event
.get_mut("signatures")
.expect("we created a valid pdu")
.as_object_mut()
.expect("we created a valid pdu")
.insert(remote_server.to_string(), signature.clone());
},
| Err(e) => {
warn!(
"Server {remote_server} sent invalid signature in send_join signatures \
for event {signed_value:?}: {e:?}",
);
},
}
}
}
services
.rooms
.short
.get_or_create_shortroomid(room_id)
.await;
info!("Parsing join event");
let parsed_join_pdu = PduEvent::from_id_val(&event_id, join_event.clone())
.map_err(|e| err!(BadServerResponse("Invalid join event PDU: {e:?}")))?;
info!("Acquiring server signing keys for response events");
let resp_events = &send_join_response.room_state;
let resp_state = &resp_events.state;
let resp_auth = &resp_events.auth_chain;
services
.server_keys
.acquire_events_pubkeys(resp_auth.iter().chain(resp_state.iter()))
.await;
info!("Going through send_join response room_state");
let cork = services.db.cork_and_flush();
let state = send_join_response
.room_state
.state
.iter()
.stream()
.then(|pdu| {
services
.server_keys
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
.inspect_err(|e| {
debug_warn!("Could not validate send_join response room_state event: {e:?}");
})
.inspect(|_| debug!("Completed validating send_join response room_state event"))
})
.ready_filter_map(Result::ok)
.fold(HashMap::new(), |mut state, (event_id, value)| async move {
let pdu = match PduEvent::from_id_val(&event_id, value.clone()) {
| Ok(pdu) => pdu,
| Err(e) => {
debug_warn!("Invalid PDU in send_join response: {e:?}: {value:#?}");
return state;
},
};
if !pdu_fits(&mut value.clone()) {
warn!(
"dropping incoming PDU {event_id} in room {room_id} from room join because \
it exceeds 65535 bytes or is otherwise too large."
);
return state;
}
services.rooms.outlier.add_pdu_outlier(&event_id, &value);
if let Some(state_key) = &pdu.state_key {
let shortstatekey = services
.rooms
.short
.get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key)
.await;
state.insert(shortstatekey, pdu.event_id.clone());
}
state
})
.await;
drop(cork);
info!("Going through send_join response auth_chain");
let cork = services.db.cork_and_flush();
send_join_response
.room_state
.auth_chain
.iter()
.stream()
.then(|pdu| {
services
.server_keys
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
})
.ready_filter_map(Result::ok)
.ready_for_each(|(event_id, value)| {
trace!(%event_id, "Adding PDU as an outlier from send_join auth_chain");
services.rooms.outlier.add_pdu_outlier(&event_id, &value);
})
.await;
drop(cork);
debug!("Running send_join auth check");
let fetch_state = &state;
let state_fetch = |k: StateEventType, s: StateKey| async move {
let shortstatekey = services.rooms.short.get_shortstatekey(&k, &s).await.ok()?;
let event_id = fetch_state.get(&shortstatekey)?;
services.rooms.timeline.get_pdu(event_id).await.ok()
};
let auth_check = state_res::event_auth::auth_check(
&state_res::RoomVersion::new(&room_version_id)?,
&parsed_join_pdu,
None, // TODO: third party invite
|k, s| state_fetch(k.clone(), s.into()),
&state_fetch(StateEventType::RoomCreate, "".into())
.await
.expect("create event is missing from send_join auth"),
)
.await
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
if !auth_check {
return Err!(Request(Forbidden("Auth check failed")));
}
info!("Compressing state from send_join");
let compressed: CompressedState = services
.rooms
.state_compressor
.compress_state_events(state.iter().map(|(ssk, eid)| (ssk, eid.borrow())))
.collect()
.await;
debug!("Saving compressed state");
let HashSetCompressStateEvent {
shortstatehash: statehash_before_join,
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_join, added, removed, &state_lock)
.await?;
debug!("Updating joined counts for new room");
services
.rooms
.state_cache
.update_joined_count(room_id)
.await;
// We append to state before appending the pdu, so we don't have a moment in
// time with the pdu without it's state. This is okay because append_pdu can't
// fail.
let statehash_after_join = services
.rooms
.state
.append_to_state(&parsed_join_pdu, room_id)
.await?;
info!("Appending new room join event");
services
.rooms
.timeline
.append_pdu(
&parsed_join_pdu,
join_event,
once(parsed_join_pdu.event_id.borrow()),
&state_lock,
room_id,
)
.await?;
info!("Setting final room state for new room");
// 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_join, &state_lock);
Ok(())
}
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_local", level = "info")]
async fn join_room_by_id_helper_local(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
state_lock: RoomMutexGuard,
) -> Result {
info!("Joining room locally");
let (room_version, join_rules, is_invited) = join!(
services.rooms.state.get_room_version(room_id),
services.rooms.state_accessor.get_join_rules(room_id),
services.rooms.state_cache.is_invited(sender_user, room_id)
);
let room_version = room_version?;
let mut auth_user: Option<OwnedUserId> = None;
if !is_invited && matches!(join_rules, JoinRule::Restricted(_) | JoinRule::KnockRestricted(_))
{
use RoomVersionId::*;
if !matches!(room_version, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
// This is a restricted room, check if we can complete the join requirements
// locally.
let needs_auth_user =
user_can_perform_restricted_join(services, sender_user, room_id, &room_version)
.await;
if needs_auth_user.is_ok_and(is_true!()) {
// If there was an error or the value is false, we'll try joining over
// federation. Since it's Ok(true), we can authorise this locally.
// If we can't select a local user, this will remain None, the join will fail,
// and we'll fall back to federation.
auth_user = select_authorising_user(services, room_id, sender_user, &state_lock)
.await
.ok();
}
}
}
let content = RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason: reason.clone(),
join_authorized_via_users_server: auth_user,
..RoomMemberEventContent::new(MembershipState::Join)
};
// Try normal join first
let Err(error) = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(sender_user.to_string(), &content),
sender_user,
Some(room_id),
&state_lock,
)
.await
else {
info!("Joined room locally");
return Ok(());
};
if servers.is_empty() || servers.len() == 1 && services.globals.server_is_ours(&servers[0]) {
if !services.rooms.metadata.exists(room_id).await {
return Err!(Request(
Unknown(
"Room was not found locally and no servers were found to help us discover it"
),
NOT_FOUND
));
}
return Err(error);
}
info!(
?error,
remote_servers = %servers.len(),
"Could not join room locally, attempting remote join",
);
join_room_by_id_helper_remote(services, sender_user, room_id, reason, servers, state_lock)
.await
}
async fn make_join_request(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
servers: &[OwnedServerName],
) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> {
let mut make_join_counter: usize = 1;
for remote_server in servers {
if services.globals.server_is_ours(remote_server) {
continue;
}
info!(
"Asking {remote_server} for make_join (attempt {make_join_counter}/{})",
servers.len()
);
let make_join_response = services
.sending
.send_federation_request(
remote_server,
federation::membership::prepare_join_event::v1::Request {
room_id: room_id.to_owned(),
user_id: sender_user.to_owned(),
ver: services.server.supported_room_versions().collect(),
},
)
.await;
trace!("make_join response: {:?}", make_join_response);
make_join_counter = make_join_counter.saturating_add(1);
match make_join_response {
| Ok(response) => {
info!("Received make_join response from {remote_server}");
if let Err(e) = validate_remote_member_event_stub(
&MembershipState::Join,
sender_user,
room_id,
&to_canonical_object(&response.event)?,
) {
warn!("make_join response from {remote_server} failed validation: {e}");
continue;
}
return Ok((response, remote_server.clone()));
},
| Err(e) => match e.kind() {
| ErrorKind::UnableToAuthorizeJoin => {
info!(
"{remote_server} was unable to verify the joining user satisfied \
restricted join requirements: {e}. Will continue trying."
);
},
| ErrorKind::UnableToGrantJoin => {
info!(
"{remote_server} believes the joining user satisfies restricted join \
rules, but is unable to authorise a join for us. Will continue trying."
);
},
| ErrorKind::IncompatibleRoomVersion { room_version } => {
warn!(
"{remote_server} reports the room we are trying to join is \
v{room_version}, which we do not support."
);
return Err(e);
},
| ErrorKind::Forbidden { .. } => {
warn!("{remote_server} refuses to let us join: {e}.");
return Err(e);
},
| ErrorKind::NotFound => {
info!(
"{remote_server} does not know about {room_id}: {e}. Will continue \
trying."
);
},
| _ => {
info!("{remote_server} failed to make_join: {e}. Will continue trying.");
},
},
}
}
info!("All {} servers were unable to assist in joining {room_id} :(", servers.len());
Err!(BadServerResponse("No server available to assist in joining."))
Ok(join_room_by_id_or_alias::v3::Response::new(room_id))
}
/// Moves deprioritized servers (if any) to the back of the list.

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