Compare commits

...

288 Commits

Author SHA1 Message Date
strawberry
d0069cc100 log out any sessions when the server emergency password is unset
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-12 14:04:47 -04:00
Matthias Ahouansou
556e78214a fix: only allow the server user to set the admin alias
Should make it safer to move the alias if the admin room broke on a public server.

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-12 14:04:47 -04:00
strawberry
8fff7ea706 cleanup+refactor admin room alias and server account accessing to globals
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-12 14:04:47 -04:00
Matthias Ahouansou
f712c0cefb fix: restrict who can remove aliases
Previously, anyone could remove any local alias, meaning that someone could re-route a popular alias elsewhere
Now, only the creator of the alias, users who can set canonical aliases for the room, server admins and the server user can delete aliases

added some additional changes/fixes to adapt to our codebase

Co-authored-by: strawberry <strawberry@puppygock.gay>
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-12 14:04:47 -04:00
Matthias Ahouansou
26d103d314 fix: only process admin commands if server user is in the room
Should prevent some edge cases with a misconfigured admin room

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-12 14:04:47 -04:00
Benjamin Lee
0688a96c37 drop redacted events from search results
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-12 14:04:47 -04:00
Benjamin Lee
eb73d8c669 fix: de-index pdus when redacted
bit of code dedupe as well

Co-authored-by: strawberry <strawberry@puppygock.gay>
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-12 14:04:47 -04:00
Benjamin Lee
20a54aacd6 factor search tokenization out into a function
This ensures that the tokenization algorithm will remain in sync between
querying, indexing, and deindexing. The existing code had slightly
different behavior for querying, because it did not discard words with
>50 bytes. This was inconsequential, because >50 byte tokens are never
present in the index.

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-12 14:04:47 -04:00
Benjamin Lee
81cd677b4e fix dropped events in search
The previous code would drop some events entirely if any events between
`skip` and `skip + limit` were not visible to the user. This would cause
the set of events skipped by the `skip(skip)` method to extend past
`skip` in the raw result set, because `skip(skip)` was being called
*after* filtering out invisible events.

This bug will become much more severe with a full filtering
implementation, because it will be more likely for events to be filtered
out. Currently, it is only possible to trigger with rooms that have
history visibility set to "invited" or "joined".

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-12 14:04:47 -04:00
Benjamin Lee
73da353e52 fix missing next_batch for search
The previous code would fail to return next_batch if any of the events
in the window were not visible to the user. It would also return an
unnecessary next_batch when no more results are available if the total
number of results is exactly `skip + limit`.

This bug will become much more severe with a full filtering
implementation, because we will be more likely to trigger it by
filtering out events in a search call. Currently, it is only possible to
trigger with rooms that have history visibility set to "invited" or
"joined".

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-12 14:04:47 -04:00
strawberry
d5677b6ae7 bump cargo.lock due to yanked httparse
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-11 20:18:28 -04:00
strawberry
01a77f8a71 add replaces_state and prev_sender in unsigned for membership changes
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-11 13:03:32 -04:00
strawberry
ea03a50e21 remove pfps/avatars and display names upon deactivation
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-11 01:19:18 -04:00
strawberry
25d44cad31 slightly dedupe pfp/displayname updates, update blurhash in PDUs too
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-11 01:19:18 -04:00
strawberry
91519959ed mark account as deactivated before leaving rooms
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-11 01:19:18 -04:00
strawberry
2e31bcc213 use our fork of axum-server instead
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-11 01:19:18 -04:00
strawberry
305dfc3b42 adminroom: cmds to view room topic and room members
this will be extended more

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 23:38:03 -04:00
strawberry
65fbb80145 adminroom: leave all rooms by default on manual deactivations
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 21:14:18 -04:00
strawberry
f1d90e5df6 log client/remote IP address on various routes tracing calls
this uses InsecureClientIp as this is purely for informational
and logging purposes

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 21:04:51 -04:00
strawberry
74b29ce067 adminroom: improved room list outputs, add counts
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
0e7c3cb338 adminroom: simplify codeblock checks
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
14a3471fcb adminroom: fix function typo
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
c834e86e67 adminroom: user cmds to put/get/delete room account data
primarily useful for inserting `m.server_notice` user account data
onto the admin room

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
b4f0a8a8b5 adminroom: clean up and optimise user commands
`deactivate-all` was terrible and incredibly inefficient

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
9bb90213e1 adminroom: add user ID parsing utils
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
fcdf1463ef refactor get_room_topic into 1 single function
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
88d038ffec refactor getting room's canonical alias into 1 function
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
4b4c0952a2 refactor guest_can_join into 1 single function
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
016270b33b adminroom: add server_in_room state cache accessor
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
d2063013b4 adminroom: add various state_cache db accessors
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
03ba9bde29 admin cmd to force download and use a server's room state
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
1287a86c05 client-api: export validate_and_add_event_id
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 19:45:04 -04:00
strawberry
8210e8c42e slightly adjust sliding sync code for ruma bump
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 17:20:59 -04:00
strawberry
adf0bfd894 bump ruma
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 16:57:44 -04:00
strawberry
6b843ec4dd switch to patch/fork of axum-server to fix shutdown hangs
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 16:55:40 -04:00
strawberry
ac02078395 bump cargo.lock, add tracing feature to axum-extra
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 16:55:40 -04:00
renovate[bot]
b9d38fd3ba chore(deps): update rust crate http-body-util to v0.1.2 2024-06-10 16:55:40 -04:00
renovate[bot]
1b2c8236fb chore(deps): update rust crate clap to v4.5.7 2024-06-10 16:55:40 -04:00
renovate[bot]
d7b8af627c chore(deps): update rust crate console-subscriber to 0.3 2024-06-10 16:55:40 -04:00
renovate[bot]
130aae8758 chore(deps): update rust crate url to v2.5.1 2024-06-10 16:55:40 -04:00
renovate[bot]
4741a76896 chore(deps): update rust crate regex to v1.10.5 2024-06-10 16:55:40 -04:00
strawberry
5bfb62e979 Revert "update complement results"
This reverts commit 9a3c52aa75.
2024-06-10 15:38:20 -04:00
strawberry
cb03654dc1 fix broken federated room invites/joins
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-10 14:53:26 -04:00
Jason Volk
f0557e3303 split migrations function
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:56:41 +00:00
Jason Volk
f52acd9cdf Fix idiomatic let if
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
Jason Volk
eae41fc411 Fix use-self
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
Jason Volk
c3c91e9d80 Fix suboptimal flops
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
Jason Volk
a8de5d1e60 Fix futures not Send
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
Jason Volk
7688d67870 Fix needless pass by ref mut
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
Jason Volk
89d7d48324 Fix equatable if let
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
Jason Volk
b525031a25 Fix derive partial eq without eq
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
Jason Volk
67f4285504 Fix branches sharing code
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
Jason Volk
b65f05ce19 simplify lifetime parameters
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
Jason Volk
db2c9f28b6 split admin room moderation commands
prior stack frame allocated 170 KiB

Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
Jason Volk
fc1b8326e6 split join_room_by_id_helper into local and remote
prior stack frame allocated 180 KiB

Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
Jason Volk
6e50b07bf5 Fix large future
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-09 20:06:50 +00:00
strawberry
9a3c52aa75 update complement results
they seem to be failures out of
our control (?)

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-09 12:05:30 -04:00
strawberry
ccf9f95cc9 retroactively fix bad data in roomuserid_joined, remove pointless prefix scans
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-09 03:55:04 -04:00
strawberry
0524e6ed52 remove unnecessary active_local_joined_users_in_room state_cache accessor
the underlying bug has been fixed

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-08 17:32:04 -04:00
strawberry
7f5b59afbb add conduwuit-specific db migration fixing double split db entries
for a few months now we accidentally had double 0xFF splits being
inserted into `roomuserid_joined` cf when membership counts and such
are being updated.

this is a conduwuit-specific db migration and does NOT break conduit
compatibility.

`fix_bad_double_separator_in_state_cache`

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-08 16:58:24 -04:00
strawberry
ab5db37851 fix bad double split insert on roomuserid_joined cf
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-08 16:57:48 -04:00
strawberry
c0c7f23a05 services(timeline): use self instead of services()
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
14ec41c211 services(state_cache): use self instead of services()
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
2230bc7339 services(state): use self instead of services()
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
0ebabba971 services(pdu_metadata): use self instead of services()
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
3ed561cb31 service(globals): use self instead of services()
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
a061644b2d service(event_handler): use self instead of services()
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
82ac6b01b2 service(auth_chain): use self instead of services()
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
97ddb2ce87 bump conduwuit to version 0.4.2
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
a04ff7d4af fix(fed): dont reject /state_ids/ on world readable rooms
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
10dfbf6420 fix(fed): dont reject /state/ on world readable rooms
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
d10bc67c9d fix(fed): dont reject /event_auth/ on world readable rooms
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
197a02bf8d fix(fed): dont reject /event/ on world readable rooms
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
8103bd7310 fix(fed): dont reject /backfill/ on world readable rooms
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
81487e3f07 fix(fed): dont reject /get_missing_events on world readable rooms
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
bfbb29dded add is_world_readable state_accessor func, use self instead of services()
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:10:00 -04:00
strawberry
1cc7cf54a7 add config option to allow guests to access TURN server
`turn_allow_guests`

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:09:59 -04:00
strawberry
40e4019f7f add missing TURN example config options
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-07 15:09:59 -04:00
Lux Aliaga
176d95c2a8 admin: media: Force flag on past media removal
When enabled, if a file is deemed unremovable, it skips past it and
continues deleting all other files that fit the criteria. Additionally,
fix age comparison under the same command.

Signed-off-by: Lux Aliaga <lux@nixgoat.me>
2024-06-07 15:08:08 -04:00
renovate[bot]
8d32fb1445 chore(deps): update sentry-rust monorepo to 0.34.0 2024-06-07 00:52:25 -04:00
renovate[bot]
82a3b73774 chore(deps): update rust crate clap to v4.5.6 2024-06-07 00:52:13 -04:00
renovate[bot]
1f19356693 chore(deps): update aquasecurity/trivy-action action to v0.22.0 2024-06-07 00:52:05 -04:00
Jason Volk
3ada847570 extract client ip from connection state
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-07 02:00:28 +00:00
Jason Volk
0bade5317f add connection info to router
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-07 02:00:28 +00:00
Jason Volk
c2267d4c03 add services state to router
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-07 02:00:28 +00:00
Jason Volk
aebae11c82 reintroduce the variadic macro for ruma handler.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-07 02:00:28 +00:00
Jason Volk
f871d8fd4e move impl FromRequest for Ruma up one level; some cleanup
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-07 02:00:28 +00:00
Jason Volk
4a68e28c71 use debug_warn for presence spam
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-07 02:00:28 +00:00
Jason Volk
6e59135a7d eliminate RotationHandler
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-07 02:00:28 +00:00
Jason Volk
0e74ade7d7 isolate axum shutdown in router; minor run-cycle/signalling tweaks
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-07 02:00:28 +00:00
Jason Volk
e4aa20ebeb move services ctor/dtor detail into service
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-06 18:21:40 +00:00
Jason Volk
427aa4645c cleanup/reduce some tracing spans.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-06 18:21:40 +00:00
Jason Volk
73718a1208 elminate generic argument in ruma_wrapper::auth
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-06 18:21:40 +00:00
Jason Volk
0e3d192ad2 fix trivial-casts
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-06 18:21:40 +00:00
Jason Volk
76a4d8aa4c additional clippy configuration
these are documentary values seeking review, not final standards set for the project.

Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-06 18:21:40 +00:00
Jason Volk
9bb52cb3ec add missing dev_release_log_level feature projection
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-06 18:21:40 +00:00
Jason Volk
dd49b3c3a1 fix/simplify emergency access initialization
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-06 18:21:40 +00:00
Jason Volk
b2e56777af fix missing toolchain in cargo smoketest run invocation.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-06 18:21:40 +00:00
Jason Volk
f32380772f rename api::client_server to api::client
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-06 18:21:40 +00:00
strawberry
8428f43c78 add legacy element hack for UIAA using invalid user field
see:
- e9302a9556
- https://github.com/element-hq/element-android/issues/8043
- https://github.com/element-hq/element-ios/issues/7405

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-06 10:57:00 -04:00
Jason Volk
3af153f5ae split s2s into units
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-06 04:41:27 -04:00
strawberry
38238c309f appservices: remove unnecessary services() call for self
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-05 18:18:08 -04:00
strawberry
0857fe7907 abstract+add more "users in room" accessors, check membership state on active_local_joined_users_in_room
`roomuserid_joined` cf seems unreliable, so in the mean time we need to check
membership state (or maybe this is a more reliable check anyways)

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-05 18:18:08 -04:00
strawberry
c738c119f8 delete unnecessary real_users_cache, fix overwriting push_target iter, add proper function for getting local active users in room
this `real_users_cache` cache seems weird, and i have no idea what
prompted its creation upstream. perhaps they did this because
sqlite was very slow and their rocksdb setup is very poor, so
a "solution" was to stick member counts in memory.
slow iterators, scanning, etc do not apply to conduwuit where
our rocksdb is extremely tuned, and i seriously doubt something
like this would have any real world net-positive performance impact.

also for some reason, there is suspicious logic where we
overwrite the entire push target collection.

both of these things could be a potential cause for receiving
notifications in rooms we've left.

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-05 18:18:08 -04:00
strawberry
c1227340b3 update complement results
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-05 18:17:46 -04:00
strawberry
bf10ff65a4 media: ignore Content-Type params, use binary_search
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-05 17:28:51 -04:00
strawberry
b781771a9b media: drop Content-Type detection support
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-05 16:33:53 -04:00
strawberry
df8ba04e31 media: trust client Content-Type again
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-05 16:33:53 -04:00
strawberry
19926ba00d sort ALLOWED_INLINE_CONTENT_TYPES
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-05 16:33:53 -04:00
strawberry
893cc50570 csp: set form-action 'none'
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-05 16:33:53 -04:00
strawberry
c9fbbdce1c csp: remove unusual directives, slight security improvement
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-05 02:50:44 -04:00
Jason Volk
732e8b82aa Abstract password hashing into util.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-05 03:00:23 +00:00
Tom Black
282c2feca8 Clarify purpose and temporary muting 2024-06-04 17:13:21 +01:00
strawberry
919735b4ce remove usages of &String and &Owned[..]
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 14:17:42 -04:00
strawberry
2e83e56a07 remove deleted config options and update address example option
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 14:17:42 -04:00
strawberry
ff7dfec74c slightly cleanup update check
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 14:17:42 -04:00
strawberry
84290bd668 update deps, remove unnecessary zstd crate, pin rust-rocksdb rev
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 14:17:32 -04:00
strawberry
b29a8791de admincmd: leave all rooms if deactivating all users with --force
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 00:10:28 -04:00
strawberry
83220b43a2 use saturating_add and vec with_capacity in even more places
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 00:10:28 -04:00
strawberry
4ea7af5780 ci(engage): use all-features devshell for cargo doc / rustdoc --all-features
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 00:10:28 -04:00
strawberry
79fb8091dc ci(engage): use all-features direnv devshell for clippy/all
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 00:10:28 -04:00
strawberry
f6fa2a4f65 use swap_remove instead of remove in a few places
`swap_remove` is faster if we don't care about the order (O(1))

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 00:10:28 -04:00
strawberry
b63937af0b ci(engage): add cargo test default, use all-features devshell for cargo test --all-features
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 00:10:28 -04:00
strawberry
3c4e325036 nix(bin): cache default devshell on top of all-features devshell
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 00:10:28 -04:00
strawberry
023fb41c49 load .env file before initializing flake to use DIRENV_DEVSHELL in .env
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 00:10:28 -04:00
strawberry
9a5f1dac57 drop unnecessarily verbose get_alias_helper logging
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 00:10:28 -04:00
strawberry
173ff26eb6 disable URL previews by default upon admin room creation
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 00:10:28 -04:00
strawberry
45e3fdba69 admin room: add get-latest-pdu and get-first-pdu commands
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-03 00:10:24 -04:00
strawberry
9f359e0550 make resolve_state public to resolve new forced compressed room state
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
strawberry
ffdf47d1ea add latest_pdu_in_room timeline function
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
strawberry
1af65e695d media: return application/octet-stream if no content-type was provided
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
strawberry
b1886583d9 csp: fix typo, add base-uri none
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
strawberry
f11103b43b media: check detected content-type against MSC2702
only return `inline` if the detected content-type is an allowed
inline content-type as defined by MSC2702

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
strawberry
9b096cc67b fix: check if you've left the room before forgetting it
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
strawberry
1ac72ab914 init a few state hashmaps using with_capacity
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
strawberry
f0533e07ef fed: remove unnecessary mutables, use with_capacity in couple more places
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
strawberry
68f42f5a2f fed: relax read receipt EDU check
so in theory: guest users, peaking over federation,
and world readable rooms should be allowed to send
read receipts even if they're not joined.

relaxing this check to only allow the read receipt if
the server has at least 1 member in the room makes
some of this still work

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
strawberry
884cbab135 ci: comment out hardened_malloc clippy check for now
i need to either fix static linking, or just make it
dynamic always (but then kinda useless idk)

https://gitlab.com/conduwuit/conduwuit/-/jobs/6953419673#L3155

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
strawberry
4aead5de7a reflax a couple restrictions on custom room IDs and aliases
🙃

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
strawberry
aef25ea1f7 enable tracing feature for axum
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 23:53:31 -04:00
Jason Volk
1a4736d40b support configuring multiple bind hosts; default to dual localhost.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-03 01:17:58 +00:00
strawberry
f09e0dc137 add conduwuit community code of conduct
this is a code of conduct that the moderation team made up,
specific to conduwuit's community spaces such as the matrix rooms.

the matrix foundation and contributor's covenant still apply, but
having conduwuit-specific additional guidelines help

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 21:02:38 -04:00
strawberry
de79b66cea misc docs changes
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-06-02 21:02:38 -04:00
strawberry
95ca9d00a2 nix: get all features from main crate 2024-06-02 23:01:12 +00:00
Jason Volk
887496d040 consolidate default cargo features
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 23:01:12 +00:00
Jason Volk
c2586737ae accept receipts prior to events
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 23:01:12 +00:00
Jason Volk
7d2f510cc3 single-source for defaulty log filter string
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 23:01:12 +00:00
Jason Volk
102bd1b4a6 use debug_warn for parse_incoming_pdu err results.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 23:01:12 +00:00
Jason Volk
89ab687f16 move signal handling out to main
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 23:01:12 +00:00
Jason Volk
1108235c63 misc simplifications and cleanup
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 20:17:45 +00:00
Jason Volk
90d9a997a5 split / cleanup core utils.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 20:17:45 +00:00
Jason Volk
5fe5ab279c split RouterExt impl related into ruma_wrapper unit.
slightly restrict client_server mod index.

Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 20:17:45 +00:00
Jason Volk
f1d1366129 split resolve_actual_dest
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 20:17:45 +00:00
Jason Volk
ba48758b89 impl fmt::Display for FedDest
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 20:17:45 +00:00
Jason Volk
9df5265c00 split sending resolver into unit.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 20:17:45 +00:00
Jason Volk
ee52d2f751 refactor lints into categories. lints are now more strict.
rust:
* ALL lints which rustc defaults to "allow" have been set to "warn".
* NEW "warn" lints which produce a warning as of this commit have been
explicitly identified and commented with a TODO for later review.

clippy:
* ALL categories (sans restriction) now fully enabled to "warn".
* redundant lints set to "warn" from categories now at "warn" are removed.
* previous "allow" sadness moved into respective categories.
* new warnings produced as of this commit have been explicitly identified:
	- nursery lints set to "allow" marked with TODO for later review.
	- pedantic lints set to "allow"

Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
53fe2362fc Fix path-buf-push-overwrite
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
38ab1083e3 Fix ptr-cast-constness
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
050841a871 Fix inefficient-to-string
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
4521e93d04 Fix stable-sort-primitive
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
0f3d43153b Fix unused-self
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
e5eccb3a0c Fix unreadable-literal
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
68cbf19154 Fix items-after-statements
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
2ab427fe99 Fix default-trait-access 2024-06-02 12:34:05 -04:00
Jason Volk
02081b66c4 Fix some unnecessary-unwraps w/ addl cleanup/simplification.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
b3fc8516ed Fix unnested-or-patterns
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
9e51525c25 Fix uninlined-format-args
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
14039d9df4 cleanup unused extern crates.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
eed8a2a801 add and sort incomplete package metadata
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 12:34:05 -04:00
Jason Volk
c3a0d28309 add tracing span; mute connection errors.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 01:20:28 -04:00
Jason Volk
6d1144bb69 move unix socket unlink from services to router
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 01:20:28 -04:00
Jason Volk
2e45cb281a split router::serve units.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 01:20:28 -04:00
Jason Volk
0baa57f5d9 add back unix socket listener.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 01:20:28 -04:00
Jason Volk
faa2b95c84 add unwrap_infallible tool
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 01:20:28 -04:00
Jason Volk
dd1d8fa760 upgrade to hyper-util 0.1.5
Signed-off-by: Jason Volk <jason@zemos.net>
2024-06-02 01:20:28 -04:00
Jayryn
f4cfc77a57 Check if database symlink already exists
test -L $object [object exists and is a symbolic link (same as -h)]

It is not recommended to use -h 
[True if file exists and is a symbolic link. This operator is retained for compatibility with previous versions of this program. Do not rely on its existence; use -L instead.]
2024-05-28 04:06:53 -04:00
Jason Volk
b8b93a2e86 Bump 0.4.1
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
29d69b7688 update complement test results
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
strawberry
bd07fb61e0 add hot_reload.md to SUMMARY.md
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
a41a60ef07 media: dont ignore requested filename on /download for Content-Disposition
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
Jason Volk
ec7a9ab726 add toolchain and build/check shortcut to smoketest
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
25f598ce6c enable http2 feature for reqwest.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
dbcb3be0ab fix duplicate output; increase wait in smoketest.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
a537462d51 replace num_cpus dependency with available_parallelism()
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
d2aef071bc add possibly referenced rocksdb symbol to export list.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
d68b11e8ff fix rustflags for release-max-perf
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
9cf5b0926e fix regressed jemalloc stats feature
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
ff0b57c89c remove unused jemalloc dep in main module.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
b94045a468 dissolve key_value/*
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
3122648767 split ruma_wrapper from_request() related.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
3f5349ad76 simplify RumaHandler for Router building.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
27dcf213f1 tweak error strings.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
a1b526b3b7 tweak log levels
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
dc614e11d6 check invite target is our server.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
c5569b4c6e dedup acl checks
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
Jason Volk
71a1285c7b hoist receipt ACL check
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-27 18:16:23 -04:00
strawberry
abdda6cf32 check invited user's server against ACLs on /invite
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
4d21f9d962 use ok_or_else instead of ok_or for function calls in server_server.rs
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
1013fe5a42 check for membership join state at /send_join
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
f31b7b9420 ignore inbound EDUs for users that dont belong to origin server
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
e5e358cc68 compare X-Matrix origin + body origin and check PDU/EDU length at /send txn
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
50bc7cc005 check state_key matches sender user at /send_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
445015e9ea check user ID server against ACLs at /send_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
7a38c12e5d check for member event type at /send_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
2a77951152 check for membership leave state at /send_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
0256c27363 check if we know about room at /make_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
826edc0a3a check state_key matches sender user at /send_join
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
a5043a38e1 only allow membership event types at /send_join
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
bfd471a863 check user ID server against ACLs for /send_join
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
3981e77ec6 check user ID server against ACLs for /make_join
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
81bf4b7150 check user ID server against ACLs for /make_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
b8ec763a7c ignore read receipts from ACL'd servers and users not joined
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
003d4edbfa debug log receiving typing EDUs for users not in room
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
4f0006d18a ignore typing EDUs from ACL'd user's servers
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
b822e3a94c listen on IPv6 localhost by default
this is dual-stack by default on linux, resolves
issues with nginx using `localhost` and randomly
choosing between 127.0.0.1 and [::1], causing
intermittent upstream issues

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
68fffe8e96 check room ACLs on sender user's server for incoming PDUs
`handle_incoming_pdu`

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
strawberry
7328ed7509 rename misleading sender_servername to origin
this is the X-Matrix origin/server, NOT the `"sender"``
user's server name.

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-27 18:16:23 -04:00
Benjamin Lee
6ccf578437 bump rocksdb input
Nedded to pull in [1], which is rared for dynamic rocksdb builds with
liburing.

[1]: c8a1450231
2024-05-27 04:54:57 -04:00
Benjamin Lee
8a1848a814 Revert "nix: default output to scopeHostStatic instead of scopeHost"
This reverts commit a37b2b9e64.

Dynamic builds are working again, so we'd prefer having that be the
default output for consistency with nixpkgs.
2024-05-27 04:54:57 -04:00
Benjamin Lee
b4cd8e9140 fix dynamic builds with liburing
The original implementation of this was really weird, so I restructed it
a lot while debugging, and am just gonna leave the restructured version.

Root cause of the segfault seems to be that upstream nixpkgs liburing
derivation is generating both static and dynamic libraries, causing
rocksdb to statically link liburing in a dynamic build, pulling in some
allocator stuff at the same time. I created a PR[1] to fix this upstream,
but it probably won't be available on nixos-unstable for quite a while,
so we can also patch it locally.

[1]: https://github.com/NixOS/nixpkgs/pull/314945
2024-05-27 04:54:57 -04:00
Benjamin Lee
a08f90b161 add a smoke-test to CI for the nix 'default' output
I talked to somebody yesterday in #conduwuit:puppygock.gay that was using
this output in their system config. The dynamically-linked jemalloc build
is quite fragile, and is not tested by anything else in CI. We want to
make sure we don't break it again in the future.
2024-05-27 04:54:57 -04:00
Benjamin Lee
207979579c fix dynamically linked devshell
This failed to inherit the fix from bec507d739
because the crane package's buildInputs become propagatedBuildInputs in
a static stdenv, but become normal buildInputs in a dynamic stdenv. Since
we were only pulling propagatedBuildInputs into the devshell, dynamically
linked devshells did not include the rust-jemalloc-sys package. This
causes tikv-jemalloc-sys to build it's own static jemalloc package, and
we end up loading libc before jemalloc at runtime.
2024-05-27 04:54:57 -04:00
Benjamin Lee
68b96026ec unmark dynamically-linked jemalloc builds as broken
It turns out that this was actually fixed by
bec507d739 and
857ac42aac, but we didn't identify it at the
time. Notably, the `dynamic` devshell is still broken.
2024-05-27 04:54:57 -04:00
strawberry
30beb20230 conditionally static link rust-rocksdb-uwu by hot reload cfg
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
19e7779693 update complement test results
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
6269822613 actually fix all let_underscore_must_use lints
CI caught some more

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
0877ee6191 allow let underscore use lint for rocksdb create cf for now
the workaround needs to be extended to rocksdb caches, but
i dont know that part of code

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
a37b2b9e64 nix: default output to scopeHostStatic instead of scopeHost
defaults to static builds instead of dynamically linked builds

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
29fe960efa bump hyper-util and libz-sys
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
6bf2e73830 ci: run cache dependencies in ci.yml as well
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
630760b5da bump rocksdb to v9.2.1
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
61e7f1e614 remove rpath = true from dev profile as the rustflags have it
needed for hot reloading but rpath being true by default
causes linker errors on lld because of the sad rpath bug

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
7ebed7aa3e clarify disable-room message after banning room
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
ad3eeaf4c1 delete audit.toml
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
5215fbe695 drop redaction calculated hash log to debug_info
this is normal redactions. no valid reason this needs to be
warn as it just causes confusion.

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
dc9fe657d5 fix guest accounts being logged still
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
1c7c5bc09c feat: add /_conduwuit/local_user_count endpoint
only enabled if federation is enabled

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
32161801ed use/enable let_underscore_must_use lint
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
71bdcb958a fix: dont drop remote federation error on 4xx responses
for a very long time, if a remote server responded to us with
a valid but unsuccessful (HTTP 4xx) response and the caller was the
`send_federation_request` function, we may find ourselves
with a warning message only containing the destination's
server name which was very unhelpful. the true error was
buried away in trace logs. this would primarily be noticed
with server key fetch requests from us.

conduit has been throwing away the ruma request error: https://gitlab.com/famedly/conduit/-/blame/next/src/utils/error.rs#L62

before: 2024-05-23T04:45:02.930224Z  WARN router:{path=/_matrix/client/v3/publicRooms}:handle: conduit_api::client_server::directory: Failed to return our /publicRooms: matrix.org
after: 2024-05-23T05:05:02.435272Z  WARN router:{path=/_matrix/client/v3/publicRooms}:handle: conduit_api::client_server::directory: Failed to return our /publicRooms: matrix.org: [401 / M_UNAUTHORIZED] Failed to find any key to satisfy: _FetchKeyRequest(server_name='your.server.name', minimum_valid_until_ts=1716440702337, key_ids=['ed25519:RQB3XPQX'])

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
d3db0ad4e2 renovate: label PRs as dependencies and github_actions
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
e098448b9d init a few vecs in event_handler using with_capacity
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
d49507bc21 media: decomplexify get_all_media_keys for deleting all MXC URIs
wow this was terrible, early strawberry code

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
cb73ae3732 add registration token validity endpoint as per matrix 1.2
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
06bec40591 fix: add missing fetch_required_signing_keys for remote send_leave
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
9a7ba94ccf explicity define unstable support for sliding sync
this matrix-react-sdk PR (and the cited sliding sync MSC)
says that they will intend on checking sliding sync support
from this unstable feature flag at /versions until the CORS
header stuff is specced

https://github.com/matrix-org/matrix-react-sdk/pull/12498

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
2990c30ac9 nix: bump rocksdb input
• Updated input 'rocksdb':
    'github:girlbossceo/rocksdb/db6df0b185774778457dabfcbd822cb81760cade' (2024-05-03)
  → 'github:girlbossceo/rocksdb/be68b3c95ccd225f3121ba33a67cfaf3c3596afc' (2024-05-23)

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
d9c575d96f bump deps
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
c32406aa0e replace deprecated config option for complement
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
03d12cb44e update docs a tad
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
bef7dbd1cb finally error on complement diff mismatch, remove jemalloc builds from
CI

jemalloc is now a default feature

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
08577873b4 update complement test results
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
a3931b0f1f nix: bump flake.lock
• Updated input 'crane':
    'github:ipetkov/crane/27025ab71bdca30e7ed0a16c88fd74c5970fc7f5' (2024-05-09)
  → 'github:ipetkov/crane/7443df1c478947bf96a2e699209f53b2db26209d' (2024-05-19)
• Updated input 'fenix':
    'github:nix-community/fenix/297c756ba6249d483c1dafe42378560458842173' (2024-05-10)
  → 'github:nix-community/fenix/063d7e5fac454edd35b7e2cedb6ca9fb1410c79b' (2024-05-21)
• Updated input 'fenix/rust-analyzer-src':
    'github:rust-lang/rust-analyzer/5bf2f85c8054d80424899fa581db1b192230efb5' (2024-05-09)
  → 'github:rust-lang/rust-analyzer/21ec8f523812b88418b2bfc64240c62b3dd967bd' (2024-05-19)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/f1010e0469db743d14519a1efd37e23f8513d714' (2024-05-09)
  → 'github:NixOS/nixpkgs/3eaeaeb6b1e08a016380c279f8846e0bd8808916' (2024-05-21)

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
ba2f22b5d3 nix: remove jemalloc (now default) targets, add jq input for default
jq input change was from 17eb354590
to prevent unnecessary bindgen rebuilds

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
0914aaa1b6 skip a few known flaky/unreliable complement tests
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-25 22:28:43 -04:00
strawberry
f3427afc7f nix: use new public keys for binary caches due to attic issues
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-24 18:13:02 -04:00
morguldir
9aa372d83b nix: Allow excluding features, allow disabling release_max_log_level 2024-05-24 15:12:23 -04:00
morguldir
5893901a75 Explicitly include snappy as well
Not sure what changed that we need this

Signed-off-by: morguldir <morguldir@protonmail.com>
2024-05-24 12:46:15 -04:00
morguldir
8ba9b33a95 Make sure we use the liburing of the platform we're building for
Signed-off-by: morguldir <morguldir@protonmail.com>
2024-05-24 12:46:15 -04:00
morguldir
70047ff26d Make rocksdb include liburing, and tell gcc the path during the build
With: strings /nix/store/9skicdac6xs4yww1nd3h7m6xydv4hxlj-rocksdb-9.1.1/lib/librocksdb.so.9|rg io_uring|wc -l
112
With: strings result/bin/conduit |rg io_uring|wc -l
5

Without: strings static-x86_64-unknown-linux-musl-jemalloc |rg io_uring | wc -l
0

Signed-off-by: morguldir <morguldir@protonmail.com>
2024-05-24 12:46:15 -04:00
Benjamin Lee
1d57e14dc0 set C/LDFLAGS for complement dependencies directly
Previously we were relying on NIX_CFLAGS_COMPILE, but this is not being
set in static devshells. A cleaner solution for complement would likely
be to build the tests in their own nix derivation instead of building
them in the devshell, but this change unblocks CI for now.
2024-05-24 10:53:47 -04:00
Benjamin Lee
5d81203277 use a statically-linked binary for complement
Dynamically-linked jemalloc is broken.
2024-05-24 10:53:47 -04:00
Benjamin Lee
ad39a34c16 add a dynamically-linked devshell
This is broken on linux, but can be used by darwin users for development,
since static/jemalloc/darwin is broken.
2024-05-24 10:53:47 -04:00
Benjamin Lee
a007338b34 mark dynamic jemalloc builds as broken on linux 2024-05-24 10:53:47 -04:00
Benjamin Lee
3d1507e6dd mark static rocksdb broken on darwin 2024-05-24 10:53:47 -04:00
Benjamin Lee
4cb7c0b982 don't use prefixed jemalloc with rocksdb
This is causing build failures on Mac:

> In file included from /tmp/nix-build-rocksdb-static-aarch64-apple-darwin-9.1.1.drv-0/source/memory/memory_allocator.cc:8:
> In file included from /tmp/nix-build-rocksdb-static-aarch64-apple-darwin-9.1.1.drv-0/source/memory/jemalloc_nodump_allocator.h:11:
> /tmp/nix-build-rocksdb-static-aarch64-apple-darwin-9.1.1.drv-0/source/port/jemalloc_helper.h:63:36: warning: unknown attribute '_rjem_malloc' ignored [-Wunknown-attributes]
> mallocx(size_t, int) JEMALLOC_ATTR(malloc) JEMALLOC_ALLOC_SIZE(1)
>                                    ^~~~~~
> /nix/store/3bix0kzy670dyhhizri3dwb1qfj3sdpa-jemalloc-static-aarch64-apple-darwin-5.3.0/include/jemalloc/jemalloc.h:412:18: note: expanded from macro 'malloc'
> #  define malloc je_malloc
>                  ^~~~~~~~~
> /nix/store/3bix0kzy670dyhhizri3dwb1qfj3sdpa-jemalloc-static-aarch64-apple-darwin-5.3.0/include/jemalloc/jemalloc.h:75:21: note: expanded from macro 'je_malloc'
> #  define je_malloc _rjem_malloc
>                     ^~~~~~~~~~~~
> /nix/store/3bix0kzy670dyhhizri3dwb1qfj3sdpa-jemalloc-static-aarch64-apple-darwin-5.3.0/include/jemalloc/jemalloc.h:183:43: note: expanded from macro 'JEMALLOC_ATTR'
> #  define JEMALLOC_ATTR(s) __attribute__((s))

Full build log at <https://girlboss.ceo/~strawberry/pb/ygJ3>. This is
likely fixable with patches to rocksdb, but not worth it since darwin is
only a dev platform.
2024-05-24 10:53:47 -04:00
Benjamin Lee
0c34cf95ce set show-trace for nix in CI 2024-05-24 10:53:47 -04:00
Benjamin Lee
17cc02ff99 add a 'no-features' devshell for local testing 2024-05-24 10:53:47 -04:00
Benjamin Lee
c0f8253fc5 enable all-features in nix for CI builds
CI is running `cargo build --all-features`, so we should be passing all
the features to nix as well.

The only thing this currently affects is the jemalloc_prof feature, but if
we add any non-default features that affect nix in the future they should
also be handled correctly now.
2024-05-24 10:53:47 -04:00
Benjamin Lee
0fd0a5d73c switch default devshell to static linking
Dynamically-linked jemalloc doesn't work due to link-order issues, and we
want CI to be testing a static binary anyway since that's what we're
publishing in releases.
2024-05-24 10:53:47 -04:00
Benjamin Lee
4e6fc2f2df factor devshell out into a helper function
We're planning to add a second devshell with `all-features` for CI.
2024-05-24 10:53:47 -04:00
Benjamin Lee
a6742ce8a7 remove liburing from devshell
This doesn't seem to be necessary to build, and the derivation is broken
in pkgsStatic.
2024-05-24 10:53:47 -04:00
Benjamin Lee
188dea13e0 do default-feature unification in nix
Some of the features affect nix dependencies, so we need to have a
full feature list available when constructing the nix derivation. This
incidentally fixes the bug where we weren't enabling jemalloc on rocksdb
in CI/devshells, because jemalloc is now a default feature. It does not
fix the more general class of that issue, where CI is performing an
`--all-features` build in a nix devshell built for default-features.

I am now passing `--no-default-features` to cargo, and having it use our
unified feature list rather than duplicating the unification inside cargo.
2024-05-24 10:53:47 -04:00
Benjamin Lee
a7fe434086 only link to one jemalloc build
Without setting JEMALLOC_OVERRIDE, we end up linking to two different
jemalloc builds. Once dynamically, as a transitive dependency through
rocksdb, and a second time to the static jemalloc that tikv-jemalloc-sys
builds.
2024-05-24 10:53:47 -04:00
renovate[bot]
eb8dd9cb44 chore(deps): update aquasecurity/trivy-action action to v0.21.0 2024-05-23 01:30:36 -04:00
strawberry
474d50d10c bump conduwuit version to 0.4.0
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-21 20:22:17 -04:00
Jason Volk
2e732c711c docs: Update docs for hot-reloading.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
strawberry
981ec51ec0 docs: add initial docs for hot reload
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-21 20:22:17 -04:00
Jason Volk
2dd5cf8c68 move clap; fix version
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
Jason Volk
74832bdc47 fix smoke from builds produced by --all-features
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
Jason Volk
fdc9a9a1b8 add cargo smoketest
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
Jason Volk
1f3a9a40e5 lint clippy::collapsible_match (nightly)
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
Jason Volk
362649ff87 rename src/bin to src/main
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
strawberry
4aeec78ab4 debian: remove old symlink on postrm
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-21 20:22:17 -04:00
strawberry
9bfa89a555 adjust debian metadata, set crane workspace name
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-05-21 20:22:17 -04:00
Jason Volk
6c1434c165 Hot-Reloading Refactor
Signed-off-by: Jason Volk <jason@zemos.net>
2024-05-21 20:22:17 -04:00
slonkazoid
ae1a4fd283 add modification time fallback if birth time is not supported on this platform 2024-05-21 16:58:30 -04:00
Benjamin Lee
9eb0784f6f don't return extra member count or e2ee device updates from sync
Previously, we were returning redundant member count updates or encrypted
device updates from the /sync endpoint in some cases. The extra member
count updates are spec-compliant, but unnecessary, while the extra
encrypted device updates violate the spec.

The refactor necessary to fix this bug is also necessary to support
filtering on state events in sync.

Details:

Joined room incremental sync needs to examine state events for four
purposes:

 1. determining whether we need to return an update to room member counts
 2. determining the set of left/joined devices for encrypted rooms
    (returned in `device_lists`)
 3. returning state events to the client (in `rooms.joined.*.state`)
 4. tracking which member events we have sent to the client, so they can
    be omitted on future requests when lazy-loading is enabled.

The state events that we need to examine for the first two cases is member
events in the delta between `since` and the end of `timeline`. For the
second two cases, we need the delta between `since` and the start of
`timeline`, plus contextual member events for any senders that occur in
`timeline`. The second list is subject to filtering, while the first is
not.

Before this change, we were using the same set of state events that we are
returning to the client (cases 3/4) to do the analysis for cases 1/2.
In a compliant implementation, this would result in us missing some
relevant member events in 1/2 in addition to seeing redundant member
events. In current conduwuit this is not the case because the set of
events that we return to the client is always a superset of the set that
is needed for cases 1/2. This is because we don't support filtering, and
we have an existing bug[1] where we are returning the delta between
`since` and the end of `timeline` rather than the start.

[1]: https://github.com/girlbossceo/conduwuit/issues/361

Fixing this is necessary to implement filtering because otherwise
we would start missing some member events for member count or encrypted
device updates if the relevant member events are rejected by the filter.
This would be much worse than our current behavior.
2024-05-20 20:55:56 -04:00
Benjamin Lee
8bffcfe82b remove sync response cache
This cache can serve invalid responses, and has an extremely low hit
rate.

It serves invalid responses because because it's only keyed off
the `since` parameter, but many of the other request parameters also
affect the response or it's side effects. This will become worse once we
implement filtering, because there will be a wider space of parameters
with different responses. This problem is fixable, but not worth it
because of the low hit rate.

The low hit rate is because normal clients will always issue the next
sync request with `since` set to the `prev_batch` value of the previous
response. The only time we expect to see multiple requests with the same
`since` is when the response is empty, but we don't cache empty
responses.

This was confirmed experimentally by logging cache hits and misses over
15 minutes with a wide variety of clients. This test was run on
matrix.computer.surgery, which has only a few active users, but a
large volume of sync traffic from many rooms. Over the test period, we
had 3 hits and 5309 misses. All hits occurred in the first minute, so I
suspect that they had something to do with client recovery from an
offline state. The clients that were connected during the test are:

 - element web
 - schildichat web
 - iamb
 - gomuks
 - nheko
 - fractal
 - fluffychat web
 - fluffychat android
 - cinny web
 - element android
 - element X android

Fixes: #336
2024-05-17 18:13:11 -04:00
309 changed files with 20568 additions and 16753 deletions

6
.envrc
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
use flake
dotenv_if_exists
use flake ".#${DIRENV_DEVSHELL:-default}"
PATH_add bin
dotenv_if_exists

View File

@@ -35,6 +35,11 @@ env:
# Custom nix binary cache if fork is being used
ATTIC_ENDPOINT: ${{ vars.ATTIC_ENDPOINT }}
ATTIC_PUBLIC_KEY: ${{ vars.ATTIC_PUBLIC_KEY }}
# Use the all-features devshell instead of default, to ensure that features
# match between nix and cargo
DIRENV_DEVSHELL: all-features
# Get error output from nix that we can actually use
NIX_CONFIG: show-trace = true
permissions:
packages: write
@@ -76,7 +81,7 @@ jobs:
run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit https://cache.lix.systems
extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg= conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o=
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o=
EOF
- name: Use alternative Nix binary caches if specified
@@ -92,7 +97,11 @@ jobs:
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc"
nix profile install --impure --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv
direnv allow
nix develop --command true
nix develop .#all-features --command true
- name: Cache CI dependencies
run: |
bin/nix-build-and-cache ci
- name: Run CI tests
run: |
@@ -131,8 +140,6 @@ jobs:
if-no-files-found: error
- name: Diff Complement results with checked-in repo results
# TODO: figure out why our complement results are not 100% consistent so we don't need to allow failures
continue-on-error: true
run: |
diff -u --color=always tests/test_results/complement/test_results.jsonl complement_test_results.jsonl > >(tee -a complement_test_output.log)
@@ -163,9 +170,7 @@ jobs:
matrix:
include:
- target: aarch64-unknown-linux-musl
- target: aarch64-unknown-linux-musl-jemalloc
- target: x86_64-unknown-linux-musl
- target: x86_64-unknown-linux-musl-jemalloc
steps:
- name: Sync repository
uses: actions/checkout@v4
@@ -185,8 +190,8 @@ jobs:
- name: Apply Nix binary cache configuration
run: |
sudo tee -a /etc/nix/nix.conf > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit
extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg= conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit https://cache.lix.systems
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o=
EOF
- name: Use alternative Nix binary caches if specified
@@ -202,7 +207,7 @@ jobs:
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc"
nix profile install --impure --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv
direnv allow
nix develop --command true
nix develop .#all-features --command true
- name: Build static ${{ matrix.target }}
run: |
@@ -213,7 +218,8 @@ jobs:
mkdir -v -p target/$CARGO_DEB_TARGET_TUPLE/release/
cp -v -f result/bin/conduit target/release/conduwuit
cp -v -f result/bin/conduit target/$CARGO_DEB_TARGET_TUPLE/release/conduwuit
direnv exec . cargo deb --verbose --no-build --no-strip --target=$CARGO_DEB_TARGET_TUPLE --output target/release/${{ matrix.target }}.deb
# -p conduit is the main crate name
direnv exec . cargo deb --verbose --no-build --no-strip -p conduit --target=$CARGO_DEB_TARGET_TUPLE --output target/release/${{ matrix.target }}.deb
mv -v target/release/conduwuit static-${{ matrix.target }}
mv -v target/release/${{ matrix.target }}.deb ${{ matrix.target }}.deb
@@ -295,8 +301,8 @@ jobs:
- name: Move OCI images into position
run: |
mv -v oci-image-x86_64-*-jemalloc/*.tar.gz oci-image-amd64.tar.gz
mv -v oci-image-aarch64-*-jemalloc/*.tar.gz oci-image-arm64v8.tar.gz
mv -v oci-image-x86_64-*/*.tar.gz oci-image-amd64.tar.gz
mv -v oci-image-aarch64-*/*.tar.gz oci-image-arm64v8.tar.gz
- name: Load and push amd64 image
if: ${{ (vars.DOCKER_USERNAME != '') && (env.DOCKERHUB_TOKEN != '') }}

View File

@@ -61,9 +61,9 @@ jobs:
extra-substituters = https://crane.cachix.org
extra-trusted-public-keys = crane.cachix.org-1:8Scfpmn9w+hGdXH/Q9tTLiYAE/2dnJYRJP7kl80GuRk=
extra-substituters = https://attic.kennel.juneis.dog/conduit
extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg=
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk=
extra-substituters = https://attic.kennel.juneis.dog/conduwuit
extra-trusted-public-keys = conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=
extra-trusted-public-keys = conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE=
- name: Add alternative Nix binary caches if specified
if: ${{ (env.ATTIC_ENDPOINT != '') && (env.ATTIC_PUBLIC_KEY != '') }}

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v4
- name: Run Trivy code and vulnerability scanner on repo
uses: aquasecurity/trivy-action@0.20.0
uses: aquasecurity/trivy-action@0.22.0
with:
scan-type: repo
format: sarif
@@ -34,7 +34,7 @@ jobs:
severity: CRITICAL,HIGH,MEDIUM,LOW
- name: Run Trivy code and vulnerability scanner on filesystem
uses: aquasecurity/trivy-action@0.20.0
uses: aquasecurity/trivy-action@0.22.0
with:
scan-type: fs
format: sarif

View File

@@ -26,10 +26,10 @@ before_script:
# Add conduwuit binary cache
- if command -v nix > /dev/null; then echo "extra-substituters = https://attic.kennel.juneis.dog/conduwuit" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE=" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-substituters = https://attic.kennel.juneis.dog/conduit" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg=" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk=" >> /etc/nix/nix.conf; fi
# Add alternate binary cache
- if command -v nix > /dev/null && [ -n "$ATTIC_ENDPOINT" ]; then echo "extra-substituters = $ATTIC_ENDPOINT" >> /etc/nix/nix.conf; fi

1108
Cargo.lock generated

File diff suppressed because it is too large Load Diff

1076
Cargo.toml

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
[advisories]
ignore = ["RUSTSEC-2020-0016"]

View File

@@ -17,11 +17,17 @@ RESULTS_FILE="$3"
OCI_IMAGE="complement-conduit:main"
# Complement tests that are skipped due to flakiness/reliability issues (likely
# Complement itself induced based on various open issues)
#
# According to Go docs, these are separated by forward slashes and not pipes (why)
SKIPPED_COMPLEMENT_TESTS='-skip=TestJumpToDateEndpoint.*|TestJoinFederatedRoomFromApplicationServiceBridgeUser.*|TestFederationRoomsInvite.*|TestClientSpacesSummary.*'
toplevel="$(git rev-parse --show-toplevel)"
pushd "$toplevel" > /dev/null
bin/nix-build-and-cache just .#complement
bin/nix-build-and-cache just .#static-complement
docker load < result
popd > /dev/null
@@ -31,7 +37,7 @@ set +o pipefail
env \
-C "$COMPLEMENT_SRC" \
COMPLEMENT_BASE_IMAGE="$OCI_IMAGE" \
go test -tags="conduwuit_blacklist" -v -timeout 1h -json ./tests | tee "$LOG_FILE"
go test -tags="conduwuit_blacklist" "$SKIPPED_COMPLEMENT_TESTS" -v -timeout 1h -json ./tests | tee "$LOG_FILE"
set -o pipefail
# Post-process the results into an easy-to-compare format, sorted by Test name for reproducible results

View File

@@ -71,6 +71,7 @@ ci() {
# Keep sorted
"$toplevel#devShells.x86_64-linux.default"
"$toplevel#devShells.x86_64-linux.all-features"
attic#default
nixpkgs#direnv
nixpkgs#jq

View File

@@ -1 +1,7 @@
too-many-lines-threshold = 700
array-size-threshold = 4096
cognitive-complexity-threshold = 94 # TODO reduce me ALARA
excessive-nesting-threshold = 11 # TODO reduce me to 4 or 5
future-size-threshold = 7745 # TODO reduce me ALARA
stack-size-threshold = 144000 # reduce me ALARA
too-many-lines-threshold = 700 # TODO reduce me to <= 100
type-complexity-threshold = 250 # reduce me to ~200

View File

@@ -77,11 +77,16 @@ database_backend = "rocksdb"
# forwarded to the conduwuit instance running on this port
# Docker users: Don't change this, you'll need to map an external port to this.
# To listen on multiple ports, specify a vector e.g. [8080, 8448]
#
# default if unspecified is 8008
port = 6167
# default address (IPv4 or IPv6) conduwuit will listen on. Generally you want this to be
# localhost (127.0.0.1 / ::1). If you are using Docker or a container NAT networking setup, you
# likely need this to be 0.0.0.0.
# To listen multiple addresses, specify a vector e.g. ["127.0.0.1", "::1"]
#
# default if unspecified is both IPv4 and IPv6 localhost: ["127.0.0.1", "::1"]
address = "127.0.0.1"
# Max request size for file uploads
@@ -375,15 +380,6 @@ allow_profile_lookup_federation_requests = true
# Defaults to 256.0
#db_cache_capacity_mb = 256.0
# Interval in seconds when conduwuit will run database cleanup operations.
#
# For SQLite: this will flush the WAL by executing `PRAGMA wal_checkpoint(RESTART)` (https://www.sqlite.org/pragma.html#pragma_wal_checkpoint)
# For RocksDB: this will run `flush_opt` to flush database memtables to SST files on disk (https://docs.rs/rocksdb/latest/rocksdb/struct.DBCommon.html#method.flush_opt)
# These operations always run on shutdown.
#
# Defaults to 30 minutes (1800 seconds) to avoid IO amplification from too frequent cleanups
#cleanup_second_interval = 1800
### RocksDB options
@@ -492,11 +488,6 @@ allow_profile_lookup_federation_requests = true
# Defaults to 1 (TolerateCorruptedTailRecords)
#rocksdb_recovery_mode = 1
# Controls whether memory buffers are written to storage at the fixed interval set by `cleanup_period_interval`
# even when they are not full. Setting this will increase load on the storage backplane and is never advised
# under normal circumstances.
#rocksdb_periodic_cleanup = false
### Domain Name Resolution and Caching
@@ -711,6 +702,44 @@ allow_profile_lookup_federation_requests = true
#typing_client_timeout_max_s = 45
### TURN / VoIP
# vector list of TURN URIs/servers to use
#
# No default
#turn_uris = ["turn:example.turn.uri?transport=udp", "turn:example.turn.uri?transport=tcp"]
# TURN secret to use for generating the HMAC-SHA1 hash apart of username and password generation
#
# this is more secure, but if needed you can use traditional username/password below.
#
# no default
#turn_secret = ""
# TURN username to provide the client
#
# no default
#turn_username = ""
# TURN password to provide the client
#
# no default
#turn_password = ""
# TURN TTL
#
# Default is 86400 seconds
#turn_ttl = 86400
# 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 `/_matrix/client/v3/voip/turnServer`.
#
# defaults to false
#turn_allow_guests = false
# Other options not in [global]:
#
#

6
debian/postinst vendored
View File

@@ -25,8 +25,10 @@ case "$1" in
# and permissions for the config.
mkdir -v -p "$CONDUWUIT_DATABASE_PATH"
# symlink the previous location for compatibility
ln -s -v "$CONDUWUIT_DATABASE_PATH" "/var/lib/matrix-conduit"
# symlink the previous location for compatibility if it does not exist yet.
if ! test -L "/var/lib/matrix-conduit" ; then
ln -s -v "$CONDUWUIT_DATABASE_PATH" "/var/lib/matrix-conduit"
fi
chown -v conduwuit:conduwuit -R "$CONDUWUIT_DATABASE_PATH"
chown -v conduwuit:conduwuit -R "$CONDUWUIT_CONFIG_PATH"

5
debian/postrm vendored
View File

@@ -5,6 +5,7 @@ set -e
CONDUWUIT_CONFIG_PATH=/etc/conduwuit
CONDUWUIT_DATABASE_PATH=/var/lib/conduwuit
CONDUWUIT_DATABASE_PATH_SYMLINK=/var/lib/matrix-conduit
case $1 in
purge)
@@ -21,6 +22,10 @@ case $1 in
if [ -d "$CONDUWUIT_DATABASE_PATH" ]; then
rm -v -r "$CONDUWUIT_DATABASE_PATH"
fi
if [ -d "$CONDUWUIT_DATABASE_PATH_SYMLINK" ]; then
rm -v -r "$CONDUWUIT_DATABASE_PATH_SYMLINK"
fi
;;
esac

42
deps/rust-rocksdb/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,42 @@
[package]
name = "rust-rocksdb-uwu"
categories.workspace = true
description = "dylib wrapper for rust-rocksdb"
edition = "2021"
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
version = "0.0.1"
[features]
default = ["snappy", "lz4", "zstd", "zlib", "bzip2"]
jemalloc = ["rust-rocksdb/jemalloc"]
io-uring = ["rust-rocksdb/io-uring"]
valgrind = ["rust-rocksdb/valgrind"]
snappy = ["rust-rocksdb/snappy"]
lz4 = ["rust-rocksdb/lz4"]
zstd = ["rust-rocksdb/zstd"]
zlib = ["rust-rocksdb/zlib"]
bzip2 = ["rust-rocksdb/bzip2"]
rtti = ["rust-rocksdb/rtti"]
mt_static = ["rust-rocksdb/mt_static"]
multi-threaded-cf = ["rust-rocksdb/multi-threaded-cf"]
serde1 = ["rust-rocksdb/serde1"]
malloc-usable-size = ["rust-rocksdb/malloc-usable-size"]
[dependencies.rust-rocksdb]
git = "https://github.com/zaidoon1/rust-rocksdb"
rev = "e9e1cb5ba92a44ea225fe8d13b31aa23621b9035"
#branch = "master"
default-features = false
[lib]
path = "lib.rs"
crate-type = [
"rlib",
# "dylib"
]
[lints]
workspace = true

61
deps/rust-rocksdb/lib.rs vendored Normal file
View File

@@ -0,0 +1,61 @@
pub use rust_rocksdb::*;
#[cfg_attr(not(conduit_mods), link(name = "rocksdb"))]
#[cfg_attr(conduit_mods, link(name = "rocksdb", kind = "static"))]
extern "C" {
pub fn rocksdb_list_column_families();
pub fn rocksdb_logger_create_stderr_logger();
pub fn rocksdb_options_set_info_log();
pub fn rocksdb_get_options_from_string();
pub fn rocksdb_writebatch_create();
pub fn rocksdb_writebatch_destroy();
pub fn rocksdb_writebatch_put_cf();
pub fn rocksdb_writebatch_delete_cf();
pub fn rocksdb_iter_value();
pub fn rocksdb_iter_seek_to_last();
pub fn rocksdb_iter_seek_for_prev();
pub fn rocksdb_iter_seek_to_first();
pub fn rocksdb_iter_next();
pub fn rocksdb_iter_prev();
pub fn rocksdb_iter_seek();
pub fn rocksdb_iter_valid();
pub fn rocksdb_iter_get_error();
pub fn rocksdb_iter_key();
pub fn rocksdb_iter_destroy();
pub fn rocksdb_livefiles();
pub fn rocksdb_livefiles_count();
pub fn rocksdb_livefiles_destroy();
pub fn rocksdb_livefiles_column_family_name();
pub fn rocksdb_livefiles_name();
pub fn rocksdb_livefiles_size();
pub fn rocksdb_livefiles_level();
pub fn rocksdb_livefiles_smallestkey();
pub fn rocksdb_livefiles_largestkey();
pub fn rocksdb_livefiles_entries();
pub fn rocksdb_livefiles_deletions();
pub fn rocksdb_put_cf();
pub fn rocksdb_delete_cf();
pub fn rocksdb_get_pinned_cf();
pub fn rocksdb_create_column_family();
pub fn rocksdb_get_latest_sequence_number();
pub fn rocksdb_batched_multi_get_cf();
pub fn rocksdb_cancel_all_background_work();
pub fn rocksdb_repair_db();
pub fn rocksdb_list_column_families_destroy();
pub fn rocksdb_flush();
pub fn rocksdb_flush_wal();
pub fn rocksdb_open_column_families();
pub fn rocksdb_open_for_read_only_column_families();
pub fn rocksdb_open_as_secondary_column_families();
pub fn rocksdb_open_column_families_with_ttl();
pub fn rocksdb_open();
pub fn rocksdb_open_for_read_only();
pub fn rocksdb_open_with_ttl();
pub fn rocksdb_open_as_secondary();
pub fn rocksdb_write();
pub fn rocksdb_create_iterator_cf();
pub fn rocksdb_backup_engine_create_new_backup_flush();
pub fn rocksdb_backup_engine_options_create();
pub fn rocksdb_write_buffer_manager_destroy();
pub fn rocksdb_options_set_ttl();
}

View File

@@ -16,3 +16,5 @@ # Summary
- [Development](development.md)
- [Contributing](contributing.md)
- [Testing](development/testing.md)
- [Hot Reloading ("Live" Development)](development/hot_reload.md)
- [conduwuit Community Code of Conduct](conduwuit_coc.md)

View File

@@ -12,9 +12,9 @@ ## Set up the appservice - general instructions
At some point the appservice guide should ask you to add a registration yaml
file to the homeserver. In Synapse you would do this by adding the path to the
homeserver.yaml, but in Conduit you can do this from within Matrix:
homeserver.yaml, but in conduwuit you can do this from within Matrix:
First, go into the #admins room of your homeserver. The first person that
First, go into the `#admins` room of your homeserver. The first person that
registered on the homeserver automatically joins it. Then send a message into
the room like this:
@@ -31,13 +31,13 @@ ## Set up the appservice - general instructions
```
You can confirm it worked by sending a message like this:
`@conduit:your.server.name: appservices list`
`!admin appservices list`
The `@conduit` bot should answer with `Appservices (1): your-bridge`
Then you are done. Conduit will send messages to the appservices and the
Then you are done. conduwuit will send messages to the appservices and the
appservice can send requests to the homeserver. You don't need to restart
Conduit, but if it doesn't work, restarting while the appservice is running
conduwuit, but if it doesn't work, restarting while the appservice is running
could help.
## Appservice-specific instructions
@@ -46,16 +46,6 @@ ### Remove an appservice
To remove an appservice go to your admin room and execute
`@conduit:your.server.name: appservices unregister <name>`
`!admin appservices unregister <name>`
where `<name>` one of the output of `appservices list`.
### Tested appservices
These appservices have been tested and work with Conduit without any extra steps:
- [matrix-appservice-discord](https://github.com/Half-Shot/matrix-appservice-discord)
- [mautrix-hangouts](https://github.com/mautrix/hangouts/)
- [mautrix-telegram](https://github.com/mautrix/telegram/)
- [mautrix-signal](https://github.com/mautrix/signal/) from version `0.2.2` forward.
- [heisenbridge](https://github.com/hifi/heisenbridge/)

77
docs/conduwuit_coc.md Normal file
View File

@@ -0,0 +1,77 @@
# Conduwuit Community Code of Conduct
Welcome to the conduwuit community! Were excited to have you here. Conduwuit is a hard-fork of the Conduit homeserver,
aimed at making Matrix more accessible and inclusive for everyone.
This space is dedicated to fostering a positive, supportive, and inclusive environment for everyone. This Code of
Conduct applies to all conduwuit spaces, including any further community rooms that reference this CoC. Here are our
guidelines to help maintain the welcoming atmosphere that sets conduwuit apart.
For the foundational rules, please refer to the [Matrix.org Code of Conduct](https://matrix.org/legal/code-of-conduct/)
and the [Contributor's Covenant](https://github.com/girlbossceo/conduwuit/blob/main/CODE_OF_CONDUCT.md). Below are
additional guidelines specific to the conduwuit community.
## Our Values and Guidelines
1. **Respect and Inclusivity**: We are committed to maintaining a community where everyone feels safe and respected.
Discrimination, harassment, or hate speech of any kind will not be tolerated. Recognise that each community member
experiences the world differently based on their past experiences, background, and identity. Share your own
experiences and be open to learning about others' diverse perspectives.
2. **Positivity and Constructiveness**: Engage in constructive discussions and support each other. If you feel angry,
negative, or aggressive, take a break until you can participate in a positive and constructive manner. Process
intense feelings with a friend or in a private setting before engaging in community conversations to help maintain
a supportive and focused environment.
3. **Clarity and Understanding**: Our community includes neurodivergent individuals and those who may not appreciate
sarcasm or subtlety. Communicate clearly and kindly, avoiding sarcasm and ensuring your messages are easily
understood by all. Additionally, avoid putting the burden of education on marginalized groups by doing your own
research before asking for explanations.
4. **Be Open to Inclusivity**: Actively engage in conversations about making our community more inclusive. Report
discriminatory behavior to the moderators and be open to constructive feedback that aims to improve our community.
Understand that discussing discrimination and negative experiences can be emotionally taxing, so focus on the
message rather than critiquing the tone used.
5. **Commit to Inclusivity**: Building an inclusive community requires time, energy, and resources. Recognise that
addressing discrimination and bias is an ongoing process that necessitates commitment and action from all community
members.
## Matrix Community
This Code of Conduct applies to the entire [Conduwuit Matrix Space](https://matrix.to/#/#conduwuit-space:puppygock.gay)
and its rooms, including:
### [#conduwuit:puppygock.gay](https://matrix.to/#/#conduwuit:puppygock.gay)
This room is for support and discussions about conduwuit. Ask questions, share insights, and help each other out.
### [#conduwuit-offtopic:girlboss.ceo](https://matrix.to/#/#conduwuit-offtopic:girlboss.ceo)
For off-topic community conversations about any subject. While this room allows for a wide range of topics, the same
CoC applies. Keep discussions respectful and inclusive, and avoid divisive subjects like country/world politics.
General topics, such as world events, are welcome as long as they follow the CoC.
### [#conduwuit-dev:puppygock.gay](https://matrix.to/#/#conduwuit-dev:puppygock.gay)
This room is dedicated to discussing active development of conduwuit. Posting requires an elevated power level, which
can be requested in one of the other rooms. Use this space to collaborate and innovate.
## Enforcement
We have a zero-tolerance policy for violations of this Code of Conduct. If someones behavior makes you uncomfortable,
please report it to the moderators. Actions we may take include:
1. **Warning**: A warning given directly in the room or via a private message from the moderators, identifying
the violation and requesting corrective action.
2. **Temporary Mute**: Temporary restriction from participating in discussions for a specified period to allow for
reflection and cooling off.
3. **Kick or Ban**: Egregious behavior may result in an immediate kick or ban to protect other community members.
Bans are considered permanent and will only be reversed in exceptional circumstances after proven good behavior.
Please highlight issues directly in rooms when possible, but if you don't feel comfortable doing that, then please send
a DM to one of the moderators directly.
Together, lets build a community where everyone feels valued and respected.
- The Conduwuit Moderation Team

View File

@@ -49,7 +49,7 @@ ## Creating the conduwuit configuration file
Now we need to create the conduwuit's config file in `/etc/conduwuit/conduwuit.toml`. The example config can be found at [conduwuit-example.toml](../configuration.md).**Please take a moment to read it. You need to change at least the server name.**
RocksDB (`rocksdb`) is the only supported database backend. SQLite only exists for historical reasons and is not recommended. Any performance issues, storage issues, database issues, etc will not be assisted if using SQLite and you will be asked to migrate to RocksDB first.
RocksDB is the only supported database backend. SQLite only exists for historical reasons, is not recommended, and will be removed soon (likely in v0.5.0). Any performance issues, storage issues, database issues, etc will not be assisted if using SQLite and you will be asked to migrate to RocksDB first.
## Setting the correct file permissions
@@ -74,6 +74,8 @@ ## Setting up the Reverse Proxy
Refer to the documentation or various guides online of your chosen reverse proxy software. A [Caddy](https://caddyserver.com/) example will be provided as this is the recommended reverse proxy for new users and is very trivial to use (handles TLS, reverse proxy headers, etc transparently with proper defaults).
Lighttpd is not supported as it seems to mess with the `X-Matrix` Authorization header, making federation non-functional. If using Apache, you need to use `nocanon` to prevent this.
### Caddy
Create `/etc/caddy/conf.d/conduwuit_caddyfile` and enter this (substitute for your server name).

View File

@@ -10,9 +10,17 @@ # conduwuit for NixOS
following places (both are the same just different names):
```
https://attic.kennel.juneis.dog/conduit
conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg=
conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk=
https://attic.kennel.juneis.dog/conduwuit
conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE=
```
The binary caches have been recreated recently due to attic issues. The old public keys were:
```
conduit:Isq8FGyEC6FOXH6nD+BOeAA+bKp6X6UIbupSlGEPuOg=
conduwuit:lYPVh7o1hLu1idH4Xt2QHaRa49WRGSAqzcfFd94aOTw=
```

View File

@@ -16,8 +16,7 @@ ## Debugging with `tokio-console`
RUSTFLAGS="--cfg tokio_unstable" cargo build \
--release \
--no-default-features \
--features
backend_rocksdb,systemd,element_hacks,sentry_telemetry,gzip_compression,brotli_compression,zstd_compression,tokio_console
--features=rocksdb,systemd,element_hacks,sentry_telemetry,gzip_compression,brotli_compression,zstd_compression,tokio_console
```
[1]: https://docs.rs/tokio-console/latest/tokio_console/

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -0,0 +1,93 @@
# Hot Reloading ("Live" Development)
### Summary
When developing in debug-builds with the nightly toolchain, conduwuit is modular using dynamic libraries and various parts of the application are hot-reloadable while the server is running: http api handlers, admin commands, services, database, etc. These are all split up into individual workspace crates as seen in the `src/` directory. Changes to sourcecode in a crate rebuild that crate and subsequent crates depending on it. Reloading then occurs for the changed crates.
Release builds still produce static binaries which are unaffected. Rust's soundness guarantees are in full force. Thus you cannot hot-reload release binaries.
### Requirements
Currently, this development setup only works on x86_64 and aarch64 Linux glibc. [musl explicitly does not support hot reloadable libraries, and does not implement `dlclose`][2]. macOS does not fully support our usage of `RTLD_GLOBAL` possibly due to some thread-local issues. [This Rust issue][3] may be of relevance, specifically [this comment][4]. It may be possible to get it working on only very modern macOS versions such as at least Sonoma, as currently loading dylibs is supported, but not unloading them in our setup, and the cited comment mentions an Apple WWDC confirming there have been TLS changes to somewhat make this possible.
As mentioned above this requires the nightly toolchain. This is due to reliance on various Cargo.toml features that are only available on nightly, most specifically `RUSTFLAGS` in Cargo.toml. Some of the implementation could also be simpler based on other various nightly features. We hope lots of nightly features start making it out of nightly sooner as there have been dozens of very helpful features that have been stuck in nightly ("unstable") for at least 5+ years that would make this simpler. We encourage greater community consensus to move these features into stability.
This currently only works on x86_64/aarch64 Linux with a glibc C library. musl C library, macOS, and likely other host architectures are not supported (if other architectures work, feel free to let us know and/or make a PR updating this). This should work on GNU ld and lld (rust-lld) and gcc/clang, however if you happen to have linker issues it's recommended to try using `mold` or `gold` linkers, and please let us know in the [conduwuit Matrix room][7] the linker error and what linker solved this issue so we can figure out a solution. Ideally there should be minimal friction to using this, and in the future a build script (`build.rs`) may be suitable to making this easier to use if the capabilities allow us.
### Usage
As of 19 May 2024, the instructions for using this are:
0. Have patience. Don't hesitate to join the [conduwuit Matrix room][7] to receive help using this. As indicated by the various rustflags used and some of the interesting issues linked at the bottom, this is definitely not something the Rust ecosystem or toolchain is used to doing.
1. Install the nightly toolchain using rustup. You may need to use `rustup override set nightly` in your local conduwuit directory, or use `cargo +nightly` for all actions.
2. Uncomment `cargo-features` at the top level / root Cargo.toml
3. Scroll down to the `# Developer profile` section and uncomment ALL the rustflags for each dev profile and their respective packages.
4. In each workspace crate's Cargo.toml (everything under `src/*` AND `deps/rust-rocksdb/Cargo.toml`), uncomment the `dylib` crate type under `[lib]`.
5. Due to [this rpath issue][5], you must export the `LD_LIBRARY_PATH` environment variable to your nightly Rust toolchain library directory. If using rustup (hopefully), use this: `export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/`
6. Start the server. You can use `cargo +nightly run` for this along with the standard.
7. Make some changes where you need to.
8. In a separate terminal window in the same directory (or using a terminal multiplexer like tmux), run the *build* Cargo command `cargo +nightly build`. Cargo should only rebuild what was changed / what's necessary, so it should not be rebuilding all the crates.
9. In your conduwuit server terminal, hit/send `CTRL+C` signal. This will tell conduwuit to find which libraries need to be reloaded, and reloads them as necessary.
10. If there were no errors, it will tell you it successfully reloaded `#` modules, and your changes should now be visible. Repeat 7 - 9 as needed.
To shutdown conduwuit in this setup, hit/send `CTRL+\`. Normal builds still shutdown with `CTRL+C` as usual.
Steps 1 - 5 are the initial first-time steps for using this. To remove the hot reload setup, revert/comment all the Cargo.toml changes.
As mentioned in the requirements section, if you happen to have some linker issues, try using the `-fuse-ld=` rustflag and specify mold or gold in all the `rustflags` definitions in the top level Cargo.toml, and please let us know in the [conduwuit Matrix room][7] the problem. mold can be installed typically through your distro, and gold is provided by the binutils package.
It's possible a helper script can be made to do all of this, or most preferably a specially made build script (build.rs). `cargo watch` support will be implemented soon which will eliminate the need to manually run `cargo build` all together.
### Addendum
Conduit was inherited as a single crate without modularity or reloading in its design. Reasonable partitioning and abstraction allowed a split into several crates, though many circular dependencies had to be corrected. The resulting crates now form a directed graph as depicted in figures below. The interfacing between these crates is still extremely broad which is not mitigable.
Initially [hot_lib_reload][6] was investigated but found appropriate for a project designed with modularity through limited interfaces, not a large and complex existing codebase. Instead a bespoke solution built directly on [libloading][8] satisfied our constraints. This required relatively minimal modifications and zero maintenance burden compared to what would be required otherwise. The technical difference lies with relocation processing: we leverage global bindings (`RTLD_GLOBAL`) in a very intentional way. Most libraries and off-the-shelf module systems (such as [hot_lib_reload][6]) restrict themselves to local bindings (`RTLD_LOCAL`). This allows them to release software to multiple platforms with much greater consistency, but at the cost of burdening applications to explicitly manage these bindings. In our case with an optional feature for developers, we shrug any such requirement to enjoy the cost/benefit on platforms where global relocations are properly cooperative.
To make use of `RTLD_GLOBAL` the application has to be oriented as a directed acyclic graph. The primary rule is simple and illustrated in the figure below: **no crate is allowed to call a function or use a variable from a crate below it.**
![conduwuit's dynamic library setup diagram - created by Jason Volk](assets/libraries.png)
When a symbol is referenced between crates they become bound: **crates cannot be unloaded until their calling crates are first unloaded.** Thus we start the reloading process from the crate which has no callers. There is a small problem though: the first crate is called by the base executable itself! This is solved by using an `RTLD_LOCAL` binding for just one link between the main executable and the first crate, freeing the executable from all modules as no global binding ever occurs between them.
![conduwuit's reload and load order diagram - created by Jason Volk](assets/reload_order.png)
Proper resource management is essential for reliable reloading to occur. This is a very basic ask in RAII-idiomatic Rust and the exposure to reloading hazards is remarkably low, generally stemming from poor patterns and practices. Unfortunately static analysis doesn't enforce reload-safety programmatically (though it could one day), for now hazards can be avoided by knowing a few basic do's and dont's:
1. Understand that code is memory. Just like one is forbidden from referencing free'd memory, one must not transfer control to free'd code. Exposure to this is primarily from two things:
- Callbacks, which this project makes very little use of.
- Async tasks, which are addressed below.
2. Tie all resources to a scope or object lifetime with greatest possible symmetry (locality). For our purposes this applies to code resources, which means async blocks and tokio tasks.
- **Never spawn a task without receiving and storing its JoinHandle**.
- **Always wait on join handles** before leaving a scope or in another cleanup function called by an owning scope.
3. Know any minor specific quirks documented in code or here:
- Don't use `tokio::spawn`, instead use our `Handle` in `core/server.rs`, which is reachable in most of the codebase via `services()` or other state. This is due to some bugs or assumptions made in tokio, as it happens in `unsafe {}` blocks, which are mitigated by circumventing some thread-local variables. Using runtime handles is good practice in any case.
The initial implementation PR is available [here][1].
### Interesting related issues/bugs
- [DT_RUNPATH produced in binary with rpath = true is wrong (cargo)][5]
- [Disabling MIR Optimization in Rust Compilation (cargo)](https://internals.rust-lang.org/t/disabling-mir-optimization-in-rust-compilation/19066/5)
- [Workspace-level metadata (cargo-deb)](https://github.com/kornelski/cargo-deb/issues/68)
[1]: https://github.com/girlbossceo/conduwuit/pull/387
[2]: https://wiki.musl-libc.org/functional-differences-from-glibc.html#Unloading-libraries
[3]: https://github.com/rust-lang/rust/issues/28794
[4]: https://github.com/rust-lang/rust/issues/28794#issuecomment-368693049
[5]: https://github.com/rust-lang/cargo/issues/12746
[6]: https://crates.io/crates/hot-lib-reloader/
[7]: https://matrix.to/#/#conduwuit:puppygock.gay
[8]: https://crates.io/crates/libloading

View File

@@ -153,6 +153,7 @@ ## Misc:
- Interest in supporting other operating systems such as macOS, BSDs, and Windows, and getting them added into CI and doing builds for them
- Add config option for disabling RocksDB Direct IO if needed
- Add various documentation on maintaining conduwuit, using RocksDB online backups, some troubleshooting, using admin commands, etc
- (Developers): Add support for [hot reloadable/"live" modular development](development/hot_reload.md)
- (Developers): Add support for tokio-console
- (Developers): Add support for tracing flame graphs
- Add `release-debuginfo` Cargo build profile

View File

@@ -59,4 +59,4 @@ #### Pinging servers
#### Allocator memory stats
If using jemalloc (for now) and built with jemallocator's `stats` feature, you can see conduwuit's jemalloc memory stats by using `!admin debug memory-stats`
When using jemalloc with jemallocator's `stats` feature, you can see conduwuit's jemalloc memory stats by using `!admin debug memory-stats`

View File

@@ -58,7 +58,7 @@ script = "lychee --version"
[[task]]
name = "cargo-audit"
group = "security"
script = "cargo audit -D warnings -D unmaintained -D unsound -D yanked --ignore RUSTSEC-2020-0016"
script = "cargo audit -D warnings -D unmaintained -D unsound -D yanked"
[[task]]
name = "cargo-fmt"
@@ -69,12 +69,15 @@ script = "cargo fmt --check -- --color=always"
name = "cargo-doc"
group = "lints"
script = """
RUSTDOCFLAGS="-D warnings" cargo doc \
--workspace \
--all-features \
--no-deps \
--document-private-items \
--color always
env DIRENV_DEVSHELL=all-features \
RUSTDOCFLAGS="-D warnings" \
direnv exec . \
cargo doc \
--workspace \
--all-features \
--no-deps \
--document-private-items \
--color always
"""
[[task]]
@@ -93,13 +96,15 @@ cargo clippy \
name = "clippy/all"
group = "lints"
script = """
cargo clippy \
--workspace \
--all-targets \
--all-features \
--color=always \
-- \
-D warnings
env DIRENV_DEVSHELL=all-features \
direnv exec . \
cargo clippy \
--workspace \
--all-targets \
--all-features \
--color=always \
-- \
-D warnings
"""
[[task]]
@@ -115,18 +120,18 @@ cargo clippy \
-D warnings
"""
[[task]]
name = "clippy/hardened_malloc"
group = "lints"
script = """
cargo clippy \
--workspace \
--features hardened_malloc \
--all-targets \
--color=always \
-- \
-D warnings
"""
#[[task]]
#name = "clippy/hardened_malloc"
#group = "lints"
#script = """
#cargo clippy \
# --workspace \
# --features hardened_malloc \
# --all-targets \
# --color=always \
# -- \
# -D warnings
#"""
[[task]]
name = "lychee"
@@ -134,14 +139,40 @@ group = "lints"
script = "lychee --verbose --offline docs *.md"
[[task]]
name = "cargo"
name = "cargo/all"
group = "tests"
script = """
env DIRENV_DEVSHELL=all-features \
direnv exec . \
cargo test \
--workspace \
--all-targets \
--all-features \
--color=always \
-- \
--color=always
"""
[[task]]
name = "cargo/default"
group = "tests"
script = """
cargo test \
--workspace \
--all-targets \
--all-features \
--color=always \
-- \
--color=always
"""
# Ensure that the flake's default output can build and run without crashing
#
# This is a dynamically-linked jemalloc build, which is a case not covered by
# our other tests. We've had linking problems in the past with dynamic
# jemalloc builds that usually show up as an immediate segfault or "invalid free"
[[task]]
name = "nix-default"
group = "tests"
script = """
nix run .#default -- --help
"""

50
flake.lock generated
View File

@@ -68,11 +68,11 @@
]
},
"locked": {
"lastModified": 1715274763,
"narHash": "sha256-3Iv1PGHJn9sV3HO4FlOVaaztOxa9uGLfOmUWrH7v7+A=",
"lastModified": 1716569590,
"narHash": "sha256-5eDbq8TuXFGGO3mqJFzhUbt5zHVTf5zilQoyW5jnJwo=",
"owner": "ipetkov",
"repo": "crane",
"rev": "27025ab71bdca30e7ed0a16c88fd74c5970fc7f5",
"rev": "109987da061a1bf452f435f1653c47511587d919",
"type": "github"
},
"original": {
@@ -90,11 +90,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1715322226,
"narHash": "sha256-ezoe/FwfJpA7sskLoLP2iwfwkYnscEFCP6Vk5kPwh9k=",
"lastModified": 1716359173,
"narHash": "sha256-pYcjP6Gy7i6jPWrjiWAVV0BCQp+DdmGaI/k65lBb/kM=",
"owner": "nix-community",
"repo": "fenix",
"rev": "297c756ba6249d483c1dafe42378560458842173",
"rev": "b6fc5035b28e36a98370d0eac44f4ef3fd323df6",
"type": "github"
},
"original": {
@@ -171,6 +171,23 @@
"type": "github"
}
},
"liburing": {
"flake": false,
"locked": {
"lastModified": 1716565485,
"narHash": "sha256-4R19aJNQYs6vb0/Hz4bWT56YN1P1DkFL/sxdE4Yj0CE=",
"owner": "axboe",
"repo": "liburing",
"rev": "b90c0e670a93caabbebe2d9e24ff85cece4cfe0e",
"type": "github"
},
"original": {
"owner": "axboe",
"ref": "master",
"repo": "liburing",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1710156097,
@@ -221,11 +238,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1715266358,
"narHash": "sha256-doPgfj+7FFe9rfzWo1siAV2mVCasW+Bh8I1cToAXEE4=",
"lastModified": 1716330097,
"narHash": "sha256-8BO3B7e3BiyIDsaKA0tY8O88rClYRTjvAp66y+VBUeU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f1010e0469db743d14519a1efd37e23f8513d714",
"rev": "5710852ba686cc1fd0d3b8e22b3117d43ba374c2",
"type": "github"
},
"original": {
@@ -238,16 +255,16 @@
"rocksdb": {
"flake": false,
"locked": {
"lastModified": 1714770052,
"narHash": "sha256-NCPYF2wYBsB9OHEkZSOYoPlxjC9BBMhJp8EM5M1o3Mc=",
"lastModified": 1716773462,
"narHash": "sha256-5kUH+XK+2lbFfUgbxuNy3YMLHbp6scfWPdtc8za1wDM=",
"owner": "girlbossceo",
"repo": "rocksdb",
"rev": "db6df0b185774778457dabfcbd822cb81760cade",
"rev": "c8a1450231e9c608edf535538dbe8ca1a8d2f3bc",
"type": "github"
},
"original": {
"owner": "girlbossceo",
"ref": "v9.1.1",
"ref": "v9.2.1",
"repo": "rocksdb",
"type": "github"
}
@@ -260,6 +277,7 @@
"fenix": "fenix",
"flake-compat": "flake-compat_2",
"flake-utils": "flake-utils_2",
"liburing": "liburing",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_2",
"rocksdb": "rocksdb"
@@ -268,11 +286,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1715255944,
"narHash": "sha256-vLLgYpdtKBaGYTamNLg1rbRo1bPXp4Jgded/gnprPVw=",
"lastModified": 1716107283,
"narHash": "sha256-NJgrwLiLGHDrCia5AeIvZUHUY7xYGVryee0/9D3Ir1I=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "5bf2f85c8054d80424899fa581db1b192230efb5",
"rev": "21ec8f523812b88418b2bfc64240c62b3dd967bd",
"type": "github"
},
"original": {

166
flake.nix
View File

@@ -9,13 +9,15 @@
nix-filter.url = "github:numtide/nix-filter?ref=main";
nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
# https://github.com/girlbossceo/rocksdb/commit/db6df0b185774778457dabfcbd822cb81760cade
rocksdb = { url = "github:girlbossceo/rocksdb?ref=v9.1.1"; flake = false; };
rocksdb = { url = "github:girlbossceo/rocksdb?ref=v9.2.1"; flake = false; };
liburing = { url = "github:axboe/liburing?ref=master"; flake = false; };
};
outputs = inputs:
inputs.flake-utils.lib.eachDefaultSystem (system:
let
pkgsHost = inputs.nixpkgs.legacyPackages.${system};
pkgsHostStatic = pkgsHost.pkgsStatic;
# The Rust toolchain to use
toolchain = inputs.fenix.packages.${system}.fromToolchainFile {
@@ -25,7 +27,8 @@
sha256 = "sha256-+syqAd2kX8KVa8/U2gz3blIQTTsYYt3U63xBWaGOSc8";
};
scope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
mkScope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
inherit pkgs;
book = self.callPackage ./nix/pkgs/book {};
complement = self.callPackage ./nix/pkgs/complement {};
craneLib = ((inputs.crane.mkLib pkgs).overrideToolchain toolchain);
@@ -39,22 +42,87 @@
(builtins.fromJSON (builtins.readFile ./flake.lock))
.nodes.rocksdb.original.ref;
});
# TODO: remove once https://github.com/NixOS/nixpkgs/pull/314945 is available
liburing = pkgs.liburing.overrideAttrs (old: {
# the configure script doesn't support these, and unconditionally
# builds both static and dynamic libraries.
configureFlags = pkgs.lib.subtractLists
[ "--enable-static" "--disable-shared" ]
old.configureFlags;
postInstall = old.postInstall + ''
# we remove the extra outputs
#
# we need to do this to prevent rocksdb from trying to link the
# static library in a dynamic stdenv
rm $out/lib/liburing*${
if pkgs.stdenv.hostPlatform.isStatic then ".so*" else ".a"
}
'';
});
});
scopeHost = (scope pkgsHost);
scopeHost = mkScope pkgsHost;
scopeHostStatic = mkScope pkgsHostStatic;
mkDevShell = scope: scope.pkgs.mkShell {
env = scope.main.env // {
# Rust Analyzer needs to be able to find the path to default crate
# sources, and it can read this environment variable to do so. The
# `rust-src` component is required in order for this to work.
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
# Convenient way to access a pinned version of Complement's source
# code.
COMPLEMENT_SRC = inputs.complement.outPath;
# Needed for Complement
CGO_CFLAGS = "-I${scope.pkgs.olm}/include";
CGO_LDFLAGS = "-L${scope.pkgs.olm}/lib";
};
# Development tools
packages = [
# Always use nightly rustfmt because most of its options are unstable
#
# This needs to come before `toolchain` in this list, otherwise
# `$PATH` will have stable rustfmt instead.
inputs.fenix.packages.${system}.latest.rustfmt
toolchain
]
++ (with pkgsHost.pkgs; [
engage
cargo-audit
# Needed for producing Debian packages
cargo-deb
# Needed for Complement
go
# Needed for our script for Complement
jq
# Needed for finding broken markdown links
lychee
# Useful for editing the book locally
mdbook
])
++ scope.main.buildInputs
++ scope.main.propagatedBuildInputs
++ scope.main.nativeBuildInputs;
meta.broken = scope.main.meta.broken;
};
in
{
packages = {
default = scopeHost.main;
jemalloc = scopeHost.main.override { features = ["jemalloc"]; };
hmalloc = scopeHost.main.override { features = ["hardened_malloc"]; };
oci-image = scopeHost.oci-image;
oci-image-jemalloc = scopeHost.oci-image.override {
main = scopeHost.main.override {
features = ["jemalloc"];
};
};
oci-image-hmalloc = scopeHost.oci-image.override {
main = scopeHost.main.override {
features = ["hardened_malloc"];
@@ -64,6 +132,7 @@
book = scopeHost.book;
complement = scopeHost.complement;
static-complement = scopeHostStatic.complement;
}
//
builtins.listToAttrs
@@ -79,7 +148,7 @@
config = crossSystem;
};
}).pkgsStatic;
scopeCrossStatic = scope pkgsCrossStatic;
scopeCrossStatic = mkScope pkgsCrossStatic;
in
[
# An output for a statically-linked binary
@@ -88,14 +157,6 @@
value = scopeCrossStatic.main;
}
# An output for a statically-linked binary with jemalloc
{
name = "${binaryName}-jemalloc";
value = scopeCrossStatic.main.override {
features = ["jemalloc"];
};
}
# An output for a statically-linked binary with hardened_malloc
{
name = "${binaryName}-hmalloc";
@@ -110,16 +171,6 @@
value = scopeCrossStatic.oci-image;
}
# An output for an OCI image based on that binary with jemalloc
{
name = "oci-image-${crossSystem}-jemalloc";
value = scopeCrossStatic.oci-image.override {
main = scopeCrossStatic.main.override {
features = ["jemalloc"];
};
};
}
# An output for an OCI image based on that binary with hardened_malloc
{
name = "oci-image-${crossSystem}-hmalloc";
@@ -138,54 +189,15 @@
)
);
devShells.default = pkgsHost.mkShell {
env = scopeHost.main.env // {
# Rust Analyzer needs to be able to find the path to default crate
# sources, and it can read this environment variable to do so. The
# `rust-src` component is required in order for this to work.
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
# Convenient way to access a pinned version of Complement's source
# code.
COMPLEMENT_SRC = inputs.complement.outPath;
};
# Development tools
packages = [
# Always use nightly rustfmt because most of its options are unstable
#
# This needs to come before `toolchain` in this list, otherwise
# `$PATH` will have stable rustfmt instead.
inputs.fenix.packages.${system}.latest.rustfmt
toolchain
]
++ (with pkgsHost; [
engage
cargo-audit
# Needed for producing Debian packages
cargo-deb
# Needed for Complement
go
olm
# Needed for our script for Complement
jq
# Needed for finding broken markdown links
lychee
# Useful for editing the book locally
mdbook
])
++ (if !pkgsHost.stdenv.isDarwin then [
# Needed for building with io_uring
pkgsHost.liburing
] else [])
++
scopeHost.main.nativeBuildInputs;
};
devShells.default = mkDevShell scopeHostStatic;
devShells.all-features = mkDevShell
(scopeHostStatic.overrideScope (final: prev: {
main = prev.main.override { all_features = true; };
}));
devShells.no-features = mkDevShell
(scopeHostStatic.overrideScope (final: prev: {
main = prev.main.override { default_features = false; };
}));
devShells.dynamic = mkDevShell scopeHost;
});
}

View File

@@ -44,9 +44,7 @@ let
-sha256
${lib.getExe' coreutils "env"} \
CONDUIT_SERVER_NAME="$SERVER_NAME" \
CONDUIT_WELL_KNOWN_SERVER="$SERVER_NAME:8448" \
CONDUIT_WELL_KNOWN_SERVER="$SERVER_NAME:8008" \
CONDUWUIT_SERVER_NAME="$SERVER_NAME" \
${lib.getExe main'}
'';
in

View File

@@ -1,27 +1,87 @@
# Dependencies (keep sorted)
{ craneLib
, inputs
, jq
, lib
, libiconv
, liburing
, pkgsBuildHost
, rocksdb
, rust
, rust-jemalloc-sys
, stdenv
# Options (keep sorted)
, default_features ? true
, disable_release_max_log_level ? false
, all_features ? false
, disable_features ? []
, features ? []
, profile ? "release"
}:
let
# We perform default-feature unification in nix, because some of the dependencies
# on the nix side depend on feature values.
crateFeatures = path:
let manifest = lib.importTOML "${path}/Cargo.toml"; in
lib.remove "default" (lib.attrNames manifest.features) ++
lib.attrNames
(lib.filterAttrs
(_: dependency: dependency.optional or false)
manifest.dependencies);
crateDefaultFeatures = path:
(lib.importTOML "${path}/Cargo.toml").features.default;
allDefaultFeatures = crateDefaultFeatures "${inputs.self}/src/main";
allFeatures = crateFeatures "${inputs.self}/src/main";
features' = lib.unique
(features ++
lib.optionals default_features allDefaultFeatures ++
lib.optionals all_features allFeatures);
disable_features' = disable_features ++ lib.optionals disable_release_max_log_level ["release_max_log_level"];
features'' = lib.subtractLists disable_features' features';
featureEnabled = feature : builtins.elem feature features'';
enableLiburing = featureEnabled "io_uring" && stdenv.isLinux;
# This derivation will set the JEMALLOC_OVERRIDE variable, causing the
# tikv-jemalloc-sys crate to use the nixpkgs jemalloc instead of building it's
# own. In order for this to work, we need to set flags on the build that match
# whatever flags tikv-jemalloc-sys was going to use. These are dependent on
# which features we enable in tikv-jemalloc-sys.
rust-jemalloc-sys' = (rust-jemalloc-sys.override {
# tikv-jemalloc-sys/unprefixed_malloc_on_supported_platforms feature
unprefixed = true;
}).overrideAttrs (old: {
configureFlags = old.configureFlags ++
# tikv-jemalloc-sys/profiling feature
lib.optional (featureEnabled "jemalloc_prof") "--enable-prof";
});
buildDepsOnlyEnv =
let
rocksdb' = rocksdb.override {
enableJemalloc = builtins.elem "jemalloc" features;
};
rocksdb' = (rocksdb.override {
jemalloc = rust-jemalloc-sys';
# rocksdb fails to build with prefixed jemalloc, which is required on
# darwin due to [1]. In this case, fall back to building rocksdb with
# libc malloc. This should not cause conflicts, because all of the
# jemalloc symbols are prefixed.
#
# [1]: https://github.com/tikv/jemallocator/blob/ab0676d77e81268cd09b059260c75b38dbef2d51/jemalloc-sys/src/env.rs#L17
enableJemalloc = featureEnabled "jemalloc" && !stdenv.isDarwin;
}).overrideAttrs (old: {
# TODO: static rocksdb fails to build on darwin
# build log at <https://girlboss.ceo/~strawberry/pb/JjGH>
meta.broken = stdenv.hostPlatform.isStatic && stdenv.isDarwin;
# TODO: switch to enableUring option once https://github.com/NixOS/nixpkgs/pull/314945 is available
buildInputs = old.buildInputs ++ lib.optional enableLiburing liburing;
});
in
{
# https://crane.dev/faq/rebuilds-bindgen.html
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
CARGO_PROFILE = profile;
ROCKSDB_INCLUDE_DIR = "${rocksdb'}/include";
ROCKSDB_LIB_DIR = "${rocksdb'}/lib";
@@ -38,7 +98,14 @@ buildDepsOnlyEnv =
buildPackageEnv = {
CONDUWUIT_VERSION_EXTRA = inputs.self.shortRev or inputs.self.dirtyShortRev or "";
} // buildDepsOnlyEnv;
} // buildDepsOnlyEnv // {
# Only needed in static stdenv because these are transitive dependencies of rocksdb
CARGO_BUILD_RUSTFLAGS = buildDepsOnlyEnv.CARGO_BUILD_RUSTFLAGS
+ lib.optionalString (enableLiburing && stdenv.hostPlatform.isStatic)
" -L${lib.getLib liburing}/lib -luring";
};
commonAttrs = {
inherit
@@ -55,16 +122,24 @@ commonAttrs = {
include = [
"Cargo.lock"
"Cargo.toml"
"hot_lib"
"deps"
"src"
];
};
buildInputs = lib.optional (featureEnabled "jemalloc") rust-jemalloc-sys';
nativeBuildInputs = [
# bindgen needs the build platform's libclang. Apparently due to "splicing
# weirdness", pkgs.rustPlatform.bindgenHook on its own doesn't quite do the
# right thing here.
pkgsBuildHost.rustPlatform.bindgenHook
# We don't actually depend on `jq`, but crane's `buildPackage` does, but
# its `buildDepsOnly` doesn't. This causes those two derivations to have
# differing values for `NIX_CFLAGS_COMPILE`, which contributes to spurious
# rebuilds of bindgen and its depedents.
jq
]
++ lib.optionals stdenv.isDarwin [
# https://github.com/NixOS/nixpkgs/issues/206242
@@ -82,22 +157,16 @@ craneLib.buildPackage ( commonAttrs // {
env = buildDepsOnlyEnv;
});
cargoExtraArgs = ""
cargoExtraArgs = "--no-default-features "
+ lib.optionalString
(!default_features)
"--no-default-features "
+ lib.optionalString
(features != [])
"--features " + (builtins.concatStringsSep "," features);
(features'' != [])
"--features " + (builtins.concatStringsSep "," features'');
# This is redundant with CI
cargoTestCommand = "";
cargoCheckCommand = "";
doCheck = false;
# https://crane.dev/faq/rebuilds-bindgen.html
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
env = buildPackageEnv;
passthru = {

View File

@@ -11,5 +11,6 @@
},
"nix": {
"enabled": true
}
},
"labels": ["dependencies", "github_actions"]
}

61
src/admin/Cargo.toml Normal file
View File

@@ -0,0 +1,61 @@
[package]
name = "conduit_admin"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
[lib]
path = "mod.rs"
crate-type = [
"rlib",
# "dylib",
]
[features]
dev_release_log_level = []
release_max_log_level = [
"tracing/max_level_trace",
"tracing/release_max_level_info",
"log/max_level_trace",
"log/release_max_level_info",
]
rocksdb = [
"dep:rust-rocksdb",
]
jemalloc = [
"rust-rocksdb/jemalloc",
]
io_uring = [
"rust-rocksdb/io-uring",
]
zstd_compression = [
"rust-rocksdb/zstd",
]
[dependencies]
clap.workspace = true
conduit-api.workspace = true
conduit-core.workspace = true
conduit-database.workspace = true
conduit-service.workspace = true
futures-util.workspace = true
log.workspace = true
loole.workspace = true
regex.workspace = true
ruma.workspace = true
rust-rocksdb.optional = true
rust-rocksdb.workspace = true
serde_json.workspace = true
serde.workspace = true
serde_yaml.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
[lints]
workspace = true

View File

@@ -1,28 +1,28 @@
use ruma::{api::appservice::Registration, events::room::message::RoomMessageEventContent};
use crate::{service::admin::escape_html, services, Result};
use crate::{escape_html, services, Result};
pub(crate) async fn register(body: Vec<&str>) -> Result<RoomMessageEventContent> {
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
let appservice_config = body[1..body.len().checked_sub(1).unwrap()].join("\n");
let parsed_config = serde_yaml::from_str::<Registration>(&appservice_config);
match parsed_config {
Ok(yaml) => match services().appservice.register_appservice(yaml).await {
Ok(id) => Ok(RoomMessageEventContent::text_plain(format!(
"Appservice registered with ID: {id}."
))),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Failed to register appservice: {e}"
))),
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Could not parse appservice config: {e}"
))),
}
} else {
Ok(RoomMessageEventContent::text_plain(
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
))
));
}
let appservice_config = body[1..body.len().checked_sub(1).unwrap()].join("\n");
let parsed_config = serde_yaml::from_str::<Registration>(&appservice_config);
match parsed_config {
Ok(yaml) => match services().appservice.register_appservice(yaml).await {
Ok(id) => Ok(RoomMessageEventContent::text_plain(format!(
"Appservice registered with ID: {id}."
))),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Failed to register appservice: {e}"
))),
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Could not parse appservice config: {e}"
))),
}
}
@@ -47,7 +47,7 @@ pub(crate) async fn show(_body: Vec<&str>, appservice_identifier: String) -> Res
{
Some(config) => {
let config_str = serde_yaml::to_string(&config).expect("config should've been validated on register");
let output = format!("Config for {}:\n\n```yaml\n{}\n```", appservice_identifier, config_str,);
let output = format!("Config for {appservice_identifier}:\n\n```yaml\n{config_str}\n```",);
let output_html = format!(
"Config for {}:\n\n<pre><code class=\"language-yaml\">{}</code></pre>",
escape_html(&appservice_identifier),

View File

@@ -1,8 +1,8 @@
use clap::Subcommand;
use conduit::Result;
use ruma::events::room::message::RoomMessageEventContent;
use self::appservice_command::{list, register, show, unregister};
use crate::Result;
pub(crate) mod appservice_command;

View File

@@ -1,18 +1,21 @@
use std::{collections::BTreeMap, sync::Arc, time::Instant};
use ruma::{
api::client::error::ErrorKind, events::room::message::RoomMessageEventContent, CanonicalJsonObject, EventId,
RoomId, RoomVersionId, ServerName,
use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
time::Instant,
};
use api::client::validate_and_add_event_id;
use conduit::{utils::HtmlEscape, Error, Result};
use ruma::{
api::{client::error::ErrorKind, federation::event::get_room_state},
events::room::message::RoomMessageEventContent,
CanonicalJsonObject, EventId, RoomId, RoomVersionId, ServerName,
};
use service::{rooms::event_handler::parse_incoming_pdu, sending::resolve::resolve_actual_dest, services, PduEvent};
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
use tracing_subscriber::EnvFilter;
use crate::{
api::server_server::parse_incoming_pdu, service::sending::send::resolve_actual_dest, services, utils::HtmlEscape,
Error, PduEvent, Result,
};
pub(crate) async fn get_auth_chain(_body: Vec<&str>, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
let event_id = Arc::<EventId>::from(event_id);
if let Some(event) = services().rooms.timeline.get_pdu_json(&event_id)? {
@@ -40,28 +43,30 @@ pub(crate) async fn get_auth_chain(_body: Vec<&str>, event_id: Box<EventId>) ->
}
pub(crate) async fn parse_pdu(body: Vec<&str>) -> Result<RoomMessageEventContent> {
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
Ok(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
Ok(hash) => {
let event_id = EventId::parse(format!("${hash}"));
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
match serde_json::from_value::<PduEvent>(serde_json::to_value(value).expect("value is json")) {
Ok(pdu) => Ok(RoomMessageEventContent::text_plain(format!("EventId: {event_id:?}\n{pdu:#?}"))),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"EventId: {event_id:?}\nCould not parse event: {e}"
))),
}
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Could not parse PDU JSON: {e:?}"))),
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
Ok(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
Ok(hash) => {
let event_id = EventId::parse(format!("${hash}"));
match serde_json::from_value::<PduEvent>(serde_json::to_value(value).expect("value is json")) {
Ok(pdu) => Ok(RoomMessageEventContent::text_plain(format!("EventId: {event_id:?}\n{pdu:#?}"))),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"EventId: {event_id:?}\nCould not parse event: {e}"
))),
}
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Invalid json in command body: {e}"
))),
}
} else {
Ok(RoomMessageEventContent::text_plain("Expected code block in command body."))
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Could not parse PDU JSON: {e:?}"))),
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Invalid json in command body: {e}"
))),
}
}
@@ -114,31 +119,40 @@ pub(crate) async fn get_remote_pdu_list(
if server == services().globals.server_name() {
return Ok(RoomMessageEventContent::text_plain(
"Not allowed to send federation requests to ourselves. Please use `get-pdu` for fetching local PDUs.",
"Not allowed to send federation requests to ourselves. Please use `get-pdu` for fetching local PDUs from \
the database.",
));
}
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
let list = body
.clone()
.drain(1..body.len().checked_sub(1).unwrap())
.filter_map(|pdu| EventId::parse(pdu).ok())
.collect::<Vec<_>>();
for pdu in list {
if force {
_ = get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await;
} else {
get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await?;
}
}
return Ok(RoomMessageEventContent::text_plain("Fetched list of remote PDUs."));
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
))
let list = body
.clone()
.drain(1..body.len().checked_sub(1).unwrap())
.filter_map(|pdu| EventId::parse(pdu).ok())
.collect::<Vec<_>>();
for pdu in list {
if force {
if let Err(e) = get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Failed to get remote PDU, ignoring error: {e}"
)))
.await;
warn!(%e, "Failed to get remote PDU, ignoring error");
}
} else {
get_remote_pdu(Vec::new(), Box::from(pdu), server.clone()).await?;
}
}
Ok(RoomMessageEventContent::text_plain("Fetched list of remote PDUs."))
}
pub(crate) async fn get_remote_pdu(
@@ -295,8 +309,7 @@ pub(crate) async fn ping(_body: Vec<&str>, server: Box<ServerName>) -> Result<Ro
}
Ok(RoomMessageEventContent::text_plain(format!(
"Got non-JSON response which took {ping_time:?} time:\n{0:?}",
response
"Got non-JSON response which took {ping_time:?} time:\n{response:?}"
)))
},
Err(e) => {
@@ -332,7 +345,7 @@ pub(crate) async fn change_log_level(
};
match services()
.globals
.server
.tracing_reload_handle
.reload(&old_filter_layer)
{
@@ -361,7 +374,7 @@ pub(crate) async fn change_log_level(
};
match services()
.globals
.server
.tracing_reload_handle
.reload(&new_filter_layer)
{
@@ -380,56 +393,239 @@ pub(crate) async fn change_log_level(
}
pub(crate) async fn sign_json(body: Vec<&str>) -> Result<RoomMessageEventContent> {
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
let string = body[1..body.len().checked_sub(1).unwrap()].join("\n");
match serde_json::from_str(&string) {
Ok(mut value) => {
ruma::signatures::sign_json(
services().globals.server_name().as_str(),
services().globals.keypair(),
&mut value,
)
.expect("our request json is what ruma expects");
let json_text = serde_json::to_string_pretty(&value).expect("canonical json is valid json");
Ok(RoomMessageEventContent::text_plain(json_text))
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))),
}
} else {
Ok(RoomMessageEventContent::text_plain(
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
))
));
}
let string = body[1..body.len().checked_sub(1).unwrap()].join("\n");
match serde_json::from_str(&string) {
Ok(mut value) => {
ruma::signatures::sign_json(
services().globals.server_name().as_str(),
services().globals.keypair(),
&mut value,
)
.expect("our request json is what ruma expects");
let json_text = serde_json::to_string_pretty(&value).expect("canonical json is valid json");
Ok(RoomMessageEventContent::text_plain(json_text))
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))),
}
}
pub(crate) async fn verify_json(body: Vec<&str>) -> Result<RoomMessageEventContent> {
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
let string = body[1..body.len().checked_sub(1).unwrap()].join("\n");
match serde_json::from_str(&string) {
Ok(value) => {
let pub_key_map = RwLock::new(BTreeMap::new());
services()
.rooms
.event_handler
.fetch_required_signing_keys([&value], &pub_key_map)
.await?;
let pub_key_map = pub_key_map.read().await;
match ruma::signatures::verify_json(&pub_key_map, &value) {
Ok(()) => Ok(RoomMessageEventContent::text_plain("Signature correct")),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Signature verification failed: {e}"
))),
}
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))),
}
} else {
Ok(RoomMessageEventContent::text_plain(
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
))
));
}
let string = body[1..body.len().checked_sub(1).unwrap()].join("\n");
match serde_json::from_str(&string) {
Ok(value) => {
let pub_key_map = RwLock::new(BTreeMap::new());
services()
.rooms
.event_handler
.fetch_required_signing_keys([&value], &pub_key_map)
.await?;
let pub_key_map = pub_key_map.read().await;
match ruma::signatures::verify_json(&pub_key_map, &value) {
Ok(()) => Ok(RoomMessageEventContent::text_plain("Signature correct")),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Signature verification failed: {e}"
))),
}
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))),
}
}
#[tracing::instrument(skip(_body))]
pub(crate) async fn first_pdu_in_room(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
if !services()
.rooms
.state_cache
.server_in_room(&services().globals.config.server_name, &room_id)?
{
return Ok(RoomMessageEventContent::text_plain(
"We are not participating in the room / we don't know about the room ID.",
));
}
let first_pdu = services()
.rooms
.timeline
.first_pdu_in_room(&room_id)?
.ok_or_else(|| Error::bad_database("Failed to find the first PDU in database"))?;
Ok(RoomMessageEventContent::text_plain(format!("{first_pdu:?}")))
}
#[tracing::instrument(skip(_body))]
pub(crate) async fn latest_pdu_in_room(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
if !services()
.rooms
.state_cache
.server_in_room(&services().globals.config.server_name, &room_id)?
{
return Ok(RoomMessageEventContent::text_plain(
"We are not participating in the room / we don't know about the room ID.",
));
}
let latest_pdu = services()
.rooms
.timeline
.latest_pdu_in_room(&room_id)?
.ok_or_else(|| Error::bad_database("Failed to find the latest PDU in database"))?;
Ok(RoomMessageEventContent::text_plain(format!("{latest_pdu:?}")))
}
#[tracing::instrument(skip(_body))]
pub(crate) async fn force_set_room_state_from_server(
_body: Vec<&str>, server_name: Box<ServerName>, room_id: Box<RoomId>,
) -> Result<RoomMessageEventContent> {
if !services()
.rooms
.state_cache
.server_in_room(&services().globals.config.server_name, &room_id)?
{
return Ok(RoomMessageEventContent::text_plain(
"We are not participating in the room / we don't know about the room ID.",
));
}
let first_pdu = services()
.rooms
.timeline
.latest_pdu_in_room(&room_id)?
.ok_or_else(|| Error::bad_database("Failed to find the latest PDU in database"))?;
let room_version = services().rooms.state.get_room_version(&room_id)?;
let mut state: HashMap<u64, Arc<EventId>> = HashMap::new();
let pub_key_map = RwLock::new(BTreeMap::new());
let remote_state_response = services()
.sending
.send_federation_request(
&server_name,
get_room_state::v1::Request {
room_id: room_id.clone().into(),
event_id: first_pdu.event_id.clone().into(),
},
)
.await?;
let mut events = Vec::with_capacity(remote_state_response.pdus.len());
for pdu in remote_state_response.pdus.clone() {
events.push(match parse_incoming_pdu(&pdu) {
Ok(t) => t,
Err(e) => {
warn!("Could not parse PDU, ignoring: {e}");
continue;
},
});
}
info!("Fetching required signing keys for all the state events we got");
services()
.rooms
.event_handler
.fetch_required_signing_keys(events.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map)
.await?;
info!("Going through room_state response PDUs");
for result in remote_state_response
.pdus
.iter()
.map(|pdu| validate_and_add_event_id(pdu, &room_version, &pub_key_map))
{
let Ok((event_id, value)) = result.await else {
continue;
};
let pdu = PduEvent::from_id_val(&event_id, value.clone()).map_err(|e| {
warn!("Invalid PDU in fetching remote room state PDUs response: {} {:?}", e, value);
Error::BadServerResponse("Invalid PDU in send_join response.")
})?;
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)?;
state.insert(shortstatekey, pdu.event_id.clone());
}
}
info!("Going through auth_chain response");
for result in remote_state_response
.auth_chain
.iter()
.map(|pdu| validate_and_add_event_id(pdu, &room_version, &pub_key_map))
{
let Ok((event_id, value)) = result.await else {
continue;
};
services()
.rooms
.outlier
.add_pdu_outlier(&event_id, &value)?;
}
let new_room_state = services()
.rooms
.event_handler
.resolve_state(room_id.clone().as_ref(), &room_version, state)
.await?;
info!("Forcing new room state");
let (short_state_hash, new, removed) = services()
.rooms
.state_compressor
.save_state(room_id.clone().as_ref(), new_room_state)?;
let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(room_id.clone().into())
.or_default(),
);
let state_lock = mutex_state.lock().await;
services()
.rooms
.state
.force_state(room_id.clone().as_ref(), short_state_hash, new, removed, &state_lock)
.await?;
info!(
"Updating joined counts for room just in case (e.g. we may have found a difference in the room's \
m.room.member state"
);
services().rooms.state_cache.update_joined_count(&room_id)?;
drop(state_lock);
Ok(RoomMessageEventContent::text_plain(
"Successfully forced the room state from the requested remote server.",
))
}
pub(crate) async fn resolve_true_destination(
@@ -447,15 +643,16 @@ pub(crate) async fn resolve_true_destination(
));
}
let (actual_dest, hostname_uri) = resolve_actual_dest(&server_name, no_cache, true).await?;
let (actual_dest, hostname_uri) = resolve_actual_dest(&server_name, !no_cache).await?;
Ok(RoomMessageEventContent::text_plain(format!(
"Actual destination: {actual_dest:?} | Hostname URI: {hostname_uri}"
"Actual destination: {actual_dest} | Hostname URI: {hostname_uri}"
)))
}
#[must_use]
pub(crate) fn memory_stats() -> RoomMessageEventContent {
let html_body = crate::alloc::memory_stats();
let html_body = conduit::alloc::memory_stats();
if html_body.is_empty() {
return RoomMessageEventContent::text_plain("malloc stats are not supported on your compiled malloc.");

View File

@@ -1,4 +1,5 @@
use clap::Subcommand;
use debug_commands::{first_pdu_in_room, force_set_room_state_from_server, latest_pdu_in_room};
use ruma::{events::room::message::RoomMessageEventContent, EventId, RoomId, ServerName};
use self::debug_commands::{
@@ -45,7 +46,8 @@ pub(crate) enum DebugCommand {
server: Box<ServerName>,
},
/// Same as `get-remote-pdu` but accepts a codeblock newline delimited list
/// - Same as `get-remote-pdu` but accepts a codeblock newline delimited
/// list
/// of PDUs and a single server to fetch from
GetRemotePduList {
/// Argument for us to attempt to fetch all the events from the
@@ -107,6 +109,41 @@ pub(crate) enum DebugCommand {
/// the command.
VerifyJson,
/// - Prints the very first PDU in the specified room (typically
/// m.room.create)
FirstPduInRoom {
/// The room ID
room_id: Box<RoomId>,
},
/// - Prints the latest ("last") PDU in the specified room (typically a
/// message)
LatestPduInRoom {
/// The room ID
room_id: Box<RoomId>,
},
/// - Forcefully replaces the room state of our local copy of the specified
/// room, with the copy (auth chain and room state events) the specified
/// remote server says.
///
/// A common desire for room deletion is to simply "reset" our copy of the
/// room. While this admin command is not a replacement for that, if you
/// know you have split/broken room state and you know another server in the
/// room that has the best/working room state, this command can let you use
/// their room state. Such example is your server saying users are in a
/// room, but other servers are saying they're not in the room in question.
///
/// This command will get the latest PDU in the room we know about, and
/// request the room state at that point in time via
/// `/_matrix/federation/v1/state/{roomId}`.
ForceSetRoomStateFromServer {
/// The impacted room ID
room_id: Box<RoomId>,
/// The server we will use to query the room state for
server_name: Box<ServerName>,
},
/// - Runs a server name through conduwuit's true destination resolution
/// process
///
@@ -148,10 +185,20 @@ pub(crate) async fn process(command: DebugCommand, body: Vec<&str>) -> Result<Ro
} => change_log_level(body, filter, reset).await?,
DebugCommand::SignJson => sign_json(body).await?,
DebugCommand::VerifyJson => verify_json(body).await?,
DebugCommand::FirstPduInRoom {
room_id,
} => first_pdu_in_room(body, room_id).await?,
DebugCommand::LatestPduInRoom {
room_id,
} => latest_pdu_in_room(body, room_id).await?,
DebugCommand::GetRemotePduList {
server,
force,
} => get_remote_pdu_list(body, server, force).await?,
DebugCommand::ForceSetRoomStateFromServer {
room_id,
server_name,
} => force_set_room_state_from_server(body, server_name, room_id).await?,
DebugCommand::ResolveTrueDestination {
server_name,
no_cache,

View File

@@ -1,13 +1,8 @@
use std::fmt::Write as _;
use std::fmt::Write;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomId, ServerName, UserId};
use crate::{
service::admin::{escape_html, get_room_info},
services,
utils::HtmlEscape,
Result,
};
use crate::{escape_html, get_room_info, services, utils::HtmlEscape, Result};
pub(crate) async fn disable_room(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
services().rooms.metadata.disable_room(&room_id, true)?;
@@ -19,13 +14,14 @@ pub(crate) async fn enable_room(_body: Vec<&str>, room_id: Box<RoomId>) -> Resul
Ok(RoomMessageEventContent::text_plain("Room enabled."))
}
pub(crate) async fn incoming_federeation(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
pub(crate) async fn incoming_federation(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
let map = services().globals.roomid_federationhandletime.read().await;
let mut msg = format!("Handling {} incoming pdus:\n", map.len());
for (r, (e, i)) in map.iter() {
let elapsed = i.elapsed();
let _ = writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60);
writeln!(msg, "{} {}: {}m{}s", r, e, elapsed.as_secs() / 60, elapsed.as_secs() % 60,)
.expect("should be able to write to string buffer");
}
Ok(RoomMessageEventContent::text_plain(&msg))
}
@@ -105,7 +101,8 @@ pub(crate) async fn remote_user_in_rooms(_body: Vec<&str>, user_id: Box<UserId>)
rooms.reverse();
let output_plain = format!(
"Rooms {user_id} shares with us:\n{}",
"Rooms {user_id} shares with us ({}):\n{}",
rooms.len(),
rooms
.iter()
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
@@ -113,19 +110,20 @@ pub(crate) async fn remote_user_in_rooms(_body: Vec<&str>, user_id: Box<UserId>)
.join("\n")
);
let output_html = format!(
"<table><caption>Rooms {user_id} shares with \
us</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
"<table><caption>Rooms {user_id} shares with us \
({})</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
rooms.len(),
rooms
.iter()
.fold(String::new(), |mut output, (id, members, name)| {
writeln!(
output,
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
escape_html(id.as_ref()),
id,
members,
escape_html(name)
)
.unwrap();
.expect("should be able to write to string buffer");
output
})
);

View File

@@ -2,7 +2,7 @@
use ruma::{events::room::message::RoomMessageEventContent, RoomId, ServerName, UserId};
use self::federation_commands::{
disable_room, enable_room, fetch_support_well_known, incoming_federeation, remote_user_in_rooms,
disable_room, enable_room, fetch_support_well_known, incoming_federation, remote_user_in_rooms,
};
use crate::Result;
@@ -51,7 +51,7 @@ pub(crate) async fn process(command: FederationCommand, body: Vec<&str>) -> Resu
FederationCommand::EnableRoom {
room_id,
} => enable_room(body, room_id).await?,
FederationCommand::IncomingFederation => incoming_federeation(body).await?,
FederationCommand::IncomingFederation => incoming_federation(body).await?,
FederationCommand::FetchSupportWellKnown {
server_name,
} => fetch_support_well_known(body, server_name).await?,

View File

@@ -17,9 +17,8 @@ pub(crate) async fn check_all_users(_body: Vec<&str>) -> Result<RoomMessageEvent
let ok_count = users.iter().filter(|user| user.is_ok()).count();
let message = format!(
"Database query completed in {query_time:?}:\n\n```\nTotal entries: {:?}\nFailure/Invalid user count: \
{:?}\nSuccess/Valid user count: {:?}```",
total, err_count, ok_count
"Database query completed in {query_time:?}:\n\n```\nTotal entries: {total:?}\nFailure/Invalid user count: \
{err_count:?}\nSuccess/Valid user count: {ok_count:?}```"
);
Ok(RoomMessageEventContent::notice_html(message, String::new()))

306
src/admin/handler.rs Normal file
View File

@@ -0,0 +1,306 @@
use std::sync::Arc;
use clap::Parser;
use regex::Regex;
use ruma::{
events::{
relation::InReplyTo,
room::message::{Relation::Reply, RoomMessageEventContent},
TimelineEventType,
},
OwnedRoomId, OwnedUserId, RoomId, ServerName, UserId,
};
use serde_json::value::to_raw_value;
use tokio::sync::MutexGuard;
use tracing::error;
extern crate conduit_service as service;
use conduit::{Error, Result};
pub(crate) use service::admin::{AdminRoomEvent, Service};
use service::{admin::HandlerResult, pdu::PduBuilder};
use self::{fsck::FsckCommand, tester::TesterCommands};
use crate::{
appservice, appservice::AppserviceCommand, debug, debug::DebugCommand, escape_html, federation,
federation::FederationCommand, fsck, media, media::MediaCommand, query, query::QueryCommand, room,
room::RoomCommand, server, server::ServerCommand, services, tester, user, user::UserCommand,
};
pub(crate) const PAGE_SIZE: usize = 100;
#[cfg_attr(test, derive(Debug))]
#[derive(Parser)]
#[command(name = "@conduit:server.name:", version = env!("CARGO_PKG_VERSION"))]
pub(crate) enum AdminCommand {
#[command(subcommand)]
/// - Commands for managing appservices
Appservices(AppserviceCommand),
#[command(subcommand)]
/// - Commands for managing local users
Users(UserCommand),
#[command(subcommand)]
/// - Commands for managing rooms
Rooms(RoomCommand),
#[command(subcommand)]
/// - Commands for managing federation
Federation(FederationCommand),
#[command(subcommand)]
/// - Commands for managing the server
Server(ServerCommand),
#[command(subcommand)]
/// - Commands for managing media
Media(MediaCommand),
#[command(subcommand)]
/// - Commands for debugging things
Debug(DebugCommand),
#[command(subcommand)]
/// - Query all the database getters and iterators
Query(QueryCommand),
#[command(subcommand)]
/// - Query all the database getters and iterators
Fsck(FsckCommand),
#[command(subcommand)]
Tester(TesterCommands),
}
#[must_use]
pub fn handle(event: AdminRoomEvent, room: OwnedRoomId, user: OwnedUserId) -> HandlerResult {
Box::pin(handle_event(event, room, user))
}
async fn handle_event(event: AdminRoomEvent, admin_room: OwnedRoomId, server_user: OwnedUserId) -> Result<()> {
let (mut message_content, reply) = match event {
AdminRoomEvent::SendMessage(content) => (content, None),
AdminRoomEvent::ProcessMessage(room_message, reply_id) => {
// This future is ~8 KiB so it's better to start it off the stack.
(Box::pin(process_admin_message(room_message)).await, Some(reply_id))
},
};
let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(admin_room.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
if let Some(reply) = reply {
message_content.relates_to = Some(Reply {
in_reply_to: InReplyTo {
event_id: reply.into(),
},
});
}
let response_pdu = PduBuilder {
event_type: TimelineEventType::RoomMessage,
content: to_raw_value(&message_content).expect("event is valid, we just created it"),
unsigned: None,
state_key: None,
redacts: None,
};
if let Err(e) = services()
.rooms
.timeline
.build_and_append_pdu(response_pdu, &server_user, &admin_room, &state_lock)
.await
{
handle_response_error(&e, &admin_room, &server_user, &state_lock).await?;
}
Ok(())
}
async fn handle_response_error(
e: &Error, admin_room: &RoomId, server_user: &UserId, state_lock: &MutexGuard<'_, ()>,
) -> Result<()> {
error!("Failed to build and append admin room response PDU: \"{e}\"");
let error_room_message = RoomMessageEventContent::text_plain(format!(
"Failed to build and append admin room PDU: \"{e}\"\n\nThe original admin command may have finished \
successfully, but we could not return the output."
));
let response_pdu = PduBuilder {
event_type: TimelineEventType::RoomMessage,
content: to_raw_value(&error_room_message).expect("event is valid, we just created it"),
unsigned: None,
state_key: None,
redacts: None,
};
services()
.rooms
.timeline
.build_and_append_pdu(response_pdu, server_user, admin_room, state_lock)
.await?;
Ok(())
}
// Parse and process a message from the admin room
async fn process_admin_message(room_message: String) -> RoomMessageEventContent {
let mut lines = room_message.lines().filter(|l| !l.trim().is_empty());
let command_line = lines.next().expect("each string has at least one line");
let body = lines.collect::<Vec<_>>();
let admin_command = match parse_admin_command(command_line) {
Ok(command) => command,
Err(error) => {
let server_name = services().globals.server_name();
let message = error.replace("server.name", server_name.as_str());
let html_message = usage_to_html(&message, server_name);
return RoomMessageEventContent::text_html(message, html_message);
},
};
match process_admin_command(admin_command, body).await {
Ok(reply_message) => reply_message,
Err(error) => {
let markdown_message = format!("Encountered an error while handling the command:\n```\n{error}\n```",);
let html_message = format!("Encountered an error while handling the command:\n<pre>\n{error}\n</pre>",);
RoomMessageEventContent::text_html(markdown_message, html_message)
},
}
}
// Parse chat messages from the admin room into an AdminCommand object
fn parse_admin_command(command_line: &str) -> Result<AdminCommand, String> {
// Note: argv[0] is `@conduit:servername:`, which is treated as the main command
let mut argv = command_line.split_whitespace().collect::<Vec<_>>();
// Replace `help command` with `command --help`
// Clap has a help subcommand, but it omits the long help description.
if argv.len() > 1 && argv[1] == "help" {
argv.remove(1);
argv.push("--help");
}
// Backwards compatibility with `register_appservice`-style commands
let command_with_dashes_argv1;
if argv.len() > 1 && argv[1].contains('_') {
command_with_dashes_argv1 = argv[1].replace('_', "-");
argv[1] = &command_with_dashes_argv1;
}
// Backwards compatibility with `register_appservice`-style commands
let command_with_dashes_argv2;
if argv.len() > 2 && argv[2].contains('_') {
command_with_dashes_argv2 = argv[2].replace('_', "-");
argv[2] = &command_with_dashes_argv2;
}
// if the user is using the `query` command (argv[1]), replace the database
// function/table calls with underscores to match the codebase
let command_with_dashes_argv3;
if argv.len() > 3 && argv[1].eq("query") {
command_with_dashes_argv3 = argv[3].replace('_', "-");
argv[3] = &command_with_dashes_argv3;
}
AdminCommand::try_parse_from(argv).map_err(|error| error.to_string())
}
async fn process_admin_command(command: AdminCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
let reply_message_content = match command {
AdminCommand::Appservices(command) => appservice::process(command, body).await?,
AdminCommand::Media(command) => media::process(command, body).await?,
AdminCommand::Users(command) => user::process(command, body).await?,
AdminCommand::Rooms(command) => room::process(command, body).await?,
AdminCommand::Federation(command) => federation::process(command, body).await?,
AdminCommand::Server(command) => server::process(command, body).await?,
AdminCommand::Debug(command) => debug::process(command, body).await?,
AdminCommand::Query(command) => query::process(command, body).await?,
AdminCommand::Fsck(command) => fsck::process(command, body).await?,
AdminCommand::Tester(command) => tester::process(command, body).await?,
};
Ok(reply_message_content)
}
// Utility to turn clap's `--help` text to HTML.
fn usage_to_html(text: &str, server_name: &ServerName) -> String {
// Replace `@conduit:servername:-subcmdname` with `@conduit:servername:
// subcmdname`
let text = text.replace(&format!("@conduit:{server_name}:-"), &format!("@conduit:{server_name}: "));
// For the conduit admin room, subcommands become main commands
let text = text.replace("SUBCOMMAND", "COMMAND");
let text = text.replace("subcommand", "command");
// Escape option names (e.g. `<element-id>`) since they look like HTML tags
let text = escape_html(&text);
// Italicize the first line (command name and version text)
let re = Regex::new("^(.*?)\n").expect("Regex compilation should not fail");
let text = re.replace_all(&text, "<em>$1</em>\n");
// Unmerge wrapped lines
let text = text.replace("\n ", " ");
// Wrap option names in backticks. The lines look like:
// -V, --version Prints version information
// And are converted to:
// <code>-V, --version</code>: Prints version information
// (?m) enables multi-line mode for ^ and $
let re = Regex::new("(?m)^ {4}(([a-zA-Z_&;-]+(, )?)+) +(.*)$").expect("Regex compilation should not fail");
let text = re.replace_all(&text, "<code>$1</code>: $4");
// Look for a `[commandbody]` tag. If it exists, use all lines below it that
// start with a `#` in the USAGE section.
let mut text_lines = text.lines().collect::<Vec<&str>>();
let mut command_body = String::new();
if let Some(line_index) = text_lines.iter().position(|line| *line == "[commandbody]") {
text_lines.remove(line_index);
while text_lines
.get(line_index)
.is_some_and(|line| line.starts_with('#'))
{
command_body += if text_lines[line_index].starts_with("# ") {
&text_lines[line_index][2..]
} else {
&text_lines[line_index][1..]
};
command_body += "[nobr]\n";
text_lines.remove(line_index);
}
}
let text = text_lines.join("\n");
// Improve the usage section
let text = if command_body.is_empty() {
// Wrap the usage line in code tags
let re = Regex::new("(?m)^USAGE:\n {4}(@conduit:.*)$").expect("Regex compilation should not fail");
re.replace_all(&text, "USAGE:\n<code>$1</code>").to_string()
} else {
// Wrap the usage line in a code block, and add a yaml block example
// This makes the usage of e.g. `register-appservice` more accurate
let re = Regex::new("(?m)^USAGE:\n {4}(.*?)\n\n").expect("Regex compilation should not fail");
re.replace_all(&text, "USAGE:\n<pre>$1[nobr]\n[commandbodyblock]</pre>")
.replace("[commandbodyblock]", &command_body)
};
// Add HTML line-breaks
text.replace("\n\n\n", "\n\n")
.replace('\n', "<br>\n")
.replace("[nobr]<br>", "")
}

View File

@@ -1,7 +1,7 @@
use ruma::{events::room::message::RoomMessageEventContent, EventId};
use ruma::{events::room::message::RoomMessageEventContent, EventId, MxcUri};
use tracing::{debug, info};
use crate::{service::admin::MxcUri, services, Result};
use crate::{services, Result};
pub(crate) async fn delete(
_body: Vec<&str>, mxc: Option<Box<MxcUri>>, event_id: Option<Box<EventId>>,
@@ -23,7 +23,7 @@ pub(crate) async fn delete(
debug!("Got event ID to delete media from: {event_id}");
let mut mxc_urls = vec![];
let mut mxc_deletion_count = 0;
let mut mxc_deletion_count: usize = 0;
// parsing the PDU for any MXC URLs begins here
if let Some(event_json) = services().rooms.timeline.get_pdu_json(&event_id)? {
@@ -123,7 +123,7 @@ pub(crate) async fn delete(
for mxc_url in mxc_urls {
services().media.delete(mxc_url).await?;
mxc_deletion_count += 1;
mxc_deletion_count = mxc_deletion_count.saturating_add(1);
}
return Ok(RoomMessageEventContent::text_plain(format!(
@@ -138,36 +138,38 @@ pub(crate) async fn delete(
}
pub(crate) async fn delete_list(body: Vec<&str>) -> Result<RoomMessageEventContent> {
if body.len() > 2 && body[0].trim().starts_with("```") && body.last().unwrap().trim() == "```" {
let mxc_list = body
.clone()
.drain(1..body.len().checked_sub(1).unwrap())
.collect::<Vec<_>>();
let mut mxc_deletion_count: usize = 0;
for mxc in mxc_list {
debug!("Deleting MXC {mxc} in bulk");
services().media.delete(mxc.to_owned()).await?;
mxc_deletion_count = mxc_deletion_count
.checked_add(1)
.expect("mxc_deletion_count should not get this high");
}
return Ok(RoomMessageEventContent::text_plain(format!(
"Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our database and the filesystem.",
)));
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
))
let mxc_list = body
.clone()
.drain(1..body.len().checked_sub(1).unwrap())
.collect::<Vec<_>>();
let mut mxc_deletion_count: usize = 0;
for mxc in mxc_list {
debug!("Deleting MXC {mxc} in bulk");
services().media.delete(mxc.to_owned()).await?;
mxc_deletion_count = mxc_deletion_count
.checked_add(1)
.expect("mxc_deletion_count should not get this high");
}
Ok(RoomMessageEventContent::text_plain(format!(
"Finished bulk MXC deletion, deleted {mxc_deletion_count} total MXCs from our database and the filesystem.",
)))
}
pub(crate) async fn delete_past_remote_media(_body: Vec<&str>, duration: String) -> Result<RoomMessageEventContent> {
pub(crate) async fn delete_past_remote_media(
_body: Vec<&str>, duration: String, force: bool,
) -> Result<RoomMessageEventContent> {
let deleted_count = services()
.media
.delete_all_remote_media_at_after_time(duration)
.delete_all_remote_media_at_after_time(duration, force)
.await?;
Ok(RoomMessageEventContent::text_plain(format!(

View File

@@ -1,8 +1,8 @@
use clap::Subcommand;
use ruma::{events::room::message::RoomMessageEventContent, EventId};
use ruma::{events::room::message::RoomMessageEventContent, EventId, MxcUri};
use self::media_commands::{delete, delete_list, delete_past_remote_media};
use crate::{service::admin::MxcUri, Result};
use crate::Result;
pub(crate) mod media_commands;
@@ -32,6 +32,9 @@ pub(crate) enum MediaCommand {
/// - The duration (at or after), e.g. "5m" to delete all media in the
/// past 5 minutes
duration: String,
/// Continues deleting remote media if an undeletable object is found
#[arg(short, long)]
force: bool,
},
}
@@ -44,6 +47,7 @@ pub(crate) async fn process(command: MediaCommand, body: Vec<&str>) -> Result<Ro
MediaCommand::DeleteList => delete_list(body).await?,
MediaCommand::DeletePastRemoteMedia {
duration,
} => delete_past_remote_media(body, duration).await?,
force,
} => delete_past_remote_media(body, duration, force).await?,
})
}

55
src/admin/mod.rs Normal file
View File

@@ -0,0 +1,55 @@
pub(crate) mod appservice;
pub(crate) mod debug;
pub(crate) mod federation;
pub(crate) mod fsck;
pub(crate) mod handler;
pub(crate) mod media;
pub(crate) mod query;
pub(crate) mod room;
pub(crate) mod server;
pub(crate) mod tester;
pub(crate) mod user;
pub(crate) mod utils;
extern crate conduit_api as api;
extern crate conduit_core as conduit;
extern crate conduit_service as service;
pub(crate) use conduit::{mod_ctor, mod_dtor, Result};
pub use handler::handle;
pub(crate) use service::{services, user_is_local};
pub(crate) use crate::{
handler::Service,
utils::{escape_html, get_room_info},
};
mod_ctor! {}
mod_dtor! {}
#[cfg(test)]
mod test {
use clap::Parser;
use crate::handler::AdminCommand;
#[test]
fn get_help_short() { get_help_inner("-h"); }
#[test]
fn get_help_long() { get_help_inner("--help"); }
#[test]
fn get_help_subcommand() { get_help_inner("help"); }
fn get_help_inner(input: &str) {
let error = AdminCommand::try_parse_from(["argv[0] doesn't matter", input])
.unwrap_err()
.to_string();
// Search for a handful of keywords that suggest the help printed properly
assert!(error.contains("Usage:"));
assert!(error.contains("Commands:"));
assert!(error.contains("Options:"));
}
}

View File

@@ -19,11 +19,8 @@ pub(crate) async fn account_data(subcommand: AccountData) -> Result<RoomMessageE
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
AccountData::Get {
@@ -39,11 +36,8 @@ pub(crate) async fn account_data(subcommand: AccountData) -> Result<RoomMessageE
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
}

View File

@@ -17,11 +17,8 @@ pub(crate) async fn appservice(subcommand: Appservice) -> Result<RoomMessageEven
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
Appservice::All => {
@@ -30,11 +27,8 @@ pub(crate) async fn appservice(subcommand: Appservice) -> Result<RoomMessageEven
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
}

View File

@@ -12,11 +12,8 @@ pub(crate) async fn globals(subcommand: Globals) -> Result<RoomMessageEventConte
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
Globals::CurrentCount => {
@@ -25,11 +22,8 @@ pub(crate) async fn globals(subcommand: Globals) -> Result<RoomMessageEventConte
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
Globals::LastCheckForUpdatesId => {
@@ -38,11 +32,8 @@ pub(crate) async fn globals(subcommand: Globals) -> Result<RoomMessageEventConte
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
Globals::LoadKeypair => {
@@ -51,11 +42,8 @@ pub(crate) async fn globals(subcommand: Globals) -> Result<RoomMessageEventConte
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
Globals::SigningKeysFor {
@@ -66,11 +54,8 @@ pub(crate) async fn globals(subcommand: Globals) -> Result<RoomMessageEventConte
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
}

View File

@@ -3,10 +3,12 @@
pub(crate) mod globals;
pub(crate) mod presence;
pub(crate) mod room_alias;
pub(crate) mod room_state_cache;
pub(crate) mod sending;
pub(crate) mod users;
use clap::Subcommand;
use room_state_cache::room_state_cache;
use ruma::{
events::{room::message::RoomMessageEventContent, RoomAccountDataEventType},
RoomAliasId, RoomId, ServerName, UserId,
@@ -38,6 +40,10 @@ pub(crate) enum QueryCommand {
#[command(subcommand)]
RoomAlias(RoomAlias),
/// - rooms/state_cache iterators and getters
#[command(subcommand)]
RoomStateCache(RoomStateCache),
/// - globals.rs iterators and getters
#[command(subcommand)]
Globals(Globals),
@@ -127,6 +133,78 @@ pub(crate) enum RoomAlias {
AllLocalAliases,
}
#[cfg_attr(test, derive(Debug))]
#[derive(Subcommand)]
pub(crate) enum RoomStateCache {
ServerInRoom {
server: Box<ServerName>,
room_id: Box<RoomId>,
},
RoomServers {
room_id: Box<RoomId>,
},
ServerRooms {
server: Box<ServerName>,
},
RoomMembers {
room_id: Box<RoomId>,
},
LocalUsersInRoom {
room_id: Box<RoomId>,
},
ActiveLocalUsersInRoom {
room_id: Box<RoomId>,
},
RoomJoinedCount {
room_id: Box<RoomId>,
},
RoomInvitedCount {
room_id: Box<RoomId>,
},
RoomUserOnceJoined {
room_id: Box<RoomId>,
},
RoomMembersInvited {
room_id: Box<RoomId>,
},
GetInviteCount {
room_id: Box<RoomId>,
user_id: Box<UserId>,
},
GetLeftCount {
room_id: Box<RoomId>,
user_id: Box<UserId>,
},
RoomsJoined {
user_id: Box<UserId>,
},
RoomsLeft {
user_id: Box<UserId>,
},
RoomsInvited {
user_id: Box<UserId>,
},
InviteState {
user_id: Box<UserId>,
room_id: Box<RoomId>,
},
}
#[cfg_attr(test, derive(Debug))]
#[derive(Subcommand)]
/// All the getters and iterators from src/database/key_value/globals.rs
@@ -216,6 +294,7 @@ pub(crate) async fn process(command: QueryCommand, _body: Vec<&str>) -> Result<R
QueryCommand::Appservice(command) => appservice(command).await?,
QueryCommand::Presence(command) => presence(command).await?,
QueryCommand::RoomAlias(command) => room_alias(command).await?,
QueryCommand::RoomStateCache(command) => room_state_cache(command).await?,
QueryCommand::Globals(command) => globals(command).await?,
QueryCommand::Sending(command) => sending(command).await?,
QueryCommand::Users(command) => users(command).await?,

View File

@@ -14,11 +14,8 @@ pub(crate) async fn presence(subcommand: Presence) -> Result<RoomMessageEventCon
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
Presence::PresenceSince {
@@ -31,11 +28,8 @@ pub(crate) async fn presence(subcommand: Presence) -> Result<RoomMessageEventCon
let presence_since: Vec<(_, _, _)> = results.collect();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", presence_since),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
presence_since
),
format!("Query completed in {query_time:?}:\n\n```\n{presence_since:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{presence_since:?}\n</code></pre>"),
))
},
}

View File

@@ -14,11 +14,8 @@ pub(crate) async fn room_alias(subcommand: RoomAlias) -> Result<RoomMessageEvent
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomAlias::LocalAliasesForRoom {
@@ -31,11 +28,8 @@ pub(crate) async fn room_alias(subcommand: RoomAlias) -> Result<RoomMessageEvent
let aliases: Vec<_> = results.collect();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", aliases),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
aliases
),
format!("Query completed in {query_time:?}:\n\n```\n{aliases:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{aliases:?}\n</code></pre>"),
))
},
RoomAlias::AllLocalAliases => {
@@ -46,11 +40,8 @@ pub(crate) async fn room_alias(subcommand: RoomAlias) -> Result<RoomMessageEvent
let aliases: Vec<_> = results.collect();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", aliases),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
aliases
),
format!("Query completed in {query_time:?}:\n\n```\n{aliases:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{aliases:?}\n</code></pre>"),
))
},
}

View File

@@ -0,0 +1,249 @@
use ruma::events::room::message::RoomMessageEventContent;
use super::RoomStateCache;
use crate::{services, Result};
pub(crate) async fn room_state_cache(subcommand: RoomStateCache) -> Result<RoomMessageEventContent> {
match subcommand {
RoomStateCache::ServerInRoom {
server,
room_id,
} => {
let timer = tokio::time::Instant::now();
let result = services()
.rooms
.state_cache
.server_in_room(&server, &room_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{result:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{result:?}\n</code></pre>"),
))
},
RoomStateCache::RoomServers {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.room_servers(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::ServerRooms {
server,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services().rooms.state_cache.server_rooms(&server).collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::RoomMembers {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.room_members(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::LocalUsersInRoom {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services()
.rooms
.state_cache
.local_users_in_room(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::ActiveLocalUsersInRoom {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Vec<_> = services()
.rooms
.state_cache
.active_local_users_in_room(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::RoomJoinedCount {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results = services().rooms.state_cache.room_joined_count(&room_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::RoomInvitedCount {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results = services().rooms.state_cache.room_invited_count(&room_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::RoomUserOnceJoined {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.room_useroncejoined(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::RoomMembersInvited {
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.room_members_invited(&room_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::GetInviteCount {
room_id,
user_id,
} => {
let timer = tokio::time::Instant::now();
let results = services()
.rooms
.state_cache
.get_invite_count(&room_id, &user_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::GetLeftCount {
room_id,
user_id,
} => {
let timer = tokio::time::Instant::now();
let results = services()
.rooms
.state_cache
.get_left_count(&room_id, &user_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::RoomsJoined {
user_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.rooms_joined(&user_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::RoomsInvited {
user_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services()
.rooms
.state_cache
.rooms_invited(&user_id)
.collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::RoomsLeft {
user_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services().rooms.state_cache.rooms_left(&user_id).collect();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
RoomStateCache::InviteState {
user_id,
room_id,
} => {
let timer = tokio::time::Instant::now();
let results = services()
.rooms
.state_cache
.invite_state(&user_id, &room_id);
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
}
}

View File

@@ -14,11 +14,8 @@ pub(crate) async fn sending(subcommand: Sending) -> Result<RoomMessageEventConte
let active_requests: Result<Vec<(_, _, _)>> = results.collect();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", active_requests),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
active_requests
),
format!("Query completed in {query_time:?}:\n\n```\n{active_requests:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{active_requests:?}\n</code></pre>"),
))
},
Sending::QueuedRequests {
@@ -96,11 +93,8 @@ pub(crate) async fn sending(subcommand: Sending) -> Result<RoomMessageEventConte
let queued_requests = results.collect::<Result<Vec<(_, _)>>>();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", queued_requests),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
queued_requests
),
format!("Query completed in {query_time:?}:\n\n```\n{queued_requests:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{queued_requests:?}\n</code></pre>"),
))
},
Sending::ActiveRequestsFor {
@@ -178,11 +172,8 @@ pub(crate) async fn sending(subcommand: Sending) -> Result<RoomMessageEventConte
let active_requests = results.collect::<Result<Vec<(_, _)>>>();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", active_requests),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
active_requests
),
format!("Query completed in {query_time:?}:\n\n```\n{active_requests:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{active_requests:?}\n</code></pre>"),
))
},
Sending::GetLatestEduCount {
@@ -193,11 +184,8 @@ pub(crate) async fn sending(subcommand: Sending) -> Result<RoomMessageEventConte
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
results
),
format!("Query completed in {query_time:?}:\n\n```\n{results:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{results:?}\n</code></pre>"),
))
},
}

View File

@@ -14,11 +14,8 @@ pub(crate) async fn users(subcommand: Users) -> Result<RoomMessageEventContent>
let users = results.collect::<Vec<_>>();
Ok(RoomMessageEventContent::text_html(
format!("Query completed in {query_time:?}:\n\n```\n{:?}```", users),
format!(
"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
users
),
format!("Query completed in {query_time:?}:\n\n```\n{users:?}```"),
format!("<p>Query completed in {query_time:?}:</p>\n<pre><code>{users:?}\n</code></pre>"),
))
},
}

View File

@@ -7,6 +7,7 @@
pub(crate) mod room_alias_commands;
pub(crate) mod room_commands;
pub(crate) mod room_directory_commands;
pub(crate) mod room_info_commands;
pub(crate) mod room_moderation_commands;
#[cfg_attr(test, derive(Debug))]
@@ -17,6 +18,10 @@ pub(crate) enum RoomCommand {
page: Option<usize>,
},
#[command(subcommand)]
/// - View information about a room we know about
Info(RoomInfoCommand),
#[command(subcommand)]
/// - Manage moderation of remote or local rooms
Moderation(RoomModerationCommand),
@@ -30,6 +35,23 @@ pub(crate) enum RoomCommand {
Directory(RoomDirectoryCommand),
}
#[cfg_attr(test, derive(Debug))]
#[derive(Subcommand)]
pub(crate) enum RoomInfoCommand {
/// - List joined members in a room
ListJoinedMembers {
room_id: Box<RoomId>,
},
/// - Displays room topic
///
/// Room topics can be huge, so this is in its
/// own separate command
ViewRoomTopic {
room_id: Box<RoomId>,
},
}
#[cfg_attr(test, derive(Debug))]
#[derive(Subcommand)]
pub(crate) enum RoomAliasCommand {
@@ -46,7 +68,7 @@ pub(crate) enum RoomAliasCommand {
room_alias_localpart: String,
},
/// - Remove an alias
/// - Remove a local alias
Remove {
/// The alias localpart to remove (`alias`, not `#alias:servername.tld`)
room_alias_localpart: String,
@@ -147,6 +169,8 @@ pub(crate) enum RoomModerationCommand {
pub(crate) async fn process(command: RoomCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
Ok(match command {
RoomCommand::Info(command) => room_info_commands::process(command, body).await?,
RoomCommand::Alias(command) => room_alias_commands::process(command, body).await?,
RoomCommand::Directory(command) => room_directory_commands::process(command, body).await?,

View File

@@ -1,11 +1,13 @@
use std::fmt::Write as _;
use std::fmt::Write;
use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId};
use super::RoomAliasCommand;
use crate::{service::admin::escape_html, services, Result};
use crate::{escape_html, services, Result};
pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
let server_user = &services().globals.server_user;
match command {
RoomAliasCommand::Set {
ref room_alias_localpart,
@@ -20,7 +22,7 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu
let room_alias_str = format!("#{}:{}", room_alias_localpart, services().globals.server_name());
let room_alias = match RoomAliasId::parse_box(room_alias_str) {
Ok(alias) => alias,
Err(err) => return Ok(RoomMessageEventContent::text_plain(format!("Failed to parse alias: {}", err))),
Err(err) => return Ok(RoomMessageEventContent::text_plain(format!("Failed to parse alias: {err}"))),
};
match command {
RoomAliasCommand::Set {
@@ -28,18 +30,24 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu
room_id,
..
} => match (force, services().rooms.alias.resolve_local_alias(&room_alias)) {
(true, Ok(Some(id))) => match services().rooms.alias.set_alias(&room_alias, &room_id) {
(true, Ok(Some(id))) => match services()
.rooms
.alias
.set_alias(&room_alias, &room_id, server_user)
{
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!(
"Successfully overwrote alias (formerly {})",
id
"Successfully overwrote alias (formerly {id})"
))),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {}", err))),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))),
},
(false, Ok(Some(id))) => Ok(RoomMessageEventContent::text_plain(format!(
"Refusing to overwrite in use alias for {}, use -f or --force to overwrite",
id
"Refusing to overwrite in use alias for {id}, use -f or --force to overwrite"
))),
(_, Ok(None)) => match services().rooms.alias.set_alias(&room_alias, &room_id) {
(_, Ok(None)) => match services()
.rooms
.alias
.set_alias(&room_alias, &room_id, server_user)
{
Ok(()) => Ok(RoomMessageEventContent::text_plain("Successfully set alias")),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))),
},
@@ -48,19 +56,24 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu
RoomAliasCommand::Remove {
..
} => match services().rooms.alias.resolve_local_alias(&room_alias) {
Ok(Some(id)) => match services().rooms.alias.remove_alias(&room_alias) {
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!("Removed alias from {}", id))),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {}", err))),
Ok(Some(id)) => match services()
.rooms
.alias
.remove_alias(&room_alias, server_user)
.await
{
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!("Removed alias from {id}"))),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))),
},
Ok(None) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {}", err))),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {err}"))),
},
RoomAliasCommand::Which {
..
} => match services().rooms.alias.resolve_local_alias(&room_alias) {
Ok(Some(id)) => Ok(RoomMessageEventContent::text_plain(format!("Alias resolves to {}", id))),
Ok(Some(id)) => Ok(RoomMessageEventContent::text_plain(format!("Alias resolves to {id}"))),
Ok(None) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {}", err))),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {err}"))),
},
RoomAliasCommand::List {
..
@@ -79,12 +92,13 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu
match aliases {
Ok(aliases) => {
let plain_list = aliases.iter().fold(String::new(), |mut output, alias| {
writeln!(output, "- {alias}").unwrap();
writeln!(output, "- {alias}").expect("should be able to write to string buffer");
output
});
let html_list = aliases.iter().fold(String::new(), |mut output, alias| {
writeln!(output, "<li>{}</li>", escape_html(alias.as_ref())).unwrap();
writeln!(output, "<li>{}</li>", escape_html(alias.as_ref()))
.expect("should be able to write to string buffer");
output
});
@@ -92,7 +106,7 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu
let html = format!("Aliases for {room_id}:\n<ul>{html_list}</ul>");
Ok(RoomMessageEventContent::text_html(plain, html))
},
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to list aliases: {}", err))),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to list aliases: {err}"))),
}
} else {
let aliases = services()
@@ -106,7 +120,8 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu
let plain_list = aliases
.iter()
.fold(String::new(), |mut output, (alias, id)| {
writeln!(output, "- `{alias}` -> #{id}:{server_name}").unwrap();
writeln!(output, "- `{alias}` -> #{id}:{server_name}")
.expect("should be able to write to string buffer");
output
});
@@ -120,7 +135,7 @@ pub(crate) async fn process(command: RoomAliasCommand, _body: Vec<&str>) -> Resu
escape_html(id.as_ref()),
server_name
)
.unwrap();
.expect("should be able to write to string buffer");
output
});

View File

@@ -1,11 +1,8 @@
use std::fmt::Write as _;
use std::fmt::Write;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
use crate::{
service::admin::{escape_html, get_room_info, PAGE_SIZE},
services, Result,
};
use crate::{escape_html, get_room_info, handler::PAGE_SIZE, services, Result};
pub(crate) async fn list(_body: Vec<&str>, page: Option<usize>) -> Result<RoomMessageEventContent> {
// TODO: i know there's a way to do this with clap, but i can't seem to find it
@@ -51,7 +48,7 @@ pub(crate) async fn list(_body: Vec<&str>, page: Option<usize>) -> Result<RoomMe
members,
escape_html(name)
)
.unwrap();
.expect("should be able to write to string buffer");
output
})
);

View File

@@ -1,12 +1,9 @@
use std::fmt::Write as _;
use std::fmt::Write;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
use super::RoomDirectoryCommand;
use crate::{
service::admin::{escape_html, get_room_info, PAGE_SIZE},
services, Result,
};
use crate::{escape_html, get_room_info, handler::PAGE_SIZE, services, Result};
pub(crate) async fn process(command: RoomDirectoryCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
match command {
@@ -68,7 +65,7 @@ pub(crate) async fn process(command: RoomDirectoryCommand, _body: Vec<&str>) ->
members,
escape_html(name.as_ref())
)
.unwrap();
.expect("should be able to write to string buffer");
output
})
);

View File

@@ -0,0 +1,93 @@
use std::fmt::Write;
use ruma::{events::room::message::RoomMessageEventContent, RoomId};
use service::services;
use super::RoomInfoCommand;
use crate::{escape_html, Result};
pub(crate) async fn process(command: RoomInfoCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
match command {
RoomInfoCommand::ListJoinedMembers {
room_id,
} => list_joined_members(body, room_id).await,
RoomInfoCommand::ViewRoomTopic {
room_id,
} => view_room_topic(body, room_id).await,
}
}
async fn list_joined_members(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
let room_name = services()
.rooms
.state_accessor
.get_name(&room_id)
.ok()
.flatten()
.unwrap_or_else(|| room_id.to_string());
let members = services()
.rooms
.state_cache
.room_members(&room_id)
.filter_map(Result::ok);
let member_info = members
.into_iter()
.map(|user_id| {
(
user_id.clone(),
services()
.users
.displayname(&user_id)
.unwrap_or(None)
.unwrap_or_else(|| user_id.to_string()),
)
})
.collect::<Vec<_>>();
let output_plain = format!(
"{} Members in Room \"{}\":\n```\n{}\n```",
member_info.len(),
room_name,
member_info
.iter()
.map(|(mxid, displayname)| format!("{mxid} | {displayname}"))
.collect::<Vec<_>>()
.join("\n")
);
let output_html = format!(
"<table><caption>{} Members in Room \"{}\" </caption>\n<tr><th>MXID</th>\t<th>Display \
Name</th></tr>\n{}</table>",
member_info.len(),
room_name,
member_info
.iter()
.fold(String::new(), |mut output, (mxid, displayname)| {
writeln!(
output,
"<tr><td>{}</td>\t<td>{}</td></tr>",
mxid,
escape_html(displayname.as_ref())
)
.expect("should be able to write to string buffer");
output
})
);
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
}
async fn view_room_topic(_body: Vec<&str>, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
let Some(room_topic) = services().rooms.state_accessor.get_room_topic(&room_id)? else {
return Ok(RoomMessageEventContent::text_plain("Room does not have a room topic set."));
};
let output_html = format!("<p>Room topic:</p>\n<hr>\n{}<hr>", escape_html(&room_topic));
Ok(RoomMessageEventContent::text_html(
format!("Room topic:\n\n```{room_topic}\n```"),
output_html,
))
}

View File

@@ -0,0 +1,522 @@
use std::fmt::Write;
use api::client::{get_alias_helper, leave_room};
use ruma::{
events::room::message::RoomMessageEventContent, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId,
};
use tracing::{debug, error, info, warn};
use super::{super::Service, RoomModerationCommand};
use crate::{escape_html, get_room_info, services, user_is_local, Result};
pub(crate) async fn process(command: RoomModerationCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
match command {
RoomModerationCommand::BanRoom {
force,
room,
disable_federation,
} => ban_room(body, force, room, disable_federation).await,
RoomModerationCommand::BanListOfRooms {
force,
disable_federation,
} => ban_list_of_rooms(body, force, disable_federation).await,
RoomModerationCommand::UnbanRoom {
room,
enable_federation,
} => unban_room(body, room, enable_federation).await,
RoomModerationCommand::ListBannedRooms => list_banned_rooms(body).await,
}
}
async fn ban_room(
_body: Vec<&str>, force: bool, room: Box<RoomOrAliasId>, disable_federation: bool,
) -> Result<RoomMessageEventContent> {
debug!("Got room alias or ID: {}", room);
let admin_room_alias = &services().globals.admin_alias;
if let Some(admin_room_id) = Service::get_admin_room()? {
if room.to_string().eq(&admin_room_id) || room.to_string().eq(admin_room_alias) {
return Ok(RoomMessageEventContent::text_plain("Not allowed to ban the admin room."));
}
}
let room_id = if room.is_room_id() {
let room_id = match RoomId::parse(&room) {
Ok(room_id) => room_id,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to parse room ID {room}. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
)))
},
};
debug!("Room specified is a room ID, banning room ID");
services().rooms.metadata.ban_room(&room_id, true)?;
room_id
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
Ok(room_alias) => room_alias,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to parse room ID {room}. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
)))
},
};
debug!(
"Room specified is not a room ID, attempting to resolve room alias to a room ID locally, if not using \
get_alias_helper to fetch room ID remotely"
);
let room_id = if let Some(room_id) = services().rooms.alias.resolve_local_alias(&room_alias)? {
room_id
} else {
debug!("We don't have this room alias to a room ID locally, attempting to fetch room ID over federation");
match get_alias_helper(room_alias, None).await {
Ok(response) => {
debug!("Got federation response fetching room ID for room {room}: {:?}", response);
response.room_id
},
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to resolve room alias {room} to a room ID: {e}"
)));
},
}
};
services().rooms.metadata.ban_room(&room_id, true)?;
room_id
} else {
return Ok(RoomMessageEventContent::text_plain(
"Room specified is not a room ID or room alias. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`)",
));
};
debug!("Making all users leave the room {}", &room);
if force {
for local_user in services()
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
user_is_local(local_user)
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (user_is_local(local_user)
&& services()
.users
.is_admin(local_user)
.unwrap_or(true)) // since this is a force
// operation, assume user
// is an admin if somehow
// this fails
})
})
.collect::<Vec<OwnedUserId>>()
{
debug!(
"Attempting leave for user {} in room {} (forced, ignoring all errors, evicting admins too)",
&local_user, &room_id
);
if let Err(e) = leave_room(&local_user, &room_id, None).await {
warn!(%e, "Failed to leave room");
}
}
} else {
for local_user in services()
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == services().globals.server_name()
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (local_user.server_name()
== services().globals.server_name()
&& !services()
.users
.is_admin(local_user)
.unwrap_or(false))
})
})
.collect::<Vec<OwnedUserId>>()
{
debug!("Attempting leave for user {} in room {}", &local_user, &room_id);
if let Err(e) = leave_room(&local_user, &room_id, None).await {
error!(
"Error attempting to make local user {} leave room {} during room banning: {}",
&local_user, &room_id, e
);
return Ok(RoomMessageEventContent::text_plain(format!(
"Error attempting to make local user {} leave room {} during room banning (room is still banned \
but not removing any more users): {}\nIf you would like to ignore errors, use --force",
&local_user, &room_id, e
)));
}
}
}
if disable_federation {
services().rooms.metadata.disable_room(&room_id, true)?;
return Ok(RoomMessageEventContent::text_plain(
"Room banned, removed all our local users, and disabled incoming federation with room.",
));
}
Ok(RoomMessageEventContent::text_plain(
"Room banned and removed all our local users, use `!admin federation disable-room` to stop receiving new \
inbound federation events as well if needed.",
))
}
async fn ban_list_of_rooms(body: Vec<&str>, force: bool, disable_federation: bool) -> Result<RoomMessageEventContent> {
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
let rooms_s = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
let admin_room_alias = &services().globals.admin_alias;
let mut room_ban_count: usize = 0;
let mut room_ids: Vec<OwnedRoomId> = Vec::new();
for &room in &rooms_s {
match <&RoomOrAliasId>::try_from(room) {
Ok(room_alias_or_id) => {
if let Some(admin_room_id) = Service::get_admin_room()? {
if room.to_owned().eq(&admin_room_id) || room.to_owned().eq(admin_room_alias) {
info!("User specified admin room in bulk ban list, ignoring");
continue;
}
}
if room_alias_or_id.is_room_id() {
let room_id = match RoomId::parse(room_alias_or_id) {
Ok(room_id) => room_id,
Err(e) => {
if force {
// ignore rooms we failed to parse if we're force banning
warn!(
"Error parsing room \"{room}\" during bulk room banning, ignoring error and \
logging here: {e}"
);
continue;
}
return Ok(RoomMessageEventContent::text_plain(format!(
"{room} is not a valid room ID or room alias, please fix the list and try again: {e}"
)));
},
};
room_ids.push(room_id);
}
if room_alias_or_id.is_room_alias_id() {
match RoomAliasId::parse(room_alias_or_id) {
Ok(room_alias) => {
let room_id =
if let Some(room_id) = services().rooms.alias.resolve_local_alias(&room_alias)? {
room_id
} else {
debug!(
"We don't have this room alias to a room ID locally, attempting to fetch room \
ID over federation"
);
match get_alias_helper(room_alias, None).await {
Ok(response) => {
debug!(
"Got federation response fetching room ID for room {room}: {:?}",
response
);
response.room_id
},
Err(e) => {
// don't fail if force blocking
if force {
warn!("Failed to resolve room alias {room} to a room ID: {e}");
continue;
}
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to resolve room alias {room} to a room ID: {e}"
)));
},
}
};
room_ids.push(room_id);
},
Err(e) => {
if force {
// ignore rooms we failed to parse if we're force deleting
error!(
"Error parsing room \"{room}\" during bulk room banning, ignoring error and \
logging here: {e}"
);
continue;
}
return Ok(RoomMessageEventContent::text_plain(format!(
"{room} is not a valid room ID or room alias, please fix the list and try again: {e}"
)));
},
}
}
},
Err(e) => {
if force {
// ignore rooms we failed to parse if we're force deleting
error!(
"Error parsing room \"{room}\" during bulk room banning, ignoring error and logging here: {e}"
);
continue;
}
return Ok(RoomMessageEventContent::text_plain(format!(
"{room} is not a valid room ID or room alias, please fix the list and try again: {e}"
)));
},
}
}
for room_id in room_ids {
if services().rooms.metadata.ban_room(&room_id, true).is_ok() {
debug!("Banned {room_id} successfully");
room_ban_count = room_ban_count.saturating_add(1);
}
debug!("Making all users leave the room {}", &room_id);
if force {
for local_user in services()
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == services().globals.server_name()
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (local_user.server_name()
== services().globals.server_name()
&& services()
.users
.is_admin(local_user)
.unwrap_or(true)) // since this is a
// force operation,
// assume user is
// an admin if
// somehow this
// fails
})
})
.collect::<Vec<OwnedUserId>>()
{
debug!(
"Attempting leave for user {} in room {} (forced, ignoring all errors, evicting admins too)",
&local_user, room_id
);
if let Err(e) = leave_room(&local_user, &room_id, None).await {
warn!(%e, "Failed to leave room");
}
}
} else {
for local_user in services()
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == services().globals.server_name()
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (local_user.server_name()
== services().globals.server_name()
&& !services()
.users
.is_admin(local_user)
.unwrap_or(false))
})
})
.collect::<Vec<OwnedUserId>>()
{
debug!("Attempting leave for user {} in room {}", &local_user, &room_id);
if let Err(e) = leave_room(&local_user, &room_id, None).await {
error!(
"Error attempting to make local user {} leave room {} during bulk room banning: {}",
&local_user, &room_id, e
);
return Ok(RoomMessageEventContent::text_plain(format!(
"Error attempting to make local user {} leave room {} during room banning (room is still \
banned but not removing any more users and not banning any more rooms): {}\nIf you would \
like to ignore errors, use --force",
&local_user, &room_id, e
)));
}
}
}
if disable_federation {
services().rooms.metadata.disable_room(&room_id, true)?;
}
}
if disable_federation {
Ok(RoomMessageEventContent::text_plain(format!(
"Finished bulk room ban, banned {room_ban_count} total rooms, evicted all users, and disabled incoming \
federation with the room."
)))
} else {
Ok(RoomMessageEventContent::text_plain(format!(
"Finished bulk room ban, banned {room_ban_count} total rooms and evicted all users."
)))
}
}
async fn unban_room(
_body: Vec<&str>, room: Box<RoomOrAliasId>, enable_federation: bool,
) -> Result<RoomMessageEventContent> {
let room_id = if room.is_room_id() {
let room_id = match RoomId::parse(&room) {
Ok(room_id) => room_id,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to parse room ID {room}. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
)))
},
};
debug!("Room specified is a room ID, unbanning room ID");
services().rooms.metadata.ban_room(&room_id, false)?;
room_id
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
Ok(room_alias) => room_alias,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to parse room ID {room}. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`): {e}"
)))
},
};
debug!(
"Room specified is not a room ID, attempting to resolve room alias to a room ID locally, if not using \
get_alias_helper to fetch room ID remotely"
);
let room_id = if let Some(room_id) = services().rooms.alias.resolve_local_alias(&room_alias)? {
room_id
} else {
debug!("We don't have this room alias to a room ID locally, attempting to fetch room ID over federation");
match get_alias_helper(room_alias, None).await {
Ok(response) => {
debug!("Got federation response fetching room ID for room {room}: {:?}", response);
response.room_id
},
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to resolve room alias {room} to a room ID: {e}"
)));
},
}
};
services().rooms.metadata.ban_room(&room_id, false)?;
room_id
} else {
return Ok(RoomMessageEventContent::text_plain(
"Room specified is not a room ID or room alias. Please note that this requires a full room ID \
(`!awIh6gGInaS5wLQJwa:example.com`) or a room alias (`#roomalias:example.com`)",
));
};
if enable_federation {
services().rooms.metadata.disable_room(&room_id, false)?;
return Ok(RoomMessageEventContent::text_plain("Room unbanned."));
}
Ok(RoomMessageEventContent::text_plain(
"Room unbanned, you may need to re-enable federation with the room using enable-room if this is a remote room \
to make it fully functional.",
))
}
async fn list_banned_rooms(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
let rooms = services()
.rooms
.metadata
.list_banned_rooms()
.collect::<Result<Vec<_>, _>>();
match rooms {
Ok(room_ids) => {
if room_ids.is_empty() {
return Ok(RoomMessageEventContent::text_plain("No rooms are banned."));
}
let mut rooms = room_ids
.into_iter()
.map(|room_id| get_room_info(&room_id))
.collect::<Vec<_>>();
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let output_plain = format!(
"Rooms Banned ({}):\n```\n{}```",
rooms.len(),
rooms
.iter()
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
.collect::<Vec<_>>()
.join("\n")
);
let output_html = format!(
"<table><caption>Rooms Banned ({}) \
</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
rooms.len(),
rooms
.iter()
.fold(String::new(), |mut output, (id, members, name)| {
writeln!(
output,
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
id,
members,
escape_html(name.as_ref())
)
.expect("should be able to write to string buffer");
output
})
);
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
},
Err(e) => {
error!("Failed to list banned rooms: {}", e);
Ok(RoomMessageEventContent::text_plain(format!("Unable to list banned rooms: {e}")))
},
}
}

View File

@@ -4,7 +4,7 @@
pub(crate) async fn uptime(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
let seconds = services()
.globals
.server
.started
.elapsed()
.expect("standard duration")
@@ -28,7 +28,7 @@ pub(crate) async fn show_config(_body: Vec<&str>) -> Result<RoomMessageEventCont
pub(crate) async fn memory_usage(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
let response0 = services().memory_usage().await;
let response1 = services().globals.db.memory_usage();
let response2 = crate::alloc::memory_usage();
let response2 = conduit::alloc::memory_usage();
Ok(RoomMessageEventContent::text_plain(format!(
"Services:\n{response0}\n\nDatabase:\n{response1}\n{}",
@@ -69,12 +69,15 @@ pub(crate) async fn backup_database(_body: Vec<&str>) -> Result<RoomMessageEvent
));
}
let mut result = tokio::task::spawn_blocking(move || match services().globals.db.backup() {
Ok(()) => String::new(),
Err(e) => (*e).to_string(),
})
.await
.unwrap();
let mut result = services()
.server
.runtime()
.spawn_blocking(move || match services().globals.db.backup() {
Ok(()) => String::new(),
Err(e) => (*e).to_string(),
})
.await
.unwrap();
if result.is_empty() {
result = services().globals.db.backup_list()?;

View File

@@ -9,6 +9,6 @@ pub(crate) enum TesterCommands {
}
pub(crate) async fn process(command: TesterCommands, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
Ok(match command {
TesterCommands::Tester => RoomMessageEventContent::notice_plain(String::from("complete")),
TesterCommands::Tester => RoomMessageEventContent::notice_plain(String::from("completed")),
})
}

View File

@@ -1,7 +1,8 @@
pub(crate) mod user_commands;
use clap::Subcommand;
use ruma::events::room::message::RoomMessageEventContent;
use ruma::{events::room::message::RoomMessageEventContent, RoomId};
use user_commands::{delete_room_tag, get_room_tags, put_room_tag};
use self::user_commands::{create, deactivate, deactivate_all, list, list_joined_rooms, reset_password};
use crate::Result;
@@ -25,11 +26,11 @@ pub(crate) enum UserCommand {
/// - Deactivate a user
///
/// User will not be removed from all rooms by default.
/// Use --leave-rooms to force the user to leave all rooms
/// User will be removed from all rooms by default.
/// Use --no-leave-rooms to not leave all rooms by default.
Deactivate {
#[arg(short, long)]
leave_rooms: bool,
no_leave_rooms: bool,
user_id: String,
},
@@ -37,8 +38,10 @@ pub(crate) enum UserCommand {
///
/// Recommended to use in conjunction with list-local-users.
///
/// Users will not be removed from joined rooms by default.
/// Can be overridden with --leave-rooms flag.
/// Users will be removed from joined rooms by default.
///
/// Can be overridden with --no-leave-rooms.
///
/// Removing a mass amount of users from a room may cause a significant
/// amount of leave events. The time to leave rooms may depend significantly
/// on joined rooms and servers.
@@ -48,9 +51,9 @@ pub(crate) enum UserCommand {
DeactivateAll {
#[arg(short, long)]
/// Remove users from their joined rooms
leave_rooms: bool,
no_leave_rooms: bool,
#[arg(short, long)]
/// Also deactivate admin accounts
/// Also deactivate admin accounts and will assume leave all rooms too
force: bool,
},
@@ -62,6 +65,32 @@ pub(crate) enum UserCommand {
ListJoinedRooms {
user_id: String,
},
/// - Puts a room tag for the specified user and room ID.
///
/// This is primarily useful if you'd like to set your admin room
/// to the special "System Alerts" section in Element as a way to
/// permanently see your admin room without it being buried away in your
/// favourites or rooms. To do this, you would pass your user, your admin
/// room's internal ID, and the tag name `m.server_notice`.
PutRoomTag {
user_id: String,
room_id: Box<RoomId>,
tag: String,
},
/// - Deletes the room tag for the specified user and room ID
DeleteRoomTag {
user_id: String,
room_id: Box<RoomId>,
tag: String,
},
/// - Gets all the room tags for the specified user and room ID
GetRoomTags {
user_id: String,
room_id: Box<RoomId>,
},
}
pub(crate) async fn process(command: UserCommand, body: Vec<&str>) -> Result<RoomMessageEventContent> {
@@ -72,18 +101,32 @@ pub(crate) async fn process(command: UserCommand, body: Vec<&str>) -> Result<Roo
password,
} => create(body, username, password).await?,
UserCommand::Deactivate {
leave_rooms,
no_leave_rooms,
user_id,
} => deactivate(body, leave_rooms, user_id).await?,
} => deactivate(body, no_leave_rooms, user_id).await?,
UserCommand::ResetPassword {
username,
} => reset_password(body, username).await?,
UserCommand::DeactivateAll {
leave_rooms,
no_leave_rooms,
force,
} => deactivate_all(body, leave_rooms, force).await?,
} => deactivate_all(body, no_leave_rooms, force).await?,
UserCommand::ListJoinedRooms {
user_id,
} => list_joined_rooms(body, user_id).await?,
UserCommand::PutRoomTag {
user_id,
room_id,
tag,
} => put_room_tag(body, user_id, room_id, tag).await?,
UserCommand::DeleteRoomTag {
user_id,
room_id,
tag,
} => delete_room_tag(body, user_id, room_id, tag).await?,
UserCommand::GetRoomTags {
user_id,
room_id,
} => get_room_tags(body, user_id, room_id).await?,
})
}

View File

@@ -0,0 +1,427 @@
use std::{collections::BTreeMap, fmt::Write as _};
use api::client::{join_room_by_id_helper, leave_all_rooms, update_avatar_url, update_displayname};
use conduit::utils;
use ruma::{
events::{
room::message::RoomMessageEventContent,
tag::{TagEvent, TagEventContent, TagInfo},
RoomAccountDataEventType,
},
OwnedRoomId, OwnedUserId, RoomId,
};
use tracing::{error, info, warn};
use crate::{
escape_html, get_room_info, services,
utils::{parse_active_local_user_id, parse_local_user_id},
Result,
};
const AUTO_GEN_PASSWORD_LENGTH: usize = 25;
pub(crate) async fn list(_body: Vec<&str>) -> Result<RoomMessageEventContent> {
match services().users.list_local_users() {
Ok(users) => {
let mut plain_msg = format!("Found {} local user account(s):\n```\n", users.len());
plain_msg += &users.join("\n");
plain_msg += "\n```";
let mut html_msg = format!("<p>Found {} local user account(s):</p><pre><code>", users.len());
html_msg += &users.join("\n");
html_msg += "\n</code></pre>";
Ok(RoomMessageEventContent::text_html(&plain_msg, &html_msg))
},
Err(e) => Ok(RoomMessageEventContent::text_plain(e.to_string())),
}
}
pub(crate) async fn create(
_body: Vec<&str>, username: String, password: Option<String>,
) -> Result<RoomMessageEventContent> {
// Validate user id
let user_id = parse_local_user_id(&username)?;
if services().users.exists(&user_id)? {
return Ok(RoomMessageEventContent::text_plain(format!("Userid {user_id} already exists")));
}
let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
// Create user
services().users.create(&user_id, Some(password.as_str()))?;
// Default to pretty displayname
let mut displayname = user_id.localpart().to_owned();
// If `new_user_displayname_suffix` is set, registration will push whatever
// content is set to the user's display name with a space before it
if !services()
.globals
.config
.new_user_displayname_suffix
.is_empty()
{
write!(displayname, " {}", services().globals.config.new_user_displayname_suffix)
.expect("should be able to write to string buffer");
}
services()
.users
.set_displayname(&user_id, Some(displayname))
.await?;
// Initial account data
services().account_data.update(
None,
&user_id,
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),
},
})
.expect("to json value always works"),
)?;
if !services().globals.config.auto_join_rooms.is_empty() {
for room in &services().globals.config.auto_join_rooms {
if !services()
.rooms
.state_cache
.server_in_room(services().globals.server_name(), room)?
{
warn!("Skipping room {room} to automatically join as we have never joined before.");
continue;
}
if let Some(room_id_server_name) = room.server_name() {
match join_room_by_id_helper(
Some(&user_id),
room,
Some("Automatically joining this room upon registration".to_owned()),
&[room_id_server_name.to_owned(), services().globals.server_name().to_owned()],
None,
)
.await
{
Ok(_response) => {
info!("Automatically joined room {room} for user {user_id}");
},
Err(e) => {
// don't return this error so we don't fail registrations
error!("Failed to automatically join room {room} for user {user_id}: {e}");
},
};
}
}
}
// we dont add a device since we're not the user, just the creator
// Inhibit login does not work for guests
Ok(RoomMessageEventContent::text_plain(format!(
"Created user with user_id: {user_id} and password: `{password}`"
)))
}
pub(crate) async fn deactivate(
_body: Vec<&str>, no_leave_rooms: bool, user_id: String,
) -> Result<RoomMessageEventContent> {
// Validate user id
let user_id = parse_local_user_id(&user_id)?;
// don't deactivate the server service account
if user_id == services().globals.server_user {
return Ok(RoomMessageEventContent::text_plain(
"Not allowed to deactivate the server service account.",
));
}
services().users.deactivate_account(&user_id)?;
if !no_leave_rooms {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Making {user_id} leave all rooms after deactivation..."
)))
.await;
let all_joined_rooms: Vec<OwnedRoomId> = services()
.rooms
.state_cache
.rooms_joined(&user_id)
.filter_map(Result::ok)
.collect();
update_displayname(user_id.clone(), None, all_joined_rooms.clone()).await?;
update_avatar_url(user_id.clone(), None, None, all_joined_rooms).await?;
leave_all_rooms(&user_id).await;
}
Ok(RoomMessageEventContent::text_plain(format!(
"User {user_id} has been deactivated"
)))
}
pub(crate) async fn reset_password(_body: Vec<&str>, username: String) -> Result<RoomMessageEventContent> {
let user_id = parse_local_user_id(&username)?;
if user_id == services().globals.server_user {
return Ok(RoomMessageEventContent::text_plain(
"Not allowed to set the password for the server account. Please use the emergency password config option.",
));
}
let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH);
match services()
.users
.set_password(&user_id, Some(new_password.as_str()))
{
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!(
"Successfully reset the password for user {user_id}: `{new_password}`"
))),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Couldn't reset the password for user {user_id}: {e}"
))),
}
}
pub(crate) async fn deactivate_all(
body: Vec<&str>, no_leave_rooms: bool, force: bool,
) -> Result<RoomMessageEventContent> {
if body.len() < 2 || !body[0].trim().starts_with("```") || body.last().unwrap_or(&"").trim() != "```" {
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
let usernames = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
let mut user_ids: Vec<OwnedUserId> = Vec::with_capacity(usernames.len());
let mut admins = Vec::new();
for username in usernames {
match parse_active_local_user_id(username) {
Ok(user_id) => {
if services().users.is_admin(&user_id)? && !force {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"{username} is an admin and --force is not set, skipping over"
)))
.await;
admins.push(username);
continue;
}
// don't deactivate the server service account
if user_id == services().globals.server_user {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"{username} is the server service account, skipping over"
)))
.await;
continue;
}
user_ids.push(user_id);
},
Err(e) => {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"{username} is not a valid username, skipping over: {e}"
)))
.await;
continue;
},
}
}
let mut deactivation_count: usize = 0;
for user_id in user_ids {
match services().users.deactivate_account(&user_id) {
Ok(()) => {
deactivation_count = deactivation_count.saturating_add(1);
if !no_leave_rooms {
info!("Forcing user {user_id} to leave all rooms apart of deactivate-all");
let all_joined_rooms: Vec<OwnedRoomId> = services()
.rooms
.state_cache
.rooms_joined(&user_id)
.filter_map(Result::ok)
.collect();
update_displayname(user_id.clone(), None, all_joined_rooms.clone()).await?;
update_avatar_url(user_id.clone(), None, None, all_joined_rooms).await?;
leave_all_rooms(&user_id).await;
}
},
Err(e) => {
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!("Failed deactivating user: {e}")))
.await;
},
}
}
if admins.is_empty() {
Ok(RoomMessageEventContent::text_plain(format!(
"Deactivated {deactivation_count} accounts."
)))
} else {
Ok(RoomMessageEventContent::text_plain(format!(
"Deactivated {deactivation_count} accounts.\nSkipped admin accounts: {}. Use --force to deactivate admin \
accounts",
admins.join(", ")
)))
}
}
pub(crate) async fn list_joined_rooms(_body: Vec<&str>, user_id: String) -> Result<RoomMessageEventContent> {
// Validate user id
let user_id = parse_local_user_id(&user_id)?;
let mut rooms: Vec<(OwnedRoomId, u64, String)> = services()
.rooms
.state_cache
.rooms_joined(&user_id)
.filter_map(Result::ok)
.map(|room_id| get_room_info(&room_id))
.collect();
if rooms.is_empty() {
return Ok(RoomMessageEventContent::text_plain("User is not in any rooms."));
}
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let output_plain = format!(
"Rooms {user_id} Joined ({}):\n{}",
rooms.len(),
rooms
.iter()
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
.collect::<Vec<_>>()
.join("\n")
);
let output_html = format!(
"<table><caption>Rooms {user_id} Joined \
({})</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
rooms.len(),
rooms
.iter()
.fold(String::new(), |mut output, (id, members, name)| {
writeln!(
output,
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
escape_html(id.as_ref()),
members,
escape_html(name)
)
.unwrap();
output
})
);
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
}
pub(crate) async fn put_room_tag(
_body: Vec<&str>, user_id: String, room_id: Box<RoomId>, tag: String,
) -> Result<RoomMessageEventContent> {
let user_id = parse_active_local_user_id(&user_id)?;
let event = services()
.account_data
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
let mut tags_event = event.map_or_else(
|| TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
},
|e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"),
);
tags_event
.content
.tags
.insert(tag.clone().into(), TagInfo::new());
services().account_data.update(
Some(&room_id),
&user_id,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)?;
Ok(RoomMessageEventContent::text_plain(format!(
"Successfully updated room account data for {user_id} and room {room_id} with tag {tag}"
)))
}
pub(crate) async fn delete_room_tag(
_body: Vec<&str>, user_id: String, room_id: Box<RoomId>, tag: String,
) -> Result<RoomMessageEventContent> {
let user_id = parse_active_local_user_id(&user_id)?;
let event = services()
.account_data
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
let mut tags_event = event.map_or_else(
|| TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
},
|e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"),
);
tags_event.content.tags.remove(&tag.clone().into());
services().account_data.update(
Some(&room_id),
&user_id,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)?;
Ok(RoomMessageEventContent::text_plain(format!(
"Successfully updated room account data for {user_id} and room {room_id}, deleting room tag {tag}"
)))
}
pub(crate) async fn get_room_tags(
_body: Vec<&str>, user_id: String, room_id: Box<RoomId>,
) -> Result<RoomMessageEventContent> {
let user_id = parse_active_local_user_id(&user_id)?;
let event = services()
.account_data
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
let tags_event = event.map_or_else(
|| TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
},
|e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"),
);
Ok(RoomMessageEventContent::text_html(
format!("<pre><code>\n{:?}\n</code></pre>", tags_event.content.tags),
format!("```\n{:?}\n```", tags_event.content.tags),
))
}

64
src/admin/utils.rs Normal file
View File

@@ -0,0 +1,64 @@
pub(crate) use conduit::utils::HtmlEscape;
use conduit_core::Error;
use ruma::{OwnedRoomId, OwnedUserId, RoomId, UserId};
use service::user_is_local;
use crate::{services, Result};
pub(crate) fn escape_html(s: &str) -> String {
s.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}
pub(crate) fn get_room_info(id: &RoomId) -> (OwnedRoomId, u64, String) {
(
id.into(),
services()
.rooms
.state_cache
.room_joined_count(id)
.ok()
.flatten()
.unwrap_or(0),
services()
.rooms
.state_accessor
.get_name(id)
.ok()
.flatten()
.unwrap_or_else(|| id.to_string()),
)
}
/// Parses user ID
pub(crate) fn parse_user_id(user_id: &str) -> Result<OwnedUserId> {
UserId::parse_with_server_name(user_id.to_lowercase(), services().globals.server_name())
.map_err(|e| Error::Err(format!("The supplied username is not a valid username: {e}")))
}
/// Parses user ID as our local user
pub(crate) fn parse_local_user_id(user_id: &str) -> Result<OwnedUserId> {
let user_id = parse_user_id(user_id)?;
if !user_is_local(&user_id) {
return Err(Error::Err(String::from("User does not belong to our server.")));
}
Ok(user_id)
}
/// Parses user ID that is an active (not guest or deactivated) local user
pub(crate) fn parse_active_local_user_id(user_id: &str) -> Result<OwnedUserId> {
let user_id = parse_local_user_id(user_id)?;
if !services().users.exists(&user_id)? {
return Err(Error::Err(String::from("User does not exist on this server.")));
}
if services().users.is_deactivated(&user_id)? {
return Err(Error::Err(String::from("User is deactivated.")));
}
Ok(user_id)
}

View File

@@ -1,7 +0,0 @@
//! Default allocator with no special features
/// Always returns the empty string
pub(crate) fn memory_stats() -> String { Default::default() }
/// Always returns the empty string
pub(crate) fn memory_usage() -> String { Default::default() }

View File

@@ -1,8 +0,0 @@
#[global_allocator]
static HMALLOC: hardened_malloc_rs::HardenedMalloc = hardened_malloc_rs::HardenedMalloc;
pub(crate) fn memory_usage() -> String {
String::default() //TODO: get usage
}
pub(crate) fn memory_stats() -> String { "Extended statistics are not available from hardened_malloc.".to_owned() }

65
src/api/Cargo.toml Normal file
View File

@@ -0,0 +1,65 @@
[package]
name = "conduit_api"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
version.workspace = true
[lib]
path = "mod.rs"
crate-type = [
"rlib",
# "dylib",
]
[features]
element_hacks = []
dev_release_log_level = []
release_max_log_level = [
"tracing/max_level_trace",
"tracing/release_max_level_info",
"log/max_level_trace",
"log/release_max_level_info",
]
gzip_compression = [
"reqwest/gzip",
]
brotli_compression = [
"reqwest/brotli",
]
[dependencies]
axum-client-ip.workspace = true
axum-extra.workspace = true
axum.workspace = true
base64.workspace = true
bytes.workspace = true
conduit-core.workspace = true
conduit-database.workspace = true
conduit-service.workspace = true
futures-util.workspace = true
hmac.workspace = true
http.workspace = true
hyper.workspace = true
image.workspace = true
ipaddress.workspace = true
jsonwebtoken.workspace = true
log.workspace = true
rand.workspace = true
reqwest.workspace = true
ruma.workspace = true
serde_html_form.workspace = true
serde_json.workspace = true
serde.workspace = true
sha-1.workspace = true
thiserror.workspace = true
tokio.workspace = true
tracing.workspace = true
webpage.workspace = true
[lints]
workspace = true

View File

@@ -1,10 +1,12 @@
use std::fmt::Write as _;
use std::fmt::Write;
use axum_client_ip::InsecureClientIp;
use conduit::debug_info;
use register::RegistrationKind;
use ruma::{
api::client::{
account::{
change_password, deactivate, get_3pids, get_username_availability,
change_password, check_registration_token_validity, deactivate, get_3pids, get_username_availability,
register::{self, LoginType},
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn, whoami,
ThirdPartyIdRemovalStatus,
@@ -13,15 +15,15 @@
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType},
push, UserId,
push, OwnedRoomId, UserId,
};
use tracing::{error, info, warn};
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
use super::{join_room_by_id_helper, DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
use crate::{
api::client_server::{self, join_room_by_id_helper},
service, services,
utils::{self, user_id::user_is_local},
service::user_is_local,
services,
utils::{self},
Error, Result, Ruma,
};
@@ -38,8 +40,9 @@
///
/// Note: This will not reserve the username, so the username might become
/// invalid when trying to register
#[tracing::instrument(skip_all, fields(%client_ip))]
pub(crate) async fn get_register_available_route(
body: Ruma<get_username_availability::v3::Request>,
InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<get_username_availability::v3::Request>,
) -> Result<get_username_availability::v3::Response> {
// Validate user id
let user_id = UserId::parse_with_server_name(body.username.to_lowercase(), services().globals.server_name())
@@ -86,7 +89,10 @@ pub(crate) async fn get_register_available_route(
/// - If `inhibit_login` is false: Creates a device and returns device id and
/// access_token
#[allow(clippy::doc_markdown)]
pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<register::v3::Response> {
#[tracing::instrument(skip_all, fields(%client_ip))]
pub(crate) async fn register_route(
InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<register::v3::Request>,
) -> Result<register::v3::Response> {
if !services().globals.allow_registration() && body.appservice_info.is_none() {
info!(
"Registration disabled and request not from known appservice, rejecting registration attempt for username \
@@ -103,8 +109,8 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
|| (services().globals.allow_registration() && services().globals.config.registration_token.is_some()))
{
info!(
"Guest registration disabled / registration enabled with token configured, rejecting guest registration, \
initial device name: {:?}",
"Guest registration disabled / registration enabled with token configured, rejecting guest registration \
attempt, initial device name: {:?}",
body.initial_device_display_name
);
return Err(Error::BadRequest(
@@ -172,8 +178,7 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
// UIAA
let mut uiaainfo;
let skip_auth;
if services().globals.config.registration_token.is_some() {
let skip_auth = if services().globals.config.registration_token.is_some() {
// Registration token required
uiaainfo = UiaaInfo {
flows: vec![AuthFlow {
@@ -184,7 +189,7 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
session: None,
auth_error: None,
};
skip_auth = body.appservice_info.is_some();
body.appservice_info.is_some()
} else {
// No registration token necessary, but clients must still go through the flow
uiaainfo = UiaaInfo {
@@ -196,8 +201,8 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
session: None,
auth_error: None,
};
skip_auth = body.appservice_info.is_some() || is_guest;
}
body.appservice_info.is_some() || is_guest
};
if !skip_auth {
if let Some(auth) = &body.auth {
@@ -240,7 +245,8 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
// If `new_user_displayname_suffix` is set, registration will push whatever
// content is set to the user's display name with a space before it
if !services().globals.new_user_displayname_suffix().is_empty() {
_ = write!(displayname, " {}", services().globals.config.new_user_displayname_suffix);
write!(displayname, " {}", services().globals.config.new_user_displayname_suffix)
.expect("should be able to write to string buffer");
}
services()
@@ -288,20 +294,23 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
.users
.create_device(&user_id, &device_id, &token, body.initial_device_display_name.clone())?;
info!("New user \"{}\" registered on this server.", user_id);
debug_info!(%user_id, %device_id, "User account was created");
// log in conduit admin channel if a non-guest user registered
if body.appservice_info.is_none() && !is_guest {
info!("New user \"{user_id}\" registered on this server.");
services()
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"New user \"{user_id}\" registered on this server."
"New user \"{user_id}\" registered on this server from IP {client_ip}."
)))
.await;
}
// log in conduit admin channel if a guest registered
if body.appservice_info.is_none() && is_guest && services().globals.log_guest_registrations() {
info!("New guest user \"{user_id}\" registered on this server from IP.");
if let Some(device_display_name) = &body.initial_device_display_name {
if body
.initial_device_display_name
@@ -312,14 +321,15 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"Guest user \"{user_id}\" with device display name `{device_display_name}` registered on this \
server."
server from IP {client_ip}."
)))
.await;
} else {
services()
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"Guest user \"{user_id}\" with no device display name registered on this server.",
"Guest user \"{user_id}\" with no device display name registered on this server from IP \
{client_ip}.",
)))
.await;
}
@@ -327,7 +337,8 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
services()
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"Guest user \"{user_id}\" with no device display name registered on this server.",
"Guest user \"{user_id}\" with no device display name registered on this server from IP \
{client_ip}.",
)))
.await;
}
@@ -336,7 +347,7 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
// If this is the first real user, grant them admin privileges except for guest
// users Note: the server user, @conduit:servername, is generated first
if !is_guest {
if let Some(admin_room) = service::admin::Service::get_admin_room().await? {
if let Some(admin_room) = service::admin::Service::get_admin_room()? {
if services()
.rooms
.state_cache
@@ -348,7 +359,7 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
.make_user_admin(&user_id, displayname)
.await?;
warn!("Granting {} admin privileges as the first user", user_id);
warn!("Granting {user_id} admin privileges as the first user");
}
}
}
@@ -412,8 +423,9 @@ pub(crate) async fn register_route(body: Ruma<register::v3::Request>) -> Result<
/// last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
#[tracing::instrument(skip_all, fields(%client_ip))]
pub(crate) async fn change_password_route(
body: Ruma<change_password::v3::Request>,
InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<change_password::v3::Request>,
) -> Result<change_password::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
@@ -462,7 +474,7 @@ pub(crate) async fn change_password_route(
}
}
info!("User {} changed their password.", sender_user);
info!("User {sender_user} changed their password.");
services()
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
@@ -500,7 +512,10 @@ pub(crate) async fn whoami_route(body: Ruma<whoami::v3::Request>) -> Result<whoa
/// - Forgets all to-device events
/// - Triggers device list updates
/// - Removes ability to log in again
pub(crate) async fn deactivate_route(body: Ruma<deactivate::v3::Request>) -> Result<deactivate::v3::Response> {
#[tracing::instrument(skip_all, fields(%client_ip))]
pub(crate) async fn deactivate_route(
InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<deactivate::v3::Request>,
) -> Result<deactivate::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
@@ -532,13 +547,23 @@ pub(crate) async fn deactivate_route(body: Ruma<deactivate::v3::Request>) -> Res
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
}
// Make the user leave all rooms before deactivation
client_server::leave_all_rooms(sender_user).await;
// Remove devices and mark account as deactivated
services().users.deactivate_account(sender_user)?;
info!("User {} deactivated their account.", sender_user);
// Remove profile pictures and display name
let all_joined_rooms: Vec<OwnedRoomId> = services()
.rooms
.state_cache
.rooms_joined(sender_user)
.filter_map(Result::ok)
.collect();
super::update_displayname(sender_user.clone(), None, all_joined_rooms.clone()).await?;
super::update_avatar_url(sender_user.clone(), None, None, all_joined_rooms).await?;
// Make the user leave all rooms before deactivation
super::leave_all_rooms(sender_user).await;
info!("User {sender_user} deactivated their account.");
services()
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
@@ -593,3 +618,24 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
"Third party identifier is not allowed",
))
}
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
///
/// Checks if the provided registration token is valid at the time of checking
///
/// Currently does not have any ratelimiting, and this isn't very practical as
/// there is only one registration token allowed.
pub(crate) async fn check_registration_token_validity(
body: Ruma<check_registration_token_validity::v1::Request>,
) -> Result<check_registration_token_validity::v1::Response> {
let Some(reg_token) = services().globals.config.registration_token.clone() else {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Server does not allow token registration.",
));
};
Ok(check_registration_token_validity::v1::Response {
valid: reg_token == body.token,
})
}

View File

@@ -8,19 +8,22 @@
},
federation,
},
OwnedRoomAliasId, OwnedRoomId, OwnedServerName,
OwnedRoomAliasId, OwnedServerName, RoomAliasId, RoomId,
};
use tracing::debug;
use crate::{
debug_info, debug_warn, service::appservice::RegistrationInfo, services, utils::server_name::server_is_ours, Error,
Result, Ruma,
debug_info, debug_warn,
service::{appservice::RegistrationInfo, server_is_ours},
services, Error, Result, Ruma,
};
/// # `PUT /_matrix/client/v3/directory/room/{roomAlias}`
///
/// Creates a new room alias on this server.
pub(crate) async fn create_alias_route(body: Ruma<create_alias::v3::Request>) -> Result<create_alias::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
alias_checks(&body.room_alias, &body.appservice_info).await?;
// this isn't apart of alias_checks or delete alias route because we should
@@ -42,17 +45,10 @@ pub(crate) async fn create_alias_route(body: Ruma<create_alias::v3::Request>) ->
return Err(Error::Conflict("Alias already exists."));
}
if services()
services()
.rooms
.alias
.set_alias(&body.room_alias, &body.room_id)
.is_err()
{
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid room alias. Alias must be in the form of '#localpart:server_name'",
));
};
.set_alias(&body.room_alias, &body.room_id, sender_user)?;
Ok(create_alias::v3::Response::new())
}
@@ -61,9 +57,10 @@ pub(crate) async fn create_alias_route(body: Ruma<create_alias::v3::Request>) ->
///
/// Deletes a room alias from this server.
///
/// - TODO: additional access control checks
/// - TODO: Update canonical alias event
pub(crate) async fn delete_alias_route(body: Ruma<delete_alias::v3::Request>) -> Result<delete_alias::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
alias_checks(&body.room_alias, &body.appservice_info).await?;
if services()
@@ -75,17 +72,11 @@ pub(crate) async fn delete_alias_route(body: Ruma<delete_alias::v3::Request>) ->
return Err(Error::BadRequest(ErrorKind::NotFound, "Alias does not exist."));
}
if services()
services()
.rooms
.alias
.remove_alias(&body.room_alias)
.is_err()
{
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid room alias. Alias must be in the form of '#localpart:server_name'",
));
};
.remove_alias(&body.room_alias, sender_user)
.await?;
// TODO: update alt_aliases?
@@ -99,7 +90,7 @@ pub(crate) async fn get_alias_route(body: Ruma<get_alias::v3::Request>) -> Resul
get_alias_helper(body.body.room_alias, None).await
}
pub(crate) async fn get_alias_helper(
pub async fn get_alias_helper(
room_alias: OwnedRoomAliasId, servers: Option<Vec<OwnedServerName>>,
) -> Result<get_alias::v3::Response> {
debug!("get_alias_helper servers: {servers:?}");
@@ -119,7 +110,7 @@ pub(crate) async fn get_alias_helper(
)
.await;
debug_info!("room alias server_name get_alias_helper response: {response:?}");
debug!("room alias server_name get_alias_helper response: {response:?}");
if let Err(ref e) = response {
debug_info!(
@@ -140,7 +131,7 @@ pub(crate) async fn get_alias_helper(
},
)
.await;
debug_info!("Got response from server {server} for room aliases: {response:?}");
debug!("Got response from server {server} for room aliases: {response:?}");
if let Ok(ref response) = response {
if !response.servers.is_empty() {
@@ -162,7 +153,7 @@ pub(crate) async fn get_alias_helper(
pre_servers.push(room_alias.server_name().into());
let servers = room_available_servers(&room_id, &room_alias, &Some(pre_servers));
debug_warn!(
debug!(
"room alias servers from federation response for room ID {room_id} and room alias {room_alias}: \
{servers:?}"
);
@@ -213,13 +204,13 @@ pub(crate) async fn get_alias_helper(
let servers = room_available_servers(&room_id, &room_alias, &None);
debug_warn!("room alias servers for room ID {room_id} and room alias {room_alias}");
debug!("room alias servers for room ID {room_id} and room alias {room_alias}");
Ok(get_alias::v3::Response::new(room_id, servers))
}
fn room_available_servers(
room_id: &OwnedRoomId, room_alias: &OwnedRoomAliasId, pre_servers: &Option<Vec<OwnedServerName>>,
room_id: &RoomId, room_alias: &RoomAliasId, pre_servers: &Option<Vec<OwnedServerName>>,
) -> Vec<OwnedServerName> {
// find active servers in room state cache to suggest
let mut servers: Vec<OwnedServerName> = services()
@@ -247,20 +238,20 @@ fn room_available_servers(
.iter()
.position(|server_name| server_is_ours(server_name))
{
servers.remove(server_index);
servers.swap_remove(server_index);
servers.insert(0, services().globals.server_name().to_owned());
} else if let Some(alias_server_index) = servers
.iter()
.position(|server| server == room_alias.server_name())
{
servers.remove(alias_server_index);
servers.swap_remove(alias_server_index);
servers.insert(0, room_alias.server_name().into());
}
servers
}
async fn alias_checks(room_alias: &OwnedRoomAliasId, appservice_info: &Option<RegistrationInfo>) -> Result<()> {
async fn alias_checks(room_alias: &RoomAliasId, appservice_info: &Option<RegistrationInfo>) -> Result<()> {
if !server_is_ours(room_alias.server_name()) {
return Err(Error::BadRequest(ErrorKind::InvalidParam, "Alias is from another server."));
}

View File

@@ -163,7 +163,7 @@ pub(crate) async fn get_context_route(body: Ruma<get_context::v3::Request>) -> R
.map(|(_, pdu)| pdu.to_room_event())
.collect();
let mut state = Vec::new();
let mut state = Vec::with_capacity(state_ids.len());
for (shortstatekey, id) in state_ids {
let (event_type, state_key) = services()

View File

@@ -1,3 +1,4 @@
use axum_client_ip::InsecureClientIp;
use ruma::{
api::{
client::{
@@ -11,12 +12,8 @@
events::{
room::{
avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent},
topic::RoomTopicEventContent,
},
StateEventType,
},
@@ -24,15 +21,16 @@
};
use tracing::{error, info, warn};
use crate::{services, utils::server_name::server_is_ours, Error, Result, Ruma};
use crate::{service::server_is_ours, services, Error, Result, Ruma};
/// # `POST /_matrix/client/v3/publicRooms`
///
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
#[tracing::instrument(skip_all, fields(%client_ip))]
pub(crate) async fn get_public_rooms_filtered_route(
body: Ruma<get_public_rooms_filtered::v3::Request>,
InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<get_public_rooms_filtered::v3::Request>,
) -> Result<get_public_rooms_filtered::v3::Response> {
if let Some(server) = &body.server {
if services()
@@ -56,8 +54,8 @@ pub(crate) async fn get_public_rooms_filtered_route(
)
.await
.map_err(|e| {
warn!("Failed to return our /publicRooms: {e}");
Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.")
warn!(?body.server, "Failed to return /publicRooms: {e}");
Error::BadRequest(ErrorKind::Unknown, "Failed to return the requested server's public room list.")
})?;
Ok(response)
@@ -68,8 +66,9 @@ pub(crate) async fn get_public_rooms_filtered_route(
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
#[tracing::instrument(skip_all, fields(%client_ip))]
pub(crate) async fn get_public_rooms_route(
body: Ruma<get_public_rooms::v3::Request>,
InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<get_public_rooms::v3::Request>,
) -> Result<get_public_rooms::v3::Response> {
if let Some(server) = &body.server {
if services()
@@ -93,8 +92,8 @@ pub(crate) async fn get_public_rooms_route(
)
.await
.map_err(|e| {
warn!("Failed to return our /publicRooms: {e}");
Error::BadRequest(ErrorKind::Unknown, "Failed to return this server's public room list.")
warn!(?body.server, "Failed to return /publicRooms: {e}");
Error::BadRequest(ErrorKind::Unknown, "Failed to return the requested server's public room list.")
})?;
Ok(get_public_rooms::v3::Response {
@@ -110,8 +109,9 @@ pub(crate) async fn get_public_rooms_route(
/// Sets the visibility of a given room in the room directory.
///
/// - TODO: Access control checks
#[tracing::instrument(skip_all, fields(%client_ip))]
pub(crate) async fn set_room_visibility_route(
body: Ruma<set_room_visibility::v3::Request>,
InsecureClientIp(client_ip): InsecureClientIp, body: Ruma<set_room_visibility::v3::Request>,
) -> Result<set_room_visibility::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
@@ -231,12 +231,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
canonical_alias: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCanonicalAlias, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomCanonicalAliasEventContent| c.alias)
.map_err(|_| Error::bad_database("Invalid canonical alias event in database."))
})?,
.get_canonical_alias(&room_id)?,
name: services().rooms.state_accessor.get_name(&room_id)?,
num_joined_members: services()
.rooms
@@ -251,40 +246,13 @@ pub(crate) async fn get_public_rooms_filtered_helper(
topic: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomTopic, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomTopicEventContent| Some(c.topic))
.map_err(|e| {
error!("Invalid room topic event in database for room {room_id}: {e}");
Error::bad_database("Invalid room topic event in database.")
})
})
.get_room_topic(&room_id)
.unwrap_or(None),
world_readable: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomHistoryVisibility, "")?
.map_or(Ok(false), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomHistoryVisibilityEventContent| {
c.history_visibility == HistoryVisibility::WorldReadable
})
.map_err(|e| {
error!(
"Invalid room history visibility event in database for room {room_id}, assuming is \"shared\": {e}",
);
Error::bad_database("Invalid room history visibility event in database.")
})}).unwrap_or(false),
world_readable: services().rooms.state_accessor.is_world_readable(&room_id)?,
guest_can_join: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomGuestAccess, "")?
.map_or(Ok(false), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomGuestAccessEventContent| c.guest_access == GuestAccess::CanJoin)
.map_err(|_| Error::bad_database("Invalid room guest access event in database."))
})?,
.guest_can_join(&room_id)?,
avatar_url: services()
.rooms
.state_accessor

View File

@@ -22,8 +22,9 @@
use super::SESSION_ID_LENGTH;
use crate::{
service::user_is_local,
services,
utils::{self, user_id::user_is_local},
utils::{self},
Error, Result, Ruma,
};
@@ -247,7 +248,7 @@ pub(crate) async fn get_key_changes_route(
})
}
pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool + Send>(
sender_user: Option<&UserId>, device_keys_input: &BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>, allowed_signatures: F,
include_display_names: bool,
) -> Result<get_keys::v3::Response> {

View File

@@ -15,14 +15,14 @@
use crate::{
debug_warn,
service::media::{FileMeta, UrlPreviewData},
service::{
media::{FileMeta, UrlPreviewData},
server_is_ours,
},
services,
utils::{
self,
content_disposition::{
content_disposition_type, make_content_disposition, make_content_type, sanitise_filename,
},
server_name::server_is_ours,
content_disposition::{content_disposition_type, make_content_disposition, sanitise_filename},
},
Error, Result, Ruma, RumaResponse,
};
@@ -129,8 +129,6 @@ pub(crate) async fn create_content_route(
utils::random_string(MXC_LENGTH)
);
let content_type = Some(make_content_type(&body.file, &body.content_type).to_owned());
services()
.media
.create(
@@ -141,12 +139,12 @@ pub(crate) async fn create_content_route(
.map(|filename| {
format!(
"{}; filename={}",
content_disposition_type(&body.file, &content_type),
content_disposition_type(&body.content_type),
sanitise_filename(filename.to_owned())
)
})
.as_deref(),
content_type.as_deref(),
body.content_type.as_deref(),
&body.file,
)
.await?;
@@ -190,8 +188,7 @@ pub(crate) async fn get_content_route(body: Ruma<get_content::v3::Request>) -> R
content_disposition,
}) = services().media.get(mxc.clone()).await?
{
let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition));
let content_type = Some(make_content_type(&file, &content_type).to_owned());
let content_disposition = Some(make_content_disposition(&content_type, content_disposition, None));
Ok(get_content::v3::Response {
file,
@@ -215,15 +212,14 @@ pub(crate) async fn get_content_route(body: Ruma<get_content::v3::Request>) -> R
})?;
let content_disposition = Some(make_content_disposition(
&response.file,
&response.content_type,
response.content_disposition,
None,
));
let content_type = Some(make_content_type(&response.file, &response.content_type).to_owned());
Ok(get_content::v3::Response {
file: response.file,
content_type,
content_type: response.content_type,
content_disposition,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
@@ -270,8 +266,11 @@ pub(crate) async fn get_content_as_filename_route(
content_disposition,
}) = services().media.get(mxc.clone()).await?
{
let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition));
let content_type = Some(make_content_type(&file, &content_type).to_owned());
let content_disposition = Some(make_content_disposition(
&content_type,
content_disposition,
Some(body.filename.clone()),
));
Ok(get_content_as_filename::v3::Response {
file,
@@ -292,17 +291,14 @@ pub(crate) async fn get_content_as_filename_route(
{
Ok(remote_content_response) => {
let content_disposition = Some(make_content_disposition(
&remote_content_response.file,
&remote_content_response.content_type,
remote_content_response.content_disposition,
None,
));
let content_type = Some(
make_content_type(&remote_content_response.file, &remote_content_response.content_type).to_owned(),
);
Ok(get_content_as_filename::v3::Response {
content_disposition,
content_type,
content_type: remote_content_response.content_type,
file: remote_content_response.file,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
@@ -366,8 +362,7 @@ pub(crate) async fn get_content_thumbnail_route(
)
.await?
{
let content_disposition = Some(make_content_disposition(&file, &content_type, content_disposition));
let content_type = Some(make_content_type(&file, &content_type).to_owned());
let content_disposition = Some(make_content_disposition(&content_type, content_disposition, None));
Ok(get_content_thumbnail::v3::Response {
file,
@@ -420,17 +415,14 @@ pub(crate) async fn get_content_thumbnail_route(
.await?;
let content_disposition = Some(make_content_disposition(
&get_thumbnail_response.file,
&get_thumbnail_response.content_type,
get_thumbnail_response.content_disposition,
None,
));
let content_type = Some(
make_content_type(&get_thumbnail_response.file, &get_thumbnail_response.content_type).to_owned(),
);
Ok(get_content_thumbnail::v3::Response {
file: get_thumbnail_response.file,
content_type,
content_type: get_thumbnail_response.content_type,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),
content_disposition,
@@ -493,27 +485,25 @@ async fn get_remote_content(
.await?;
let content_disposition = Some(make_content_disposition(
&content_response.file,
&content_response.content_type,
content_response.content_disposition,
None,
));
let content_type = Some(make_content_type(&content_response.file, &content_response.content_type).to_owned());
services()
.media
.create(
None,
mxc.to_owned(),
content_disposition.as_deref(),
content_type.as_deref(),
content_response.content_type.as_deref(),
&content_response.file,
)
.await?;
Ok(get_content::v3::Response {
file: content_response.file,
content_type,
content_type: content_response.content_type,
content_disposition,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.to_owned()),

View File

@@ -3,6 +3,7 @@
sync::Arc,
};
use conduit::PduCount;
use ruma::{
api::client::{
error::ErrorKind,
@@ -14,10 +15,7 @@
};
use serde_json::{from_str, Value};
use crate::{
service::{pdu::PduBuilder, rooms::timeline::PduCount},
services, utils, Error, PduEvent, Result, Ruma,
};
use crate::{service::pdu::PduBuilder, services, utils, Error, PduEvent, Result, Ruma};
/// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`
///

83
src/api/client/mod.rs Normal file
View File

@@ -0,0 +1,83 @@
pub(super) mod account;
pub(super) mod alias;
pub(super) mod backup;
pub(super) mod capabilities;
pub(super) mod config;
pub(super) mod context;
pub(super) mod device;
pub(super) mod directory;
pub(super) mod filter;
pub(super) mod keys;
pub(super) mod media;
pub(super) mod membership;
pub(super) mod message;
pub(super) mod presence;
pub(super) mod profile;
pub(super) mod push;
pub(super) mod read_marker;
pub(super) mod redact;
pub(super) mod relations;
pub(super) mod report;
pub(super) mod room;
pub(super) mod search;
pub(super) mod session;
pub(super) mod space;
pub(super) mod state;
pub(super) mod sync;
pub(super) mod tag;
pub(super) mod thirdparty;
pub(super) mod threads;
pub(super) mod to_device;
pub(super) mod typing;
pub(super) mod unstable;
pub(super) mod unversioned;
pub(super) mod user_directory;
pub(super) mod voip;
pub(super) use account::*;
pub use alias::get_alias_helper;
pub(super) use alias::*;
pub(super) use backup::*;
pub(super) use capabilities::*;
pub(super) use config::*;
pub(super) use context::*;
pub(super) use device::*;
pub(super) use directory::*;
pub(super) use filter::*;
pub(super) use keys::*;
pub(super) use media::*;
pub(super) use membership::*;
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room, validate_and_add_event_id};
pub(super) use message::*;
pub(super) use presence::*;
pub(super) use profile::*;
pub use profile::{update_all_rooms, update_avatar_url, update_displayname};
pub(super) use push::*;
pub(super) use read_marker::*;
pub(super) use redact::*;
pub(super) use relations::*;
pub(super) use report::*;
pub(super) use room::*;
pub(super) use search::*;
pub(super) use session::*;
pub(super) use space::*;
pub(super) use state::*;
pub(super) use sync::*;
pub(super) use tag::*;
pub(super) use thirdparty::*;
pub(super) use threads::*;
pub(super) use to_device::*;
pub(super) use typing::*;
pub(super) use unstable::*;
pub(super) use unversioned::*;
pub(super) use user_directory::*;
pub(super) use voip::*;
/// generated device ID length
const DEVICE_ID_LENGTH: usize = 10;
/// generated user access token length
const TOKEN_LENGTH: usize = 32;
/// generated user session ID length
const SESSION_ID_LENGTH: usize = service::uiaa::SESSION_ID_LENGTH;

View File

@@ -10,10 +10,15 @@
},
events::{room::member::RoomMemberEventContent, StateEventType, TimelineEventType},
presence::PresenceState,
OwnedMxcUri, OwnedRoomId, OwnedUserId,
};
use serde_json::value::to_raw_value;
use tracing::warn;
use crate::{service::pdu::PduBuilder, services, utils::user_id::user_is_local, Error, Result, Ruma};
use crate::{
service::{pdu::PduBuilder, user_is_local},
services, Error, Result, Ruma,
};
/// # `PUT /_matrix/client/r0/profile/{userId}/displayname`
///
@@ -24,67 +29,14 @@ pub(crate) async fn set_displayname_route(
body: Ruma<set_display_name::v3::Request>,
) -> Result<set_display_name::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services()
.users
.set_displayname(sender_user, body.displayname.clone())
.await?;
// Send a new membership event and presence update into all joined rooms
let all_rooms_joined: Vec<_> = services()
let all_joined_rooms: Vec<OwnedRoomId> = services()
.rooms
.state_cache
.rooms_joined(sender_user)
.filter_map(Result::ok)
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
displayname: body.displayname.clone(),
join_authorized_via_users_server: None,
..serde_json::from_str(
services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomMember, sender_user.as_str())?
.ok_or_else(|| {
Error::bad_database("Tried to send displayname update for user not in the room.")
})?
.content
.get(),
)
.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.to_string()),
redacts: None,
},
room_id,
))
})
.filter_map(Result::ok)
.collect();
for (pdu_builder, room_id) in all_rooms_joined {
let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
_ = services()
.rooms
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
.await;
}
update_displayname(sender_user.clone(), body.displayname.clone(), all_joined_rooms).await?;
if services().globals.allow_local_presence() {
// Presence update
@@ -161,72 +113,20 @@ pub(crate) async fn set_avatar_url_route(
body: Ruma<set_avatar_url::v3::Request>,
) -> Result<set_avatar_url::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services()
.users
.set_avatar_url(sender_user, body.avatar_url.clone())
.await?;
services()
.users
.set_blurhash(sender_user, body.blurhash.clone())
.await?;
// Send a new membership event and presence update into all joined rooms
let all_joined_rooms: Vec<_> = services()
let all_joined_rooms: Vec<OwnedRoomId> = services()
.rooms
.state_cache
.rooms_joined(sender_user)
.filter_map(Result::ok)
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
avatar_url: body.avatar_url.clone(),
join_authorized_via_users_server: None,
..serde_json::from_str(
services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomMember, sender_user.as_str())?
.ok_or_else(|| {
Error::bad_database("Tried to send displayname update for user not in the room.")
})?
.content
.get(),
)
.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.to_string()),
redacts: None,
},
room_id,
))
})
.filter_map(Result::ok)
.collect();
for (pdu_builder, room_id) in all_joined_rooms {
let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
_ = services()
.rooms
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
.await;
}
update_avatar_url(
sender_user.clone(),
body.avatar_url.clone(),
body.blurhash.clone(),
all_joined_rooms,
)
.await?;
if services().globals.allow_local_presence() {
// Presence update
@@ -353,3 +253,126 @@ pub(crate) async fn get_profile_route(body: Ruma<get_profile::v3::Request>) -> R
displayname: services().users.displayname(&body.user_id)?,
})
}
pub async fn update_displayname(
user_id: OwnedUserId, displayname: Option<String>, all_joined_rooms: Vec<OwnedRoomId>,
) -> Result<()> {
services()
.users
.set_displayname(&user_id, displayname.clone())
.await?;
// Send a new join membership event into all joined rooms
let all_joined_rooms: Vec<_> = all_joined_rooms
.iter()
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
displayname: displayname.clone(),
join_authorized_via_users_server: None,
..serde_json::from_str(
services()
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())?
.ok_or_else(|| {
Error::bad_database("Tried to send display name update for user not in the room.")
})?
.content
.get(),
)
.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(user_id.to_string()),
redacts: None,
},
room_id,
))
})
.filter_map(Result::ok)
.collect();
update_all_rooms(all_joined_rooms, user_id).await;
Ok(())
}
pub async fn update_avatar_url(
user_id: OwnedUserId, avatar_url: Option<OwnedMxcUri>, blurhash: Option<String>, all_joined_rooms: Vec<OwnedRoomId>,
) -> Result<()> {
services()
.users
.set_avatar_url(&user_id, avatar_url.clone())
.await?;
services()
.users
.set_blurhash(&user_id, blurhash.clone())
.await?;
// Send a new join membership event into all joined rooms
let all_joined_rooms: Vec<_> = all_joined_rooms
.iter()
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
avatar_url: avatar_url.clone(),
blurhash: blurhash.clone(),
join_authorized_via_users_server: None,
..serde_json::from_str(
services()
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())?
.ok_or_else(|| {
Error::bad_database("Tried to send avatar URL update for user not in the room.")
})?
.content
.get(),
)
.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(user_id.to_string()),
redacts: None,
},
room_id,
))
})
.filter_map(Result::ok)
.collect();
update_all_rooms(all_joined_rooms, user_id).await;
Ok(())
}
pub async fn update_all_rooms(all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>, user_id: OwnedUserId) {
for (pdu_builder, room_id) in all_joined_rooms {
let mutex_state = Arc::clone(
services()
.globals
.roomid_mutex_state
.write()
.await
.entry(room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
if let Err(e) = services()
.rooms
.timeline
.build_and_append_pdu(pdu_builder, &user_id, room_id, &state_lock)
.await
{
warn!(%user_id, %room_id, %e, "Failed to update/send new profile join membership update in room");
}
}
}

View File

@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use conduit::PduCount;
use ruma::{
api::client::{error::ErrorKind, read_marker::set_read_marker, receipt::create_receipt},
events::{
@@ -9,7 +10,7 @@
MilliSecondsSinceUnixEpoch,
};
use crate::{service::rooms::timeline::PduCount, services, Error, Result, Ruma};
use crate::{services, Error, Result, Ruma};
/// # `POST /_matrix/client/r0/rooms/{roomId}/read_markers`
///

View File

@@ -104,14 +104,14 @@ fn is_report_valid(
));
}
if let Some(true) = score.map(|s| s > int!(0) || s < int!(-100)) {
if score.map(|s| s > int!(0) || s < int!(-100)) == Some(true) {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid score, must be within 0 to -100",
));
};
if let Some(true) = reason.clone().map(|s| s.len() >= 750) {
if reason.clone().map(|s| s.len() >= 750) == Some(true) {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Reason too long, should be 750 characters or fewer",

View File

@@ -1,5 +1,6 @@
use std::{cmp::max, collections::BTreeMap, sync::Arc};
use conduit::{debug_info, debug_warn};
use ruma::{
api::client::{
error::ErrorKind,
@@ -27,9 +28,8 @@
use serde_json::{json, value::to_raw_value};
use tracing::{error, info, warn};
use super::invite_helper;
use crate::{
api::client_server::invite_helper,
debug_info, debug_warn,
service::{appservice::RegistrationInfo, pdu::PduBuilder},
services, Error, Result, Ruma,
};
@@ -388,7 +388,7 @@ pub(crate) async fn create_room_route(body: Ruma<create_room::v3::Request>) -> R
Error::BadRequest(ErrorKind::InvalidParam, "Invalid initial state event.")
})?;
debug_warn!("initial state event: {event:?}");
debug_info!("Room creation initial state event: {event:?}");
// client/appservice workaround: if a user sends an initial_state event with a
// state event in there with the content of literally `{}` (not null or empty
@@ -460,12 +460,17 @@ pub(crate) async fn create_room_route(body: Ruma<create_room::v3::Request>) -> R
// 8. Events implied by invite (and TODO: invite_3pid)
drop(state_lock);
for user_id in &body.invite {
_ = invite_helper(sender_user, user_id, &room_id, None, body.is_direct).await;
if let Err(e) = invite_helper(sender_user, user_id, &room_id, None, body.is_direct).await {
warn!(%e, "Failed to send invite");
}
}
// Homeserver specific stuff
if let Some(alias) = alias {
services().rooms.alias.set_alias(&alias, &room_id)?;
services()
.rooms
.alias
.set_alias(&alias, &room_id, sender_user)?;
}
if body.visibility == room::Visibility::Public {
@@ -785,7 +790,7 @@ pub(crate) async fn upgrade_room_route(body: Ruma<upgrade_room::v3::Request>) ->
services()
.rooms
.alias
.set_alias(&alias, &replacement_room)?;
.set_alias(&alias, &replacement_room, sender_user)?;
}
// Get the old room power levels
@@ -815,7 +820,7 @@ pub(crate) async fn upgrade_room_route(body: Ruma<upgrade_room::v3::Request>) ->
// Modify the power levels in the old room to prevent sending of events and
// inviting new users
_ = services()
services()
.rooms
.timeline
.build_and_append_pdu(
@@ -884,7 +889,7 @@ fn default_power_levels_content(
/// if a room is being created with a room alias, run our checks
async fn room_alias_check(
room_alias_name: &String, appservice_info: &Option<RegistrationInfo>,
room_alias_name: &str, appservice_info: &Option<RegistrationInfo>,
) -> Result<OwnedRoomAliasId> {
// Basic checks on the room alias validity
if room_alias_name.contains(':') {
@@ -898,20 +903,6 @@ async fn room_alias_check(
ErrorKind::InvalidParam,
"Room alias contained spaces which is not a valid room alias.",
));
} else if room_alias_name.len() > 255 {
// there is nothing spec-wise saying to check the limit of this,
// however absurdly long room aliases are guaranteed to be unreadable or done
// maliciously. there is no reason a room alias should even exceed 100
// characters as is. generally in spec, 255 is matrix's fav number
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Room alias is excessively long, clients may not be able to handle this. Please shorten it.",
));
} else if room_alias_name.contains('"') {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Room alias contained `\"` which is not allowed.",
));
}
// check if room alias is forbidden
@@ -956,7 +947,7 @@ async fn room_alias_check(
}
/// if a room is being created with a custom room ID, run our checks against it
fn custom_room_id_check(custom_room_id: &String) -> Result<OwnedRoomId> {
fn custom_room_id_check(custom_room_id: &str) -> Result<OwnedRoomId> {
// apply forbidden room alias checks to custom room IDs too
if services()
.globals
@@ -977,8 +968,6 @@ fn custom_room_id_check(custom_room_id: &String) -> Result<OwnedRoomId> {
ErrorKind::InvalidParam,
"Custom room ID contained spaces which is not valid.",
));
} else if custom_room_id.len() > 255 {
return Err(Error::BadRequest(ErrorKind::InvalidParam, "Custom room ID is too long."));
}
let full_room_id = format!("!{}:{}", custom_room_id, services().globals.config.server_name);

View File

@@ -133,6 +133,7 @@ pub(crate) async fn search_events_route(body: Ruma<search_events::v3::Request>)
let results: Vec<_> = results
.iter()
.skip(skip)
.filter_map(|result| {
services()
.rooms
@@ -140,11 +141,12 @@ pub(crate) async fn search_events_route(body: Ruma<search_events::v3::Request>)
.get_pdu_from_id(result)
.ok()?
.filter(|pdu| {
services()
.rooms
.state_accessor
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id)
.unwrap_or(false)
!pdu.is_redacted()
&& services()
.rooms
.state_accessor
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id)
.unwrap_or(false)
})
.map(|pdu| pdu.to_room_event())
})
@@ -162,15 +164,11 @@ pub(crate) async fn search_events_route(body: Ruma<search_events::v3::Request>)
})
})
.filter_map(Result::ok)
.skip(skip)
.take(limit)
.collect();
let next_batch = if results.len() < limit {
None
} else {
Some(next_batch.to_string())
};
let more_unloaded_results = searches.iter_mut().any(|s| s.peek().is_some());
let next_batch = more_unloaded_results.then(|| next_batch.to_string());
Ok(search_events::v3::Response::new(ResultCategories {
room_events: ResultRoomEvents {

View File

@@ -1,4 +1,3 @@
use argon2::{PasswordHash, PasswordVerifier};
use ruma::{
api::client::{
error::ErrorKind,
@@ -21,7 +20,7 @@
use tracing::{debug, info, warn};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::{services, utils, Error, Result, Ruma};
use crate::{services, utils, utils::hash, Error, Result, Ruma};
#[derive(Debug, Deserialize)]
struct Claims {
@@ -87,15 +86,7 @@ pub(crate) async fn login_route(body: Ruma<login::v3::Request>) -> Result<login:
return Err(Error::BadRequest(ErrorKind::UserDeactivated, "The user has been deactivated"));
}
let parsed_hash = PasswordHash::new(&hash)
.map_err(|_| Error::BadServerResponse("Unknown error occurred hashing password."))?;
if services()
.globals
.argon
.verify_password(password.as_bytes(), &parsed_hash)
.is_err()
{
if hash::verify_password(password, &hash).is_err() {
return Err(Error::BadRequest(ErrorKind::forbidden(), "Wrong username or password."));
}

View File

@@ -19,10 +19,8 @@
use tracing::{error, log::warn};
use crate::{
service::{self, pdu::PduBuilder},
services,
utils::server_name::server_is_ours,
Error, Result, Ruma, RumaResponse,
service::{pdu::PduBuilder, server_is_ours},
services, Error, Result, Ruma, RumaResponse,
};
/// # `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}/{stateKey}`
@@ -218,7 +216,7 @@ async fn allowed_to_send_state_event(
},
// admin room is a sensitive room, it should not ever be made public
StateEventType::RoomJoinRules => {
if let Some(admin_room_id) = service::admin::Service::get_admin_room().await? {
if let Some(admin_room_id) = service::admin::Service::get_admin_room()? {
if admin_room_id == room_id {
if let Ok(join_rule) = serde_json::from_str::<RoomJoinRulesEventContent>(json.json().get()) {
if join_rule.join_rule == JoinRule::Public {
@@ -233,7 +231,7 @@ async fn allowed_to_send_state_event(
},
// admin room is a sensitive room, it should not ever be made world readable
StateEventType::RoomHistoryVisibility => {
if let Some(admin_room_id) = service::admin::Service::get_admin_room().await? {
if let Some(admin_room_id) = service::admin::Service::get_admin_room()? {
if admin_room_id == room_id {
if let Ok(visibility_content) =
serde_json::from_str::<RoomHistoryVisibilityEventContent>(json.json().get())

View File

@@ -5,6 +5,7 @@
time::Duration,
};
use conduit::PduCount;
use ruma::{
api::client::{
filter::{FilterDefinition, LazyLoadOptions},
@@ -25,15 +26,11 @@
StateEventType, TimelineEventType,
},
serde::Raw,
uint, DeviceId, EventId, OwnedDeviceId, OwnedUserId, RoomId, UInt, UserId,
uint, DeviceId, EventId, OwnedUserId, RoomId, UInt, UserId,
};
use tokio::sync::watch::Sender;
use tracing::{debug, error, Instrument as _, Span};
use tracing::{error, Instrument as _, Span};
use crate::{
service::{pdu::EventHash, rooms::timeline::PduCount},
services, utils, Error, PduEvent, Result, Ruma, RumaResponse,
};
use crate::{service::pdu::EventHash, services, utils, Error, PduEvent, Result, Ruma, RumaResponse};
/// # `GET /_matrix/client/r0/sync`
///
@@ -73,10 +70,6 @@
/// For left rooms:
/// - If the user left after `since`: `prev_batch` token, empty state (TODO:
/// subset of the state at the point of the leave)
///
/// - Sync is handled in an async task, multiple requests from the same device
/// with the same
/// `since` will be cached
pub(crate) async fn sync_events_route(
body: Ruma<sync_events::v3::Request>,
) -> Result<sync_events::v3::Response, RumaResponse<UiaaResponse>> {
@@ -84,95 +77,6 @@ pub(crate) async fn sync_events_route(
let sender_device = body.sender_device.expect("user is authenticated");
let body = body.body;
let mut rx = match services()
.globals
.sync_receivers
.write()
.await
.entry((sender_user.clone(), sender_device.clone()))
{
Entry::Vacant(v) => {
let (tx, rx) = tokio::sync::watch::channel(None);
v.insert((body.since.clone(), rx.clone()));
tokio::spawn(sync_helper_wrapper(sender_user.clone(), sender_device.clone(), body, tx));
rx
},
Entry::Occupied(mut o) => {
if o.get().0 != body.since {
let (tx, rx) = tokio::sync::watch::channel(None);
o.insert((body.since.clone(), rx.clone()));
debug!("Sync started for {sender_user}");
tokio::spawn(sync_helper_wrapper(sender_user.clone(), sender_device.clone(), body, tx));
rx
} else {
o.get().1.clone()
}
},
};
let we_have_to_wait = rx.borrow().is_none();
if we_have_to_wait {
if let Err(e) = rx.changed().await {
error!("Error waiting for sync: {}", e);
}
}
let result = match rx
.borrow()
.as_ref()
.expect("When sync channel changes it's always set to some")
{
Ok(response) => Ok(response.clone()),
Err(error) => Err(error.to_response()),
};
result
}
async fn sync_helper_wrapper(
sender_user: OwnedUserId, sender_device: OwnedDeviceId, body: sync_events::v3::Request,
tx: Sender<Option<Result<sync_events::v3::Response>>>,
) {
let since = body.since.clone();
let r = sync_helper(sender_user.clone(), sender_device.clone(), body).await;
if let Ok((_, caching_allowed)) = r {
if !caching_allowed {
match services()
.globals
.sync_receivers
.write()
.await
.entry((sender_user, sender_device))
{
Entry::Occupied(o) => {
// Only remove if the device didn't start a different /sync already
if o.get().0 == since {
o.remove();
}
},
Entry::Vacant(_) => {},
}
}
}
_ = tx.send(Some(r.map(|(r, _)| r)));
}
async fn sync_helper(
sender_user: OwnedUserId,
sender_device: OwnedDeviceId,
body: sync_events::v3::Request,
// bool = caching allowed
) -> Result<(sync_events::v3::Response, bool), Error> {
// Presence update
if services().globals.allow_local_presence() {
services()
@@ -238,7 +142,7 @@ async fn sync_helper(
.collect::<Vec<_>>();
// Coalesce database writes for the remainder of this scope.
let _cork = services().globals.db.cork_and_flush()?;
let _cork = services().globals.db.cork_and_flush();
for room_id in all_joined_rooms {
let room_id = room_id?;
@@ -413,11 +317,14 @@ async fn sync_helper(
if duration.as_secs() > 30 {
duration = Duration::from_secs(30);
}
_ = tokio::time::timeout(duration, watcher).await;
Ok((response, false))
} else {
Ok((response, since != next_batch)) // Only cache if we made progress
#[allow(clippy::let_underscore_must_use)]
{
_ = tokio::time::timeout(duration, watcher).await;
}
}
Ok(response)
}
#[tracing::instrument(skip_all, fields(user_id = %sender_user, room_id = %room_id))]
@@ -585,8 +492,10 @@ async fn handle_left_room(
}
async fn process_presence_updates(
presence_updates: &mut HashMap<OwnedUserId, PresenceEvent>, since: u64, syncing_user: &OwnedUserId,
presence_updates: &mut HashMap<OwnedUserId, PresenceEvent>, since: u64, syncing_user: &UserId,
) -> Result<()> {
use crate::service::presence::Presence;
// Take presence updates
for (user_id, _, presence_bytes) in services().presence.presence_since(since) {
if !services()
@@ -597,7 +506,6 @@ async fn process_presence_updates(
continue;
}
use crate::service::presence::Presence;
let presence_event = Presence::from_json_bytes_to_event(&presence_bytes, &user_id)?;
match presence_updates.entry(user_id) {
Entry::Vacant(slot) => {
@@ -703,7 +611,7 @@ async fn load_joined_room(
.unwrap_or(0);
// Recalculate heroes (first 5 members)
let mut heroes = Vec::new();
let mut heroes: Vec<OwnedUserId> = Vec::with_capacity(5);
if joined_member_count.saturating_add(invited_member_count) <= 5 {
// Go through all PDUs and for each member event, check if the user is still
@@ -728,7 +636,7 @@ async fn load_joined_room(
&& (services().rooms.state_cache.is_joined(&user_id, room_id)?
|| services().rooms.state_cache.is_invited(&user_id, room_id)?)
{
Ok::<_, Error>(Some(state_key.clone()))
Ok::<_, Error>(Some(user_id))
} else {
Ok(None)
}
@@ -736,12 +644,11 @@ async fn load_joined_room(
Ok(None)
}
})
// Filter out buggy users
.filter_map(Result::ok)
// Filter for possible heroes
.flatten()
{
if heroes.contains(&hero) || hero == sender_user.as_str() {
if heroes.contains(&hero) || hero == sender_user {
continue;
}
@@ -845,8 +752,7 @@ async fn load_joined_room(
// Incremental /sync
let since_shortstatehash = since_shortstatehash.unwrap();
let mut state_events = Vec::new();
let mut lazy_loaded = HashSet::new();
let mut delta_state_events = Vec::new();
if since_shortstatehash != current_shortstatehash {
let current_state_ids = services()
@@ -867,55 +773,12 @@ async fn load_joined_room(
continue;
};
if pdu.kind == TimelineEventType::RoomMember {
match UserId::parse(
pdu.state_key
.as_ref()
.expect("State event has state key")
.clone(),
) {
Ok(state_key_userid) => {
lazy_loaded.insert(state_key_userid);
},
Err(e) => error!("Invalid state key for member event: {}", e),
}
}
state_events.push(pdu);
delta_state_events.push(pdu);
tokio::task::yield_now().await;
}
}
}
for (_, event) in &timeline_pdus {
if lazy_loaded.contains(&event.sender) {
continue;
}
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
sender_user,
sender_device,
room_id,
&event.sender,
)? || lazy_load_send_redundant
{
if let Some(member_event) = services().rooms.state_accessor.room_state_get(
room_id,
&StateEventType::RoomMember,
event.sender.as_str(),
)? {
lazy_loaded.insert(event.sender.clone());
state_events.push(member_event);
}
}
}
services()
.rooms
.lazy_loading
.lazy_load_mark_sent(sender_user, sender_device, room_id, lazy_loaded, next_batchcount)
.await;
let encrypted_room = services()
.rooms
.state_accessor
@@ -931,12 +794,12 @@ async fn load_joined_room(
// Calculations:
let new_encrypted_room = encrypted_room && since_encryption.is_none();
let send_member_count = state_events
let send_member_count = delta_state_events
.iter()
.any(|event| event.kind == TimelineEventType::RoomMember);
if encrypted_room {
for state_event in &state_events {
for state_event in &delta_state_events {
if state_event.kind != TimelineEventType::RoomMember {
continue;
}
@@ -997,6 +860,57 @@ async fn load_joined_room(
(None, None, Vec::new())
};
let mut state_events = delta_state_events;
let mut lazy_loaded = HashSet::new();
// Mark all member events we're returning as lazy-loaded
for pdu in &state_events {
if pdu.kind == TimelineEventType::RoomMember {
match UserId::parse(
pdu.state_key
.as_ref()
.expect("State event has state key")
.clone(),
) {
Ok(state_key_userid) => {
lazy_loaded.insert(state_key_userid);
},
Err(e) => error!("Invalid state key for member event: {}", e),
}
}
}
// Fetch contextual member state events for events from the timeline, and
// mark them as lazy-loaded as well.
for (_, event) in &timeline_pdus {
if lazy_loaded.contains(&event.sender) {
continue;
}
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
sender_user,
sender_device,
room_id,
&event.sender,
)? || lazy_load_send_redundant
{
if let Some(member_event) = services().rooms.state_accessor.room_state_get(
room_id,
&StateEventType::RoomMember,
event.sender.as_str(),
)? {
lazy_loaded.insert(event.sender.clone());
state_events.push(member_event);
}
}
}
services()
.rooms
.lazy_loading
.lazy_load_mark_sent(sender_user, sender_device, room_id, lazy_loaded, next_batchcount)
.await;
(
heroes,
joined_member_count,
@@ -1127,8 +1041,7 @@ fn load_timeline(
sender_user: &UserId, room_id: &RoomId, roomsincecount: PduCount, limit: u64,
) -> Result<(Vec<(PduCount, PduEvent)>, bool), Error> {
let timeline_pdus;
let limited;
if services()
let limited = if services()
.rooms
.timeline
.last_timeline_count(sender_user, room_id)?
@@ -1158,11 +1071,11 @@ fn load_timeline(
// They /sync response doesn't always return all messages, so we say the output
// is limited unless there are events in non_timeline_pdus
limited = non_timeline_pdus.next().is_some();
non_timeline_pdus.next().is_some()
} else {
timeline_pdus = Vec::new();
limited = false;
}
false
};
Ok((timeline_pdus, limited))
}
@@ -1670,6 +1583,7 @@ pub(crate) async fn sync_events_v4_route(
),
num_live: None, // Count events in timeline greater than global sync counter
timestamp: None,
heroes: None,
},
);
}
@@ -1684,7 +1598,10 @@ pub(crate) async fn sync_events_v4_route(
if duration.as_secs() > 30 {
duration = Duration::from_secs(30);
}
_ = tokio::time::timeout(duration, watcher).await;
#[allow(clippy::let_underscore_must_use)]
{
_ = tokio::time::timeout(duration, watcher).await;
}
}
Ok(sync_events::v4::Response {

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