Compare commits

...

178 Commits

Author SHA1 Message Date
Jade Ellis
9e718e1d53 chore: Test 2025-04-16 15:45:19 +01:00
Jade Ellis
1985019c99 chore: Test variables 2025-04-16 15:39:44 +01:00
Jade Ellis
5a9cc1cd5d Test if condition 2025-04-16 15:18:45 +01:00
Jade Ellis
2ce42b1ef5 chore: if block 2025-04-16 15:07:16 +01:00
Jade Ellis
ac912276a6 chore: Set env variable 2025-04-16 15:05:05 +01:00
Jade Ellis
425a6c0b1a chore: Test action 2025-04-16 15:00:59 +01:00
Jade Ellis
f76f669d16 chore: Remove the default sentry endpoint 2025-04-15 22:35:54 +00:00
Jade Ellis
dad407fb22 chore: Add words to cspell dictionary 2025-04-15 22:35:39 +00:00
Jade Ellis
17a04940fc chore: Update Olivia Lee in mailmap 2025-04-15 21:58:39 +01:00
Jade Ellis
6e5392c2f5 chore: Add Timo Kösters to the mailmap 2025-04-15 14:48:09 +00:00
Jade Ellis
57779df66a chore: Add mailmap 2025-04-15 14:48:09 +00:00
Jade Ellis
35bffa5970 ci: Delete all old CI files
Part of #753
2025-04-15 10:25:49 +01:00
Jade Ellis
4f9e9174e2 docs: Mention future migration guide 2025-04-15 10:11:47 +01:00
Jade Ellis
3e54c7e691 docs: Phrasing 2025-04-15 10:11:47 +01:00
Jade Ellis
57d26dae0d docs: Remove hidden conduwuit badges 2025-04-15 10:11:47 +01:00
Jade Ellis
e054a56b32 docs: New readme
It's a continuwuation!
2025-04-15 10:10:21 +01:00
June Clementine Strawberry
d8311a5ff6 bump crossbeam-channel bc yanked crate with potential double free
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-08 23:38:54 -04:00
June Clementine Strawberry
47f8345457 bump tokio because of RUSTSEC-2025-0023
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-08 09:05:49 -04:00
June Clementine Strawberry
99868b1661 update new complement flakes
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-06 16:11:35 -04:00
June Clementine Strawberry
d5ad973464 change forbidden_server_names and etc to allow regex patterns for wildcards
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-06 15:25:19 -04:00
June Clementine Strawberry
ff276a42a3 drop unnecessary info log to debug
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-06 13:19:09 -04:00
June Clementine Strawberry
5f8c68ab84 add trace logging for room summaries, use server_in_room instead of exists
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-06 13:17:13 -04:00
June Clementine Strawberry
6578b83bce parallelise IO of user searching, improve perf, raise max limit to 500
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-05 20:09:22 -04:00
June Clementine Strawberry
3cc92b32ec bump rust toolchain to 1.86.0
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-05 18:37:13 -04:00
June Clementine Strawberry
9678948daf use patch of resolv-conf crate to allow no-aaaa resolv.conf option
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-05 18:33:43 -04:00
Jason Volk
500faa8d7f simplify space join rules related
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-05 22:12:33 +00:00
Jason Volk
d6cc447add simplify acl brick-check conditions
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-05 22:12:33 +00:00
June Clementine Strawberry
e28ae8fb4d downgrade deranged crate
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-05 14:26:00 -04:00
June Clementine Strawberry
c7246662f4 try partially reverting 94b107b42b
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-05 14:07:37 -04:00
June Clementine Strawberry
a212bf7cfc update default room version to v11
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-05 14:00:40 -04:00
Jason Volk
58b8c7516a extend extract_variant to multiple variants
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-05 02:44:46 +00:00
Jason Volk
bb8320a691 abstract and encapsulate the awkward OptionFuture into Stream pattern
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-05 02:44:46 +00:00
Jason Volk
532dfd004d move core::pdu and core::state_res into core::matrix::
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-05 02:44:46 +00:00
June Clementine Strawberry
4e5b87d0cd add missing condition for signatures upload failures
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-04 11:34:31 -04:00
Jason Volk
00f7745ec4 remove the db pool queue full warning
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-04 02:59:54 +00:00
Jason Volk
d036394ec7 refactor incoming prev events loop; mitigate large future
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-03 22:40:40 +00:00
Jason Volk
6a073b4fa4 remove additional unnecessary Arc
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-03 22:40:40 +00:00
Jason Volk
b7109131e2 further simplify get_missing_events; various log calls
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-03 22:40:40 +00:00
June Clementine Strawberry
94b107b42b add some debug logging and misc cleanup to keys/signatures/upload
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-03 16:08:18 -04:00
Jason Volk
29d55b8036 move systemd stopping notification point
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-03 19:38:51 +00:00
Jason Volk
45fd3875c8 move runtime shutdown out of main; gather final stats
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-03 19:38:51 +00:00
Jason Volk
f9529937ce patch hyper-util due to conflicts with federation resolver hooks
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-03 19:38:51 +00:00
Jason Volk
0b56204f89 bump additional dependencies
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-03 19:38:51 +00:00
Jason Volk
58adb6fead upgrade hickory and hyper-util dependencies
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-03 19:38:51 +00:00
Jason Volk
5d1404e9df fix well-known using the hooked resolver
Signed-off-by: Jason Volk <jason@zemos.net>
2025-04-03 19:38:51 +00:00
June Clementine Strawberry
f14756fb76 leave room locally if room is banned, rescind knocks on deactivation too
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-03 12:21:16 -04:00
June Clementine Strawberry
24be579477 add appservice MSC4190 support
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-03 12:21:16 -04:00
June Clementine Strawberry
0e0b8cc403 fixup+update msc3266, add fed support, parallelise IO
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-03 00:56:37 -04:00
June Clementine Strawberry
1036f8dfa8 default shared history vis on unknown visibilities, drop needless error log
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-02 22:46:01 -04:00
June Clementine Strawberry
74012c5289 significantly improve get_missing_events fed code
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-02 22:44:44 -04:00
June Clementine Strawberry
ea246d91d9 remove pointless and buggy *_visibility in-memory caches
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-02 22:38:47 -04:00
June Clementine Strawberry
1b71b99c51 fix weird issue with acl c2s check
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-04-02 10:49:38 -04:00
Jason Volk
0f81c1e1cc revert hyper-util upgrade due to continued DNS issues
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-31 22:17:08 -04:00
Jason Volk
bee1f89624 bump dependencies
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-31 05:03:52 +00:00
Jason Volk
5768ca8442 upgrade dependency ByteSize
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-31 04:27:20 +00:00
Jason Volk
3f0f89cddb use async_trait without axum re-export
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-31 04:27:20 +00:00
Jason Volk
d3b65af616 remove several services.globals config wrappers
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-31 03:00:53 +00:00
Jason Volk
d60920c728 workaround some large type name length issues
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-31 03:00:53 +00:00
Jason Volk
db99d3a001 remove recently-made-unnecessary unsafe block
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-31 02:30:32 +00:00
Jason Volk
bee4c6255a reorg PduEvent strip tools and callsites
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-30 23:00:37 +00:00
Jason Volk
dc6e9e74d9 add spans for for jemalloc mallctl points
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-30 23:00:37 +00:00
Jason Volk
5bf5afaec8 instrument tokio before/after poll hooks
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-30 23:00:37 +00:00
Jason Volk
095734a8e7 bump tokio to 1.44.1
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-30 23:00:37 +00:00
Jason Volk
a93cb34dd6 disambiguate UInt/u64 type related in client/api/directory; use err macros.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-30 23:00:37 +00:00
Jason Volk
b03c493bf9 add stub for database benches
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-29 01:06:39 +00:00
Jason Volk
d0132706cd add --read-only and --maintenance program option
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-29 01:06:39 +00:00
Jason Volk
0e2009dbf5 fix client hierarchy loop condition
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-28 22:47:51 +00:00
Ginger
3e57b7d35d Update expected test results 2025-03-28 14:30:14 -04:00
Ginger
75b6daa67f Fix off-by-one error when fetching room hierarchy 2025-03-28 14:30:14 -04:00
June Clementine Strawberry
6365f1a887 remove sccache from ci for now
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-28 14:26:12 -04:00
Jason Volk
b2bf35cfab fix benches from state-res
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-28 09:01:46 +00:00
Jason Volk
7f448d88a4 use qualified crate names from within workspace
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-27 07:08:41 +00:00
Jason Volk
c99f5770a0 mark get_summary_and_children_federation Send
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-27 07:08:41 +00:00
Jason Volk
dfe058a244 default config item to 'none' when zstd_compression not featured
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-27 01:09:27 +00:00
Jason Volk
07ba00f74e abstract raw query command iterations
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 04:43:05 +00:00
Jason Volk
9d0ce3965e fix lints
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 02:25:54 +00:00
Jason Volk
d1b82ea225 use #[ignore] for todo'ed tests
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 01:33:41 +00:00
Jason Volk
23e3f6526f split well_known resolver into unit
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 01:33:41 +00:00
Jason Volk
8010505853 implement clear_cache() for resolver service
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 01:33:41 +00:00
Jason Volk
9ce95a7030 make service memory_usage()/clear_cache() async trait
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 01:33:41 +00:00
Jason Volk
d8ea8b378c add Map::clear() to db interface
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 01:33:41 +00:00
Jason Volk
17003ba773 add FIFO compaction for persistent-cache descriptor; comments/cleanup
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 01:33:41 +00:00
Jason Volk
a57336ec13 assume canonical order in db serialization test
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 01:33:41 +00:00
Jason Volk
7294368015 parallelize IO for PublicRoomsChunk vector
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 01:33:41 +00:00
Jason Volk
aa4d2e2363 fix unused import without feature jemalloc_conf
fix span passed by value

Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 01:33:41 +00:00
Jason Volk
07ec9d6d85 re-sort pushkey_deviceid (33c5afe050)
Signed-off-by: Jason Volk <jason@zemos.net>
2025-03-26 01:33:41 +00:00
cy
33c5afe050 delete pushers created with different access token on password change 2025-03-21 10:34:17 -04:00
June Clementine Strawberry
7bf92c8a37 replace unnecessary check when updating device keys
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-17 23:02:34 -04:00
cy
658c19d55e check if we already have a more preferable key backup before adding 2025-03-16 18:23:19 -04:00
cy
4518f55408 guard against using someone else's access token in UIAA 2025-03-15 19:35:09 -04:00
June Clementine Strawberry
ee3c585555 skip a few flakey complement tests
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-15 19:14:45 -04:00
June Clementine Strawberry
6c29792b3d respect include_leave syncv3 filter
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-13 15:49:40 -04:00
June Clementine Strawberry
258b399de9 bump ruwuma
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-13 15:23:10 -04:00
June Clementine Strawberry
5dea52f0f8 stop doing complement cert gen and just use self-signed cert
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-13 10:50:43 -04:00
June Clementine Strawberry
1d1ccec532 fix some nightly clippy lints
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-13 08:37:34 -04:00
June Clementine Strawberry
0877f29439 respect membership filters on /members
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-13 08:37:34 -04:00
June Clementine Strawberry
e920c44cb4 ignore humantime dep as tracing console-subscriber uses it (somewhere)
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-11 21:15:11 -04:00
June Clementine Strawberry
ae818d5b25 remove most of cargo test from engage as crane does that but with more caching
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-11 21:09:24 -04:00
June Clementine Strawberry
7f95eef9ab bump ruwuma
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-11 21:09:19 -04:00
June Clementine Strawberry
3104586884 bump tracing-subscriber, allowlist cargo-doc lint in admin room
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-11 18:05:36 -04:00
Odd Eivind Ebbesen
c4b05e77f3 Fix up wording in the doc comments for admin media deletion (#694) 2025-03-10 17:28:29 -04:00
Ginger
1366a3092f Check the room_types filter when searching for local public rooms (#698) 2025-03-10 17:28:19 -04:00
Tamara Schmitz
1e23c95ec6 docs: refactor reverse proxy setup sections (#701) 2025-03-10 17:27:53 -04:00
June Clementine Strawberry
56dba8acb7 misc docs updates
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-10 17:27:06 -04:00
June Clementine Strawberry
889fb3cf26 add download-artifact pattern for OCI images only
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-10 13:55:11 -04:00
June Clementine Strawberry
e704bbaf11 update complement test results
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-10 12:59:18 -04:00
June Clementine Strawberry
5ba0c02d52 bump ruwuma to fix a threads issue, fix more error codes, delete legacy sytest cruft
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-10 12:29:54 -04:00
June Clementine Strawberry
df1edcf498 adjust complement cert generation
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-10 10:40:44 -04:00
June Clementine Strawberry
0e2ca7d719 implement disable TLS validation config option
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-09 21:55:07 -04:00
June Clementine Strawberry
0e342aab7f fix a few error codes
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-09 14:20:24 -04:00
June Clementine Strawberry
47ff91243d update complement results
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-09 03:33:29 -04:00
June Clementine Strawberry
d0c767c23c fix a few things to make some complement tests pass
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-09 01:43:49 -05:00
June Clementine Strawberry
06f2039eee bump ruwuma to maybe fix rare device key upload issues
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-09 00:44:56 -05:00
June Clementine Strawberry
0b012b529f comment gotestfmt for now
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-08 19:32:16 -05:00
June Clementine Strawberry
5efe804a20 always disable fed, evict admins, and forget the room when banning a room
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-08 18:29:19 -05:00
strawberry
ef96e7afac add cargo auditable for future use, ignore paste dependency being unmaintained for now
Signed-off-by: strawberry <june@girlboss.ceo>
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-08 14:05:52 -05:00
strawberry
c8a730c29e implement MSC4267 automatically forgetting room on leave
Signed-off-by: strawberry <june@girlboss.ceo>
2025-03-08 03:07:42 -05:00
strawberry
bb0b57efb8 bump rust-rocksdb
Signed-off-by: strawberry <june@girlboss.ceo>
2025-03-08 02:30:58 -05:00
strawberry
5a3264980a adjust complement script to allow using your own hs OCI image without nix
Signed-off-by: strawberry <june@girlboss.ceo>
2025-03-08 01:35:26 -05:00
strawberry
90fee4f50e add gotestfmt log output to complement script and CI output
Signed-off-by: strawberry <june@girlboss.ceo>
2025-03-08 00:15:13 -05:00
strawberry
51d29bc1cb bump complement
Signed-off-by: strawberry <june@girlboss.ceo>
2025-03-07 21:44:52 -05:00
strawberry
298b58c069 set file_shape for roomsynctoken_shortstatehash to 3, remove rust-rocksdb package spec
Signed-off-by: strawberry <june@girlboss.ceo>
2025-03-07 21:44:33 -05:00
June Clementine Strawberry
6052c0c8a2 ci: allow ourselves to write to the public docs directory
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-07 01:25:43 -05:00
June Clementine Strawberry
8b3f629198 bump rust-rocksdb
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-07 00:57:47 -05:00
June Clementine Strawberry
4f882c3bd8 add some ACL paw-gun checks, better PUT state event validation
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-07 00:57:39 -05:00
June Clementine Strawberry
2c58a6efda allow broken no-op deny+allow room server ACL keys
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-07 00:54:30 -05:00
June Clementine Strawberry
fe65648296 remove unnecessary map_err
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 20:10:32 -05:00
June Clementine Strawberry
5ad1100e0f bump our rocksdb fork
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 19:49:21 -05:00
June Clementine Strawberry
20dd1d148d add new complement test results
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 18:55:13 -05:00
June Clementine Strawberry
fa71162c7d bump rocksdb to v9.11.1
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 18:55:10 -05:00
June Clementine Strawberry
f34e0b21a3 remove rust-rocksdb dylib wrapper as we have a fork already
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 18:12:54 -05:00
June Clementine Strawberry
d80e61cbee bump ring to 0.17.12
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 17:53:23 -05:00
June Clementine Strawberry
c92678ecbe dont build with zlib in the nix flake
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 14:07:38 -05:00
June Clementine Strawberry
ecea0cff69 fix TestFetchMessagesFromNonExistentRoom complement test
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 13:44:44 -05:00
June Clementine Strawberry
931fd4c802 add missing target
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 13:44:44 -05:00
June Clementine Strawberry
657e91fd42 dont send push notifications from ignored users PDUs
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 13:44:42 -05:00
June Clementine Strawberry
f4c51cd405 remove zlib as a default rocksdb compression option
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 00:18:28 -05:00
June Clementine Strawberry
17b625a85b reject device keys if they dont match user ID or device ID or are missing fields
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 00:14:49 -05:00
June Clementine Strawberry
c10500f8ae bump rust-rocksdb and ruwuma
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-06 00:14:24 -05:00
June Clementine Strawberry
2c1ec3fb02 allow both lowercase and uppercase usernames to login
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-05 21:31:49 -05:00
June Clementine Strawberry
408f5bd30c add val_size_hints on membership cfs (todo remove these anyways)
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-05 19:34:27 -05:00
June Clementine Strawberry
97208d6081 add more safety checks before allowing a room marked as forgotten
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-05 19:34:27 -05:00
June Clementine Strawberry
35981d5aef automatically forget rooms on leaving
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-05 19:34:22 -05:00
June Clementine Strawberry
7c17163730 switch to self-hosted ci runner, remove sudo usages
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-05 17:39:59 -05:00
June Clementine Strawberry
1ecd027389 always run checks when building in nix (doCheck true)
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-03 01:06:04 -05:00
June Clementine Strawberry
df72384c16 delete snappy, bump rust-rocksdb, bump rocksdb to v9.10.0 again
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-03 01:05:43 -05:00
June Clementine Strawberry
0d741bbd46 remove nix run ci test as its covered by complement
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-03 00:15:12 -05:00
June Clementine Strawberry
af714d5778 refactor+fix various issues with regs/logins and admin user commands
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-02 23:16:30 -05:00
June Clementine Strawberry
00cc23b649 bump nix lockfile, bump cargo.lock, bump ruwuma
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-02 23:15:30 -05:00
June Clementine Strawberry
de53ad83b2 run nightly cargo fmt again
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-02 23:15:05 -05:00
June Clementine Strawberry
17e0384eeb ignore errors instead of expecting for state gathering
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-03-02 23:11:43 -05:00
June Clementine Strawberry
dca7bf9635 try bumping cache-nix-action to v6
Signed-off-by: June Clementine Strawberry <june@3.dog>
2025-02-27 10:45:04 -05:00
Jason Volk
a67ab75417 fix edition 2024 lints
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-27 10:26:45 -05:00
June Clementine Strawberry
cbf207bd1f try adding back some skipped complement tests
Signed-off-by: June Clementine Strawberry <strawberry@puppygock.gay>
2025-02-23 03:11:34 -05:00
June Clementine Strawberry
4bdd0d77db bump complement, actually run all tests
Signed-off-by: June Clementine Strawberry <strawberry@puppygock.gay>
2025-02-23 02:52:19 -05:00
June Clementine Strawberry
045e8a2937 stop building mac binaries for now because of linker issues
Signed-off-by: June Clementine Strawberry <strawberry@puppygock.gay>
2025-02-23 02:51:55 -05:00
June Clementine Strawberry
a1e1f40ded run cargo fix for rust 2024 changes and rustfmt
Signed-off-by: June Clementine Strawberry <strawberry@puppygock.gay>
2025-02-23 01:17:45 -05:00
June Clementine Strawberry
e97952b7f6 bump nix lock, update to rust 2024 and 1.85.0 toolchain
Signed-off-by: June Clementine Strawberry <strawberry@puppygock.gay>
2025-02-23 01:17:30 -05:00
morguldir
bec19df275 increase compression levels for some column families again 2025-02-22 17:12:31 +01:00
morguldir
8085a1c064 Implement MSC3967, also fixes error when uploading keys in element
Co-authored-by: Aiden McClelland <me@drbonez.dev>

Signed-off-by: morguldir <morguldir@protonmail.com>
2025-02-22 16:46:06 +01:00
morguldir
1061f68f0e bump ruwuma 2025-02-21 21:13:06 +01:00
Jason Volk
01155fa649 fix unsafe precondition violation
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-22 16:44:39 -05:00
strawberry
c614d5bf44 bump ruwuma
Signed-off-by: strawberry <june@girlboss.ceo>
2025-02-17 17:35:03 -05:00
Jason Volk
f47677c995 refactor spaces
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
6113803038 better error logging on send_join response failure
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
4de0dafdf1 bump ruma
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
f2ca670c3b optimize further into state-res with SmallString
triage and de-lints for state-res.

Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
0a9a9b3c92 larcen state-res from ruma 2025-02-16 17:40:21 -05:00
Jason Volk
b872f8e593 optimize with SmallString; consolidate related re-exports
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
ecc9099127 add conf item to re-disable atomic flush
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
e123a5b660 add state accessories for iterating state_keys of a type
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
59c073d0d8 add unconstrained feature to service worker
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
5428526120 add tail-efficient logic extension
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
d8e94ee965 split spaces service
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
31ab84e928 simplify client event endpoint
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
565837ad75 request auth media first
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
2d71d5590a fix pdu add_relation() helper
Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
Jason Volk
3ec43be959 join initial fetches in get_relations()
skip recursion for max_depth=0

Signed-off-by: Jason Volk <jason@zemos.net>
2025-02-16 17:40:21 -05:00
408 changed files with 13066 additions and 9557 deletions

27
.cargo/audit.toml Normal file
View File

@@ -0,0 +1,27 @@
[advisories]
ignore = ["RUSTSEC-2024-0436", "RUSTSEC-2025-0014"] # advisory IDs to ignore e.g. ["RUSTSEC-2019-0001", ...]
informational_warnings = [] # warn for categories of informational advisories
severity_threshold = "none" # CVSS severity ("none", "low", "medium", "high", "critical")
# Advisory Database Configuration
[database]
path = "~/.cargo/advisory-db" # Path where advisory git repo will be cloned
url = "https://github.com/RustSec/advisory-db.git" # URL to git repo
fetch = true # Perform a `git fetch` before auditing (default: true)
stale = false # Allow stale advisory DB (i.e. no commits for 90 days, default: false)
# Output Configuration
[output]
deny = ["warnings", "unmaintained", "unsound", "yanked"] # exit on error if unmaintained dependencies are found
format = "terminal" # "terminal" (human readable report) or "json"
quiet = false # Only print information on error
show_tree = true # Show inverse dependency trees along with advisories (default: true)
# Target Configuration
[target]
arch = ["x86_64", "aarch64"] # Ignore advisories for CPU architectures other than these
os = ["linux", "windows", "macos"] # Ignore advisories for operating systems other than these
[yanked]
enabled = true # Warn for yanked crates in Cargo.lock (default: true)
update_index = true # Auto-update the crates.io index (default: true)

View File

@@ -0,0 +1,12 @@
name: Test
on:
push:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "forgejo.ref = ${{ forgejo.ref }}"

View File

@@ -1,993 +0,0 @@
name: CI and Artifacts
on:
pull_request:
push:
paths-ignore:
- '.gitlab-ci.yml'
- '.gitignore'
- 'renovate.json'
- 'debian/**'
- 'docker/**'
branches:
- main
tags:
- '*'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
concurrency:
group: ${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
env:
# sccache only on main repo
SCCACHE_GHA_ENABLED: "${{ !startsWith(github.ref, 'refs/tags/') && (github.event.pull_request.draft != true) && (vars.DOCKER_USERNAME != '') && (vars.GITLAB_USERNAME != '') && (vars.SCCACHE_ENDPOINT != '') && (github.event.pull_request.user.login != 'renovate[bot]') && 'true' || 'false' }}"
RUSTC_WRAPPER: "${{ !startsWith(github.ref, 'refs/tags/') && (github.event.pull_request.draft != true) && (vars.DOCKER_USERNAME != '') && (vars.GITLAB_USERNAME != '') && (vars.SCCACHE_ENDPOINT != '') && (github.event.pull_request.user.login != 'renovate[bot]') && 'sccache' || '' }}"
SCCACHE_BUCKET: "${{ (github.event.pull_request.draft != true) && (vars.DOCKER_USERNAME != '') && (vars.GITLAB_USERNAME != '') && (vars.SCCACHE_ENDPOINT != '') && (github.event.pull_request.user.login != 'renovate[bot]') && 'sccache' || '' }}"
SCCACHE_S3_USE_SSL: ${{ vars.SCCACHE_S3_USE_SSL }}
SCCACHE_REGION: ${{ vars.SCCACHE_REGION }}
SCCACHE_ENDPOINT: ${{ vars.SCCACHE_ENDPOINT }}
SCCACHE_CACHE_MULTIARCH: ${{ vars.SCCACHE_CACHE_MULTIARCH }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Required to make some things output color
TERM: ansi
# Publishing to my nix binary cache
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
# conduwuit.cachix.org
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
# Just in case incremental is still being set to true, speeds up CI
CARGO_INCREMENTAL: 0
# Custom nix binary cache if fork is being used
ATTIC_ENDPOINT: ${{ vars.ATTIC_ENDPOINT }}
ATTIC_PUBLIC_KEY: ${{ vars.ATTIC_PUBLIC_KEY }}
# Get error output from nix that we can actually use, and use our binary caches for the earlier CI steps
NIX_CONFIG: |
show-trace = true
extra-substituters = https://attic.kennel.juneis.dog/conduwuit https://attic.kennel.juneis.dog/conduit https://conduwuit.cachix.org https://aseipp-nix-cache.freetls.fastly.net
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= conduwuit.cachix.org-1:MFRm6jcnfTf0jSAbmvLfhO3KBMt4px+1xaereWXp8Xg=
experimental-features = nix-command flakes
extra-experimental-features = nix-command flakes
accept-flake-config = true
WEB_UPLOAD_SSH_USERNAME: ${{ secrets.WEB_UPLOAD_SSH_USERNAME }}
GH_REF_NAME: ${{ github.ref_name }}
WEBSERVER_DIR_NAME: ${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}-${{ github.sha }}
permissions: {}
jobs:
tests:
name: Test
runs-on: ubuntu-24.04
steps:
- name: Setup SSH web publish
env:
web_upload_ssh_private_key: ${{ secrets.WEB_UPLOAD_SSH_PRIVATE_KEY }}
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event.pull_request.draft != true)) && (env.web_upload_ssh_private_key != '') && github.event.pull_request.user.login != 'renovate[bot]'
run: |
mkdir -p -v ~/.ssh
echo "${{ secrets.WEB_UPLOAD_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
echo "${{ secrets.WEB_UPLOAD_SSH_PRIVATE_KEY }}" >> ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
cat >>~/.ssh/config <<END
Host website
HostName ${{ secrets.WEB_UPLOAD_SSH_HOSTNAME }}
User ${{ secrets.WEB_UPLOAD_SSH_USERNAME }}
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking yes
AddKeysToAgent no
ForwardX11 no
BatchMode yes
END
echo "Checking connection"
ssh -q website "echo test" || ssh -q website "echo test"
echo "Creating commit rev directory on web server"
ssh -q website "rm -rf /var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/" || ssh -q website "rm -rf /var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/"
ssh -q website "mkdir -v /var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/" || ssh -q website "mkdir -v /var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/"
echo "SSH_WEBSITE=1" >> "$GITHUB_ENV"
- name: Install liburing
run: |
sudo apt install liburing-dev -y
- name: Free up a bit of runner space
run: |
set +o pipefail
sudo docker image prune --all --force || true
sudo apt purge -y 'php.*' '^mongodb-.*' '^mysql-.*' azure-cli google-cloud-cli google-chrome-stable firefox powershell microsoft-edge-stable || true
sudo apt clean
sudo rm -rf /usr/local/lib/android /usr/local/julia* /usr/local/games /usr/local/sqlpackage /usr/local/share/powershell /usr/local/share/edge_driver /usr/local/share/gecko_driver /usr/local/share/chromium /usr/local/share/chromedriver-linux64 /usr/lib/google-cloud-sdk /usr/lib/jvm /usr/lib/mono /usr/local/lib/heroku /usr/lib/heroku /usr/local/share/boost /usr/share/dotnet /usr/local/bin/cmake* /usr/local/bin/stack /usr/local/bin/terraform /opt/microsoft/powershell /opt/hostedtoolcache/CodeQL /opt/hostedtoolcache/go /opt/hostedtoolcache/PyPy /usr/local/bin/sam || true
set -o pipefail
- name: Sync repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Tag comparison check
if: ${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-rc') }}
run: |
# Tag mismatch with latest repo tag check to prevent potential downgrades
LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)
if [ ${LATEST_TAG} != ${GH_REF_NAME} ]; then
echo '# WARNING: Attempting to run this workflow for a tag that is not the latest repo tag. Aborting.'
echo '# WARNING: Attempting to run this workflow for a tag that is not the latest repo tag. Aborting.' >> $GITHUB_STEP_SUMMARY
exit 1
fi
- uses: nixbuild/nix-quick-install-action@master
- name: Restore and cache Nix store
# we want a fresh-state when we do releases/tags to avoid potential cache poisoning attacks impacting
# releases and tags
#if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: nix-community/cache-nix-action@v5.1.0
with:
# restore and save a cache using this key
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/.lock') }}
# if there's no cache hit, restore a cache by this prefix
restore-prefixes-first-match: nix-${{ runner.os }}-
# collect garbage until Nix store size (in bytes) is at most this number
# before trying to save a new cache
gc-max-store-size-linux: 2073741824
# do purge caches
purge: true
# purge all versions of the cache
purge-prefixes: nix-${{ runner.os }}-
# created more than this number of seconds ago relative to the start of the `Post Restore` phase
purge-last-accessed: 86400
# except the version with the `primary-key`, if it exists
purge-primary-key: never
# always save the cache
save-always: true
- name: Enable Cachix binary cache
run: |
nix profile install nixpkgs#cachix
cachix use crane
cachix use nix-community
- name: Apply Nix binary cache configuration
run: |
sudo tee -a "${XDG_CONFIG_HOME:-$HOME/.config}/nix/nix.conf" > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduwuit https://attic.kennel.juneis.dog/conduit https://conduwuit.cachix.org https://aseipp-nix-cache.freetls.fastly.net
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o= conduwuit.cachix.org-1:MFRm6jcnfTf0jSAbmvLfhO3KBMt4px+1xaereWXp8Xg=
experimental-features = nix-command flakes
extra-experimental-features = nix-command flakes
accept-flake-config = true
EOF
- name: Use alternative Nix binary caches if specified
if: ${{ (env.ATTIC_ENDPOINT != '') && (env.ATTIC_PUBLIC_KEY != '') }}
run: |
sudo tee -a "${XDG_CONFIG_HOME:-$HOME/.config}/nix/nix.conf" > /dev/null <<EOF
extra-substituters = ${ATTIC_ENDPOINT}
extra-trusted-public-keys = ${ATTIC_PUBLIC_KEY}
EOF
- name: Prepare build environment
run: |
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc"
nix profile install --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv
direnv allow
nix develop .#all-features --command true
- name: Cache CI dependencies
run: |
bin/nix-build-and-cache ci
bin/nix-build-and-cache just '.#devShells.x86_64-linux.default'
bin/nix-build-and-cache just '.#devShells.x86_64-linux.all-features'
bin/nix-build-and-cache just '.#devShells.x86_64-linux.dynamic'
# use sccache for Rust
- name: Run sccache-cache
# we want a fresh-state when we do releases/tags to avoid potential cache poisoning attacks impacting
# releases and tags
#if: ${{ (env.SCCACHE_GHA_ENABLED == 'true') && !startsWith(github.ref, 'refs/tags/') }}
uses: mozilla-actions/sccache-action@main
# use rust-cache
- uses: Swatinem/rust-cache@v2
# we want a fresh-state when we do releases/tags to avoid potential cache poisoning attacks impacting
# releases and tags
#if: ${{ !startsWith(github.ref, 'refs/tags/') }}
with:
cache-all-crates: "true"
cache-on-failure: "true"
cache-targets: "true"
- name: Run CI tests
env:
CARGO_PROFILE: "test"
run: |
direnv exec . engage > >(tee -a test_output.log)
- name: Run Complement tests
env:
CARGO_PROFILE: "test"
run: |
# the nix devshell sets $COMPLEMENT_SRC, so "/dev/null" is no-op
direnv exec . bin/complement "/dev/null" complement_test_logs.jsonl complement_test_results.jsonl > >(tee -a test_output.log)
cp -v -f result complement_oci_image.tar.gz
- name: Upload Complement OCI image
uses: actions/upload-artifact@v4
with:
name: complement_oci_image.tar.gz
path: complement_oci_image.tar.gz
if-no-files-found: error
compression-level: 0
- name: Upload Complement logs
uses: actions/upload-artifact@v4
with:
name: complement_test_logs.jsonl
path: complement_test_logs.jsonl
if-no-files-found: error
- name: Upload Complement results
uses: actions/upload-artifact@v4
with:
name: complement_test_results.jsonl
path: complement_test_results.jsonl
if-no-files-found: error
- name: Diff Complement results with checked-in repo results
run: |
diff -u --color=always tests/test_results/complement/test_results.jsonl complement_test_results.jsonl > >(tee -a complement_diff_output.log)
- name: Update Job Summary
env:
GH_JOB_STATUS: ${{ job.status }}
if: success() || failure()
run: |
if [ ${GH_JOB_STATUS} == 'success' ]; then
echo '# ✅ completed suwuccessfully' >> $GITHUB_STEP_SUMMARY
else
echo '# CI failure' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
tail -n 40 test_output.log | sed 's/\x1b\[[0-9;]*m//g' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo '# Complement diff results' >> $GITHUB_STEP_SUMMARY
echo '```diff' >> $GITHUB_STEP_SUMMARY
tail -n 100 complement_diff_output.log | sed 's/\x1b\[[0-9;]*m//g' >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
- name: Run cargo clean test artifacts to free up space
run: |
cargo clean --profile test
build:
name: Build
runs-on: ubuntu-24.04
strategy:
matrix:
include:
- target: aarch64-linux-musl
- target: x86_64-linux-musl
steps:
- name: Free up a bit of runner space
run: |
set +o pipefail
sudo docker image prune --all --force || true
sudo apt purge -y 'php.*' '^mongodb-.*' '^mysql-.*' azure-cli google-cloud-cli google-chrome-stable firefox powershell microsoft-edge-stable || true
sudo apt clean
sudo rm -rf /usr/local/lib/android /usr/local/julia* /usr/local/games /usr/local/sqlpackage /usr/local/share/powershell /usr/local/share/edge_driver /usr/local/share/gecko_driver /usr/local/share/chromium /usr/local/share/chromedriver-linux64 /usr/lib/google-cloud-sdk /usr/lib/jvm /usr/lib/mono /usr/local/lib/heroku /usr/lib/heroku /usr/local/share/boost /usr/share/dotnet /usr/local/bin/cmake* /usr/local/bin/stack /usr/local/bin/terraform /opt/microsoft/powershell /opt/hostedtoolcache/CodeQL /opt/hostedtoolcache/go /opt/hostedtoolcache/PyPy /usr/local/bin/sam || true
set -o pipefail
- name: Sync repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup SSH web publish
env:
web_upload_ssh_private_key: ${{ secrets.WEB_UPLOAD_SSH_PRIVATE_KEY }}
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event.pull_request.draft != true)) && (env.web_upload_ssh_private_key != '') && github.event.pull_request.user.login != 'renovate[bot]'
run: |
mkdir -p -v ~/.ssh
echo "${{ secrets.WEB_UPLOAD_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
echo "${{ secrets.WEB_UPLOAD_SSH_PRIVATE_KEY }}" >> ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
cat >>~/.ssh/config <<END
Host website
HostName ${{ secrets.WEB_UPLOAD_SSH_HOSTNAME }}
User ${{ secrets.WEB_UPLOAD_SSH_USERNAME }}
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking yes
AddKeysToAgent no
ForwardX11 no
BatchMode yes
END
echo "Checking connection"
ssh -q website "echo test" || ssh -q website "echo test"
echo "SSH_WEBSITE=1" >> "$GITHUB_ENV"
- uses: nixbuild/nix-quick-install-action@master
- name: Restore and cache Nix store
# we want a fresh-state when we do releases/tags to avoid potential cache poisoning attacks impacting
# releases and tags
#if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: nix-community/cache-nix-action@v5.1.0
with:
# restore and save a cache using this key
primary-key: nix-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('**/*.nix', '**/.lock') }}
# if there's no cache hit, restore a cache by this prefix
restore-prefixes-first-match: nix-${{ runner.os }}-
# collect garbage until Nix store size (in bytes) is at most this number
# before trying to save a new cache
gc-max-store-size-linux: 2073741824
# do purge caches
purge: true
# purge all versions of the cache
purge-prefixes: nix-${{ runner.os }}-
# created more than this number of seconds ago relative to the start of the `Post Restore` phase
purge-last-accessed: 86400
# except the version with the `primary-key`, if it exists
purge-primary-key: never
# always save the cache
save-always: true
- name: Enable Cachix binary cache
run: |
nix profile install nixpkgs#cachix
cachix use crane
cachix use nix-community
- name: Apply Nix binary cache configuration
run: |
sudo tee -a "${XDG_CONFIG_HOME:-$HOME/.config}/nix/nix.conf" > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduwuit https://attic.kennel.juneis.dog/conduit https://conduwuit.cachix.org https://aseipp-nix-cache.freetls.fastly.net
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o= conduwuit.cachix.org-1:MFRm6jcnfTf0jSAbmvLfhO3KBMt4px+1xaereWXp8Xg=
experimental-features = nix-command flakes
extra-experimental-features = nix-command flakes
accept-flake-config = true
EOF
- name: Use alternative Nix binary caches if specified
if: ${{ (env.ATTIC_ENDPOINT != '') && (env.ATTIC_PUBLIC_KEY != '') }}
run: |
sudo tee -a "${XDG_CONFIG_HOME:-$HOME/.config}/nix/nix.conf" > /dev/null <<EOF
extra-substituters = ${ATTIC_ENDPOINT}
extra-trusted-public-keys = ${ATTIC_PUBLIC_KEY}
EOF
- name: Prepare build environment
run: |
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 .#all-features --command true --impure
# use sccache for Rust
- name: Run sccache-cache
# we want a fresh-state when we do releases/tags to avoid potential cache poisoning attacks impacting
# releases and tags
#if: ${{ (env.SCCACHE_GHA_ENABLED == 'true') && !startsWith(github.ref, 'refs/tags/') }}
uses: mozilla-actions/sccache-action@main
# use rust-cache
- uses: Swatinem/rust-cache@v2
# we want a fresh-state when we do releases/tags to avoid potential cache poisoning attacks impacting
# releases and tags
#if: ${{ !startsWith(github.ref, 'refs/tags/') }}
with:
cache-all-crates: "true"
cache-on-failure: "true"
cache-targets: "true"
- name: Build static ${{ matrix.target }}-all-features
run: |
if [[ ${{ matrix.target }} == "x86_64-linux-musl" ]]
then
CARGO_DEB_TARGET_TUPLE="x86_64-unknown-linux-musl"
elif [[ ${{ matrix.target }} == "aarch64-linux-musl" ]]
then
CARGO_DEB_TARGET_TUPLE="aarch64-unknown-linux-musl"
fi
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
bin/nix-build-and-cache just .#static-${{ matrix.target }}-all-features
mkdir -v -p target/release/
mkdir -v -p target/$CARGO_DEB_TARGET_TUPLE/release/
cp -v -f result/bin/conduwuit target/release/conduwuit
cp -v -f result/bin/conduwuit target/$CARGO_DEB_TARGET_TUPLE/release/conduwuit
direnv exec . cargo deb --verbose --no-build --no-strip -p conduwuit --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
- name: Build static x86_64-linux-musl-all-features-x86_64-haswell-optimised
if: ${{ matrix.target == 'x86_64-linux-musl' }}
run: |
CARGO_DEB_TARGET_TUPLE="x86_64-unknown-linux-musl"
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
bin/nix-build-and-cache just .#static-x86_64-linux-musl-all-features-x86_64-haswell-optimised
mkdir -v -p target/release/
mkdir -v -p target/$CARGO_DEB_TARGET_TUPLE/release/
cp -v -f result/bin/conduwuit target/release/conduwuit
cp -v -f result/bin/conduwuit target/$CARGO_DEB_TARGET_TUPLE/release/conduwuit
direnv exec . cargo deb --verbose --no-build --no-strip -p conduwuit --target=$CARGO_DEB_TARGET_TUPLE --output target/release/x86_64-linux-musl-x86_64-haswell-optimised.deb
mv -v target/release/conduwuit static-x86_64-linux-musl-x86_64-haswell-optimised
mv -v target/release/x86_64-linux-musl-x86_64-haswell-optimised.deb x86_64-linux-musl-x86_64-haswell-optimised.deb
# quick smoke test of the x86_64 static release binary
- name: Quick smoke test the x86_64 static release binary
if: ${{ matrix.target == 'x86_64-linux-musl' }}
run: |
# GH actions default runners are x86_64 only
if file result/bin/conduwuit | grep x86-64; then
result/bin/conduwuit --version
result/bin/conduwuit --help
result/bin/conduwuit -Oserver_name="'$(date -u +%s).local'" -Odatabase_path="'/tmp/$(date -u +%s)'" --execute "server admin-notice awawawawawawawawawawa" --execute "server memory-usage" --execute "server shutdown"
fi
- name: Build static debug ${{ matrix.target }}-all-features
run: |
if [[ ${{ matrix.target }} == "x86_64-linux-musl" ]]
then
CARGO_DEB_TARGET_TUPLE="x86_64-unknown-linux-musl"
elif [[ ${{ matrix.target }} == "aarch64-linux-musl" ]]
then
CARGO_DEB_TARGET_TUPLE="aarch64-unknown-linux-musl"
fi
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
bin/nix-build-and-cache just .#static-${{ matrix.target }}-all-features-debug
# > warning: dev profile is not supported and will be a hard error in the future. cargo-deb is for making releases, and it doesn't make sense to use it with dev profiles.
# so we need to coerce cargo-deb into thinking this is a release binary
mkdir -v -p target/release/
mkdir -v -p target/$CARGO_DEB_TARGET_TUPLE/release/
cp -v -f result/bin/conduwuit target/release/conduwuit
cp -v -f result/bin/conduwuit target/$CARGO_DEB_TARGET_TUPLE/release/conduwuit
direnv exec . cargo deb --verbose --no-build --no-strip -p conduwuit --target=$CARGO_DEB_TARGET_TUPLE --output target/release/${{ matrix.target }}-debug.deb
mv -v target/release/conduwuit static-${{ matrix.target }}-debug
mv -v target/release/${{ matrix.target }}-debug.deb ${{ matrix.target }}-debug.deb
# quick smoke test of the x86_64 static debug binary
- name: Run x86_64 static debug binary
run: |
# GH actions default runners are x86_64 only
if file result/bin/conduwuit | grep x86-64; then
result/bin/conduwuit --version
fi
# check validity of produced deb package, invalid debs will error on these commands
- name: Validate produced deb package
run: |
# List contents
dpkg-deb --contents ${{ matrix.target }}.deb
dpkg-deb --contents ${{ matrix.target }}-debug.deb
# List info
dpkg-deb --info ${{ matrix.target }}.deb
dpkg-deb --info ${{ matrix.target }}-debug.deb
- name: Upload static-x86_64-linux-musl-all-features-x86_64-haswell-optimised to GitHub
uses: actions/upload-artifact@v4
if: ${{ matrix.target == 'x86_64-linux-musl' }}
with:
name: static-x86_64-linux-musl-x86_64-haswell-optimised
path: static-x86_64-linux-musl-x86_64-haswell-optimised
if-no-files-found: error
- name: Upload static-${{ matrix.target }}-all-features to GitHub
uses: actions/upload-artifact@v4
with:
name: static-${{ matrix.target }}
path: static-${{ matrix.target }}
if-no-files-found: error
- name: Upload static deb ${{ matrix.target }}-all-features to GitHub
uses: actions/upload-artifact@v4
with:
name: deb-${{ matrix.target }}
path: ${{ matrix.target }}.deb
if-no-files-found: error
compression-level: 0
- name: Upload static-x86_64-linux-musl-all-features-x86_64-haswell-optimised to webserver
if: ${{ matrix.target == 'x86_64-linux-musl' }}
run: |
if [ ! -z $SSH_WEBSITE ]; then
chmod +x static-x86_64-linux-musl-x86_64-haswell-optimised
scp static-x86_64-linux-musl-x86_64-haswell-optimised website:/var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/static-x86_64-linux-musl-x86_64-haswell-optimised
fi
- name: Upload static-${{ matrix.target }}-all-features to webserver
run: |
if [ ! -z $SSH_WEBSITE ]; then
chmod +x static-${{ matrix.target }}
scp static-${{ matrix.target }} website:/var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/static-${{ matrix.target }}
fi
- name: Upload static deb x86_64-linux-musl-all-features-x86_64-haswell-optimised to webserver
if: ${{ matrix.target == 'x86_64-linux-musl' }}
run: |
if [ ! -z $SSH_WEBSITE ]; then
scp x86_64-linux-musl-x86_64-haswell-optimised.deb website:/var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/x86_64-linux-musl-x86_64-haswell-optimised.deb
fi
- name: Upload static deb ${{ matrix.target }}-all-features to webserver
run: |
if [ ! -z $SSH_WEBSITE ]; then
scp ${{ matrix.target }}.deb website:/var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/${{ matrix.target }}.deb
fi
- name: Upload static-${{ matrix.target }}-debug-all-features to GitHub
uses: actions/upload-artifact@v4
with:
name: static-${{ matrix.target }}-debug
path: static-${{ matrix.target }}-debug
if-no-files-found: error
- name: Upload static deb ${{ matrix.target }}-debug-all-features to GitHub
uses: actions/upload-artifact@v4
with:
name: deb-${{ matrix.target }}-debug
path: ${{ matrix.target }}-debug.deb
if-no-files-found: error
compression-level: 0
- name: Upload static-${{ matrix.target }}-debug-all-features to webserver
run: |
if [ ! -z $SSH_WEBSITE ]; then
scp static-${{ matrix.target }}-debug website:/var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/static-${{ matrix.target }}-debug
fi
- name: Upload static deb ${{ matrix.target }}-debug-all-features to webserver
run: |
if [ ! -z $SSH_WEBSITE ]; then
scp ${{ matrix.target }}-debug.deb website:/var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/${{ matrix.target }}-debug.deb
fi
- name: Build OCI image ${{ matrix.target }}-all-features
run: |
bin/nix-build-and-cache just .#oci-image-${{ matrix.target }}-all-features
cp -v -f result oci-image-${{ matrix.target }}.tar.gz
- name: Build OCI image x86_64-linux-musl-all-features-x86_64-haswell-optimised
if: ${{ matrix.target == 'x86_64-linux-musl' }}
run: |
bin/nix-build-and-cache just .#oci-image-x86_64-linux-musl-all-features-x86_64-haswell-optimised
cp -v -f result oci-image-x86_64-linux-musl-all-features-x86_64-haswell-optimised.tar.gz
- name: Build debug OCI image ${{ matrix.target }}-all-features
run: |
bin/nix-build-and-cache just .#oci-image-${{ matrix.target }}-all-features-debug
cp -v -f result oci-image-${{ matrix.target }}-debug.tar.gz
- name: Upload OCI image x86_64-linux-musl-all-features-x86_64-haswell-optimised to GitHub
if: ${{ matrix.target == 'x86_64-linux-musl' }}
uses: actions/upload-artifact@v4
with:
name: oci-image-x86_64-linux-musl-all-features-x86_64-haswell-optimised
path: oci-image-x86_64-linux-musl-all-features-x86_64-haswell-optimised.tar.gz
if-no-files-found: error
compression-level: 0
- name: Upload OCI image ${{ matrix.target }}-all-features to GitHub
uses: actions/upload-artifact@v4
with:
name: oci-image-${{ matrix.target }}
path: oci-image-${{ matrix.target }}.tar.gz
if-no-files-found: error
compression-level: 0
- name: Upload OCI image ${{ matrix.target }}-debug-all-features to GitHub
uses: actions/upload-artifact@v4
with:
name: oci-image-${{ matrix.target }}-debug
path: oci-image-${{ matrix.target }}-debug.tar.gz
if-no-files-found: error
compression-level: 0
- name: Upload OCI image x86_64-linux-musl-all-features-x86_64-haswell-optimised.tar.gz to webserver
if: ${{ matrix.target == 'x86_64-linux-musl' }}
run: |
if [ ! -z $SSH_WEBSITE ]; then
scp oci-image-x86_64-linux-musl-all-features-x86_64-haswell-optimised.tar.gz website:/var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/oci-image-x86_64-linux-musl-all-features-x86_64-haswell-optimised.tar.gz
fi
- name: Upload OCI image ${{ matrix.target }}-all-features to webserver
run: |
if [ ! -z $SSH_WEBSITE ]; then
scp oci-image-${{ matrix.target }}.tar.gz website:/var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/oci-image-${{ matrix.target }}.tar.gz
fi
- name: Upload OCI image ${{ matrix.target }}-debug-all-features to webserver
run: |
if [ ! -z $SSH_WEBSITE ]; then
scp oci-image-${{ matrix.target }}-debug.tar.gz website:/var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/oci-image-${{ matrix.target }}-debug.tar.gz
fi
build_mac_binaries:
name: Build MacOS Binaries
strategy:
matrix:
os: [macos-latest, macos-13]
runs-on: ${{ matrix.os }}
steps:
- name: Sync repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup SSH web publish
env:
web_upload_ssh_private_key: ${{ secrets.WEB_UPLOAD_SSH_PRIVATE_KEY }}
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event.pull_request.draft != true)) && (env.web_upload_ssh_private_key != '') && github.event.pull_request.user.login != 'renovate[bot]'
run: |
mkdir -p -v ~/.ssh
echo "${{ secrets.WEB_UPLOAD_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
echo "${{ secrets.WEB_UPLOAD_SSH_PRIVATE_KEY }}" >> ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
cat >>~/.ssh/config <<END
Host website
HostName ${{ secrets.WEB_UPLOAD_SSH_HOSTNAME }}
User ${{ secrets.WEB_UPLOAD_SSH_USERNAME }}
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking yes
AddKeysToAgent no
ForwardX11 no
BatchMode yes
END
echo "Checking connection"
ssh -q website "echo test" || ssh -q website "echo test"
echo "SSH_WEBSITE=1" >> "$GITHUB_ENV"
- name: Tag comparison check
if: ${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-rc') }}
run: |
# Tag mismatch with latest repo tag check to prevent potential downgrades
LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)
if [ ${LATEST_TAG} != ${GH_REF_NAME} ]; then
echo '# WARNING: Attempting to run this workflow for a tag that is not the latest repo tag. Aborting.'
echo '# WARNING: Attempting to run this workflow for a tag that is not the latest repo tag. Aborting.' >> $GITHUB_STEP_SUMMARY
exit 1
fi
# use sccache for Rust
- name: Run sccache-cache
# we want a fresh-state when we do releases/tags to avoid potential cache poisoning attacks impacting
# releases and tags
#if: ${{ (env.SCCACHE_GHA_ENABLED == 'true') && !startsWith(github.ref, 'refs/tags/') }}
uses: mozilla-actions/sccache-action@main
# use rust-cache
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
cache-on-failure: "true"
cache-targets: "true"
# Nix can't do portable macOS builds yet
- name: Build macOS x86_64 binary
if: ${{ matrix.os == 'macos-13' }}
run: |
CONDUWUIT_VERSION_EXTRA="$(git rev-parse --short ${{ github.sha }})" cargo build --release --locked --features=perf_measurements,sentry_telemetry,direct_tls
cp -v -f target/release/conduwuit conduwuit-macos-x86_64
otool -L conduwuit-macos-x86_64
# quick smoke test of the x86_64 macOS binary
- name: Run x86_64 macOS release binary
if: ${{ matrix.os == 'macos-13' }}
run: |
./conduwuit-macos-x86_64 --help
./conduwuit-macos-x86_64 --version
- name: Build macOS arm64 binary
if: ${{ matrix.os == 'macos-latest' }}
run: |
CONDUWUIT_VERSION_EXTRA="$(git rev-parse --short ${{ github.sha }})" cargo build --release --locked --features=perf_measurements,sentry_telemetry,direct_tls
cp -v -f target/release/conduwuit conduwuit-macos-arm64
otool -L conduwuit-macos-arm64
# quick smoke test of the arm64 macOS binary
- name: Run arm64 macOS release binary
if: ${{ matrix.os == 'macos-latest' }}
run: |
./conduwuit-macos-arm64 --help
./conduwuit-macos-arm64 --version
- name: Upload macOS x86_64 binary to webserver
if: ${{ matrix.os == 'macos-13' }}
run: |
if [ ! -z $SSH_WEBSITE ]; then
chmod +x conduwuit-macos-x86_64
scp conduwuit-macos-x86_64 website:/var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/conduwuit-macos-x86_64
fi
- name: Upload macOS arm64 binary to webserver
if: ${{ matrix.os == 'macos-latest' }}
run: |
if [ ! -z $SSH_WEBSITE ]; then
chmod +x conduwuit-macos-arm64
scp conduwuit-macos-arm64 website:/var/www/girlboss.ceo/~strawberry/conduwuit/ci-bins/${WEBSERVER_DIR_NAME}/conduwuit-macos-arm64
fi
- name: Upload macOS x86_64 binary
if: ${{ matrix.os == 'macos-13' }}
uses: actions/upload-artifact@v4
with:
name: conduwuit-macos-x86_64
path: conduwuit-macos-x86_64
if-no-files-found: error
- name: Upload macOS arm64 binary
if: ${{ matrix.os == 'macos-latest' }}
uses: actions/upload-artifact@v4
with:
name: conduwuit-macos-arm64
path: conduwuit-macos-arm64
if-no-files-found: error
variables:
outputs:
github_repository: ${{ steps.var.outputs.github_repository }}
runs-on: "ubuntu-latest"
steps:
- name: Setting global variables
uses: actions/github-script@v7
id: var
with:
script: |
core.setOutput('github_repository', '${{ github.repository }}'.toLowerCase())
docker:
name: Docker publish
runs-on: ubuntu-24.04
needs: [build, variables, tests]
permissions:
packages: write
contents: read
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event.pull_request.draft != true)) && github.event.pull_request.user.login != 'renovate[bot]'
env:
DOCKER_HUB_REPO: docker.io/${{ needs.variables.outputs.github_repository }}
GHCR_REPO: ghcr.io/${{ needs.variables.outputs.github_repository }}
GLCR_REPO: registry.gitlab.com/conduwuit/conduwuit
UNIQUE_TAG: ${{ (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}-${{ github.sha }}
BRANCH_TAG: ${{ (startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-rc') && 'latest') || (github.head_ref != '' && format('merge-{0}-{1}', github.event.number, github.event.pull_request.user.login)) || github.ref_name }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
GHCR_ENABLED: "${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) && 'true' || 'false' }}"
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
if: ${{ (vars.DOCKER_USERNAME != '') && (env.DOCKERHUB_TOKEN != '') }}
uses: docker/login-action@v3
with:
registry: docker.io
username: ${{ vars.DOCKER_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitLab Container Registry
if: ${{ (vars.GITLAB_USERNAME != '') && (env.GITLAB_TOKEN != '') }}
uses: docker/login-action@v3
with:
registry: registry.gitlab.com
username: ${{ vars.GITLAB_USERNAME }}
password: ${{ secrets.GITLAB_TOKEN }}
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Move OCI images into position
run: |
mv -v oci-image-x86_64-linux-musl-all-features-x86_64-haswell-optimised/*.tar.gz oci-image-amd64-haswell-optimised.tar.gz
mv -v oci-image-x86_64-linux-musl/*.tar.gz oci-image-amd64.tar.gz
mv -v oci-image-aarch64-linux-musl/*.tar.gz oci-image-arm64v8.tar.gz
mv -v oci-image-x86_64-linux-musl-debug/*.tar.gz oci-image-amd64-debug.tar.gz
mv -v oci-image-aarch64-linux-musl-debug/*.tar.gz oci-image-arm64v8-debug.tar.gz
- name: Load and push amd64 haswell image
run: |
docker load -i oci-image-amd64-haswell-optimised.tar.gz
if [ ! -z $DOCKERHUB_TOKEN ]; then
docker tag $(docker images -q conduwuit:main) ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-haswell
docker push ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-haswell
fi
if [ $GHCR_ENABLED = "true" ]; then
docker tag $(docker images -q conduwuit:main) ${GHCR_REPO}:${UNIQUE_TAG}-haswell
docker push ${GHCR_REPO}:${UNIQUE_TAG}-haswell
fi
if [ ! -z $GITLAB_TOKEN ]; then
docker tag $(docker images -q conduwuit:main) ${GLCR_REPO}:${UNIQUE_TAG}-haswell
docker push ${GLCR_REPO}:${UNIQUE_TAG}-haswell
fi
- name: Load and push amd64 image
run: |
docker load -i oci-image-amd64.tar.gz
if [ ! -z $DOCKERHUB_TOKEN ]; then
docker tag $(docker images -q conduwuit:main) ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-amd64
docker push ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-amd64
fi
if [ $GHCR_ENABLED = "true" ]; then
docker tag $(docker images -q conduwuit:main) ${GHCR_REPO}:${UNIQUE_TAG}-amd64
docker push ${GHCR_REPO}:${UNIQUE_TAG}-amd64
fi
if [ ! -z $GITLAB_TOKEN ]; then
docker tag $(docker images -q conduwuit:main) ${GLCR_REPO}:${UNIQUE_TAG}-amd64
docker push ${GLCR_REPO}:${UNIQUE_TAG}-amd64
fi
- name: Load and push arm64 image
run: |
docker load -i oci-image-arm64v8.tar.gz
if [ ! -z $DOCKERHUB_TOKEN ]; then
docker tag $(docker images -q conduwuit:main) ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-arm64v8
docker push ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-arm64v8
fi
if [ $GHCR_ENABLED = "true" ]; then
docker tag $(docker images -q conduwuit:main) ${GHCR_REPO}:${UNIQUE_TAG}-arm64v8
docker push ${GHCR_REPO}:${UNIQUE_TAG}-arm64v8
fi
if [ ! -z $GITLAB_TOKEN ]; then
docker tag $(docker images -q conduwuit:main) ${GLCR_REPO}:${UNIQUE_TAG}-arm64v8
docker push ${GLCR_REPO}:${UNIQUE_TAG}-arm64v8
fi
- name: Load and push amd64 debug image
run: |
docker load -i oci-image-amd64-debug.tar.gz
if [ ! -z $DOCKERHUB_TOKEN ]; then
docker tag $(docker images -q conduwuit:main) ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-amd64-debug
docker push ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-amd64-debug
fi
if [ $GHCR_ENABLED = "true" ]; then
docker tag $(docker images -q conduwuit:main) ${GHCR_REPO}:${UNIQUE_TAG}-amd64-debug
docker push ${GHCR_REPO}:${UNIQUE_TAG}-amd64-debug
fi
if [ ! -z $GITLAB_TOKEN ]; then
docker tag $(docker images -q conduwuit:main) ${GLCR_REPO}:${UNIQUE_TAG}-amd64-debug
docker push ${GLCR_REPO}:${UNIQUE_TAG}-amd64-debug
fi
- name: Load and push arm64 debug image
run: |
docker load -i oci-image-arm64v8-debug.tar.gz
if [ ! -z $DOCKERHUB_TOKEN ]; then
docker tag $(docker images -q conduwuit:main) ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-arm64v8-debug
docker push ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-arm64v8-debug
fi
if [ $GHCR_ENABLED = "true" ]; then
docker tag $(docker images -q conduwuit:main) ${GHCR_REPO}:${UNIQUE_TAG}-arm64v8-debug
docker push ${GHCR_REPO}:${UNIQUE_TAG}-arm64v8-debug
fi
if [ ! -z $GITLAB_TOKEN ]; then
docker tag $(docker images -q conduwuit:main) ${GLCR_REPO}:${UNIQUE_TAG}-arm64v8-debug
docker push ${GLCR_REPO}:${UNIQUE_TAG}-arm64v8-debug
fi
- name: Create Docker haswell manifests
run: |
# Dockerhub Container Registry
if [ ! -z $DOCKERHUB_TOKEN ]; then
docker manifest create ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-haswell --amend ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-haswell
docker manifest create ${DOCKER_HUB_REPO}:${BRANCH_TAG}-haswell --amend ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-haswell
fi
# GitHub Container Registry
if [ $GHCR_ENABLED = "true" ]; then
docker manifest create ${GHCR_REPO}:${UNIQUE_TAG}-haswell --amend ${GHCR_REPO}:${UNIQUE_TAG}-haswell
docker manifest create ${GHCR_REPO}:${BRANCH_TAG}-haswell --amend ${GHCR_REPO}:${UNIQUE_TAG}-haswell
fi
# GitLab Container Registry
if [ ! -z $GITLAB_TOKEN ]; then
docker manifest create ${GLCR_REPO}:${UNIQUE_TAG}-haswell --amend ${GLCR_REPO}:${UNIQUE_TAG}-haswell
docker manifest create ${GLCR_REPO}:${BRANCH_TAG}-haswell --amend ${GLCR_REPO}:${UNIQUE_TAG}-haswell
fi
- name: Create Docker combined manifests
run: |
# Dockerhub Container Registry
if [ ! -z $DOCKERHUB_TOKEN ]; then
docker manifest create ${DOCKER_HUB_REPO}:${UNIQUE_TAG} --amend ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-arm64v8 --amend ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-amd64
docker manifest create ${DOCKER_HUB_REPO}:${BRANCH_TAG} --amend ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-arm64v8 --amend ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-amd64
fi
# GitHub Container Registry
if [ $GHCR_ENABLED = "true" ]; then
docker manifest create ${GHCR_REPO}:${UNIQUE_TAG} --amend ${GHCR_REPO}:${UNIQUE_TAG}-arm64v8 --amend ${GHCR_REPO}:${UNIQUE_TAG}-amd64
docker manifest create ${GHCR_REPO}:${BRANCH_TAG} --amend ${GHCR_REPO}:${UNIQUE_TAG}-arm64v8 --amend ${GHCR_REPO}:${UNIQUE_TAG}-amd64
fi
# GitLab Container Registry
if [ ! -z $GITLAB_TOKEN ]; then
docker manifest create ${GLCR_REPO}:${UNIQUE_TAG} --amend ${GLCR_REPO}:${UNIQUE_TAG}-arm64v8 --amend ${GLCR_REPO}:${UNIQUE_TAG}-amd64
docker manifest create ${GLCR_REPO}:${BRANCH_TAG} --amend ${GLCR_REPO}:${UNIQUE_TAG}-arm64v8 --amend ${GLCR_REPO}:${UNIQUE_TAG}-amd64
fi
- name: Create Docker combined debug manifests
run: |
# Dockerhub Container Registry
if [ ! -z $DOCKERHUB_TOKEN ]; then
docker manifest create ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-debug --amend ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-arm64v8-debug --amend ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-amd64-debug
docker manifest create ${DOCKER_HUB_REPO}:${BRANCH_TAG}-debug --amend ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-arm64v8-debug --amend ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-amd64-debug
fi
# GitHub Container Registry
if [ $GHCR_ENABLED = "true" ]; then
docker manifest create ${GHCR_REPO}:${UNIQUE_TAG}-debug --amend ${GHCR_REPO}:${UNIQUE_TAG}-arm64v8-debug --amend ${GHCR_REPO}:${UNIQUE_TAG}-amd64-debug
docker manifest create ${GHCR_REPO}:${BRANCH_TAG}-debug --amend ${GHCR_REPO}:${UNIQUE_TAG}-arm64v8-debug --amend ${GHCR_REPO}:${UNIQUE_TAG}-amd64-debug
fi
# GitLab Container Registry
if [ ! -z $GITLAB_TOKEN ]; then
docker manifest create ${GLCR_REPO}:${UNIQUE_TAG}-debug --amend ${GLCR_REPO}:${UNIQUE_TAG}-arm64v8-debug --amend ${GLCR_REPO}:${UNIQUE_TAG}-amd64-debug
docker manifest create ${GLCR_REPO}:${BRANCH_TAG}-debug --amend ${GLCR_REPO}:${UNIQUE_TAG}-arm64v8-debug --amend ${GLCR_REPO}:${UNIQUE_TAG}-amd64-debug
fi
- name: Push manifests to Docker registries
run: |
if [ ! -z $DOCKERHUB_TOKEN ]; then
docker manifest push ${DOCKER_HUB_REPO}:${UNIQUE_TAG}
docker manifest push ${DOCKER_HUB_REPO}:${BRANCH_TAG}
docker manifest push ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-debug
docker manifest push ${DOCKER_HUB_REPO}:${BRANCH_TAG}-debug
docker manifest push ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-haswell
docker manifest push ${DOCKER_HUB_REPO}:${BRANCH_TAG}-haswell
fi
if [ $GHCR_ENABLED = "true" ]; then
docker manifest push ${GHCR_REPO}:${UNIQUE_TAG}
docker manifest push ${GHCR_REPO}:${BRANCH_TAG}
docker manifest push ${GHCR_REPO}:${UNIQUE_TAG}-debug
docker manifest push ${GHCR_REPO}:${BRANCH_TAG}-debug
docker manifest push ${GHCR_REPO}:${UNIQUE_TAG}-haswell
docker manifest push ${GHCR_REPO}:${BRANCH_TAG}-haswell
fi
if [ ! -z $GITLAB_TOKEN ]; then
docker manifest push ${GLCR_REPO}:${UNIQUE_TAG}
docker manifest push ${GLCR_REPO}:${BRANCH_TAG}
docker manifest push ${GLCR_REPO}:${UNIQUE_TAG}-debug
docker manifest push ${GLCR_REPO}:${BRANCH_TAG}-debug
docker manifest push ${GLCR_REPO}:${UNIQUE_TAG}-haswell
docker manifest push ${GLCR_REPO}:${BRANCH_TAG}-haswell
fi
- name: Add Image Links to Job Summary
run: |
if [ ! -z $DOCKERHUB_TOKEN ]; then
echo "- \`docker pull ${DOCKER_HUB_REPO}:${UNIQUE_TAG}\`" >> $GITHUB_STEP_SUMMARY
echo "- \`docker pull ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-debug\`" >> $GITHUB_STEP_SUMMARY
echo "- \`docker pull ${DOCKER_HUB_REPO}:${UNIQUE_TAG}-haswell\`" >> $GITHUB_STEP_SUMMARY
fi
if [ $GHCR_ENABLED = "true" ]; then
echo "- \`docker pull ${GHCR_REPO}:${UNIQUE_TAG}\`" >> $GITHUB_STEP_SUMMARY
echo "- \`docker pull ${GHCR_REPO}:${UNIQUE_TAG}-debug\`" >> $GITHUB_STEP_SUMMARY
echo "- \`docker pull ${GHCR_REPO}:${UNIQUE_TAG}-haswell\`" >> $GITHUB_STEP_SUMMARY
fi
if [ ! -z $GITLAB_TOKEN ]; then
echo "- \`docker pull ${GLCR_REPO}:${UNIQUE_TAG}\`" >> $GITHUB_STEP_SUMMARY
echo "- \`docker pull ${GLCR_REPO}:${UNIQUE_TAG}-debug\`" >> $GITHUB_STEP_SUMMARY
echo "- \`docker pull ${GLCR_REPO}:${UNIQUE_TAG}-haswell\`" >> $GITHUB_STEP_SUMMARY
fi

View File

@@ -1,41 +0,0 @@
name: Update Docker Hub Description
on:
push:
branches:
- main
paths:
- README.md
- .github/workflows/docker-hub-description.yml
workflow_dispatch:
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
if: ${{ (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event.pull_request.draft != true)) && github.event.pull_request.user.login != 'renovate[bot]' && (vars.DOCKER_USERNAME != '') }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setting variables
uses: actions/github-script@v7
id: var
with:
script: |
const githubRepo = '${{ github.repository }}'.toLowerCase()
const repoId = githubRepo.split('/')[1]
core.setOutput('github_repository', githubRepo)
const dockerRepo = '${{ vars.DOCKER_USERNAME }}'.toLowerCase() + '/' + repoId
core.setOutput('docker_repo', dockerRepo)
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ vars.DOCKER_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: ${{ steps.var.outputs.docker_repo }}
short-description: ${{ github.event.repository.description }}
enable-url-completion: true

View File

@@ -1,163 +0,0 @@
name: Documentation and GitHub Pages
on:
pull_request:
push:
branches:
- main
tags:
- '*'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
env:
# Required to make some things output color
TERM: ansi
# Publishing to my nix binary cache
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN }}
# conduwuit.cachix.org
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
# Custom nix binary cache if fork is being used
ATTIC_ENDPOINT: ${{ vars.ATTIC_ENDPOINT }}
ATTIC_PUBLIC_KEY: ${{ vars.ATTIC_PUBLIC_KEY }}
# Get error output from nix that we can actually use, and use our binary caches for the earlier CI steps
NIX_CONFIG: |
show-trace = true
extra-substituters = extra-substituters = https://attic.kennel.juneis.dog/conduwuit https://attic.kennel.juneis.dog/conduit https://conduwuit.cachix.org https://aseipp-nix-cache.freetls.fastly.net
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o= conduwuit.cachix.org-1:MFRm6jcnfTf0jSAbmvLfhO3KBMt4px+1xaereWXp8Xg=
experimental-features = nix-command flakes
extra-experimental-features = nix-command flakes
accept-flake-config = true
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
permissions: {}
jobs:
docs:
name: Documentation and GitHub Pages
runs-on: ubuntu-24.04
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Free up a bit of runner space
run: |
set +o pipefail
sudo docker image prune --all --force || true
sudo apt purge -y 'php.*' '^mongodb-.*' '^mysql-.*' azure-cli google-cloud-cli google-chrome-stable firefox powershell microsoft-edge-stable || true
sudo apt clean
sudo rm -v -rf /usr/local/games /usr/local/sqlpackage /usr/local/share/powershell /usr/local/share/edge_driver /usr/local/share/gecko_driver /usr/local/share/chromium /usr/local/share/chromedriver-linux64 /usr/lib/google-cloud-sdk /usr/lib/jvm /usr/lib/mono /usr/lib/heroku
set -o pipefail
- name: Sync repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Setup GitHub Pages
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') && (github.event_name != 'pull_request')
uses: actions/configure-pages@v5
- uses: nixbuild/nix-quick-install-action@master
- name: Restore and cache Nix store
# we want a fresh-state when we do releases/tags to avoid potential cache poisoning attacks impacting
# releases and tags
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: nix-community/cache-nix-action@v5.1.0
with:
# restore and save a cache using this key
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/.lock') }}
# if there's no cache hit, restore a cache by this prefix
restore-prefixes-first-match: nix-${{ runner.os }}-
# collect garbage until Nix store size (in bytes) is at most this number
# before trying to save a new cache
gc-max-store-size-linux: 2073741824
# do purge caches
purge: true
# purge all versions of the cache
purge-prefixes: nix-${{ runner.os }}-
# created more than this number of seconds ago relative to the start of the `Post Restore` phase
purge-last-accessed: 86400
# except the version with the `primary-key`, if it exists
purge-primary-key: never
# always save the cache
save-always: true
- name: Enable Cachix binary cache
run: |
nix profile install nixpkgs#cachix
cachix use crane
cachix use nix-community
- name: Apply Nix binary cache configuration
run: |
sudo tee -a "${XDG_CONFIG_HOME:-$HOME/.config}/nix/nix.conf" > /dev/null <<EOF
extra-substituters = https://attic.kennel.juneis.dog/conduwuit https://attic.kennel.juneis.dog/conduit https://conduwuit.cachix.org https://aseipp-nix-cache.freetls.fastly.net
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o= conduwuit.cachix.org-1:MFRm6jcnfTf0jSAbmvLfhO3KBMt4px+1xaereWXp8Xg=
experimental-features = nix-command flakes
extra-experimental-features = nix-command flakes
accept-flake-config = true
EOF
- name: Use alternative Nix binary caches if specified
if: ${{ (env.ATTIC_ENDPOINT != '') && (env.ATTIC_PUBLIC_KEY != '') }}
run: |
sudo tee -a "${XDG_CONFIG_HOME:-$HOME/.config}/nix/nix.conf" > /dev/null <<EOF
extra-substituters = ${ATTIC_ENDPOINT}
extra-trusted-public-keys = ${ATTIC_PUBLIC_KEY}
EOF
- name: Prepare build environment
run: |
echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > "$HOME/.direnvrc"
nix profile install --inputs-from . nixpkgs#direnv nixpkgs#nix-direnv
direnv allow
nix develop --command true
- name: Cache CI dependencies
run: |
bin/nix-build-and-cache ci
- name: Run lychee and markdownlint
run: |
direnv exec . engage just lints lychee
direnv exec . engage just lints markdownlint
- name: Build documentation (book)
run: |
bin/nix-build-and-cache just .#book
cp -r --dereference result public
- name: Upload generated documentation (book) as normal artifact
uses: actions/upload-artifact@v4
with:
name: public
path: public
if-no-files-found: error
# don't compress again
compression-level: 0
- name: Upload generated documentation (book) as GitHub Pages artifact
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') && (github.event_name != 'pull_request')
uses: actions/upload-pages-artifact@v3
with:
path: public
- name: Deploy to GitHub Pages
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') && (github.event_name != 'pull_request')
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -1,118 +0,0 @@
name: Upload Release Assets
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Tag to release'
required: true
type: string
action_id:
description: 'Action ID of the CI run'
required: true
type: string
permissions: {}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: write
env:
GH_EVENT_NAME: ${{ github.event_name }}
GH_EVENT_INPUTS_ACTION_ID: ${{ github.event.inputs.action_id }}
GH_EVENT_INPUTS_TAG: ${{ github.event.inputs.tag }}
GH_REPOSITORY: ${{ github.repository }}
GH_SHA: ${{ github.sha }}
GH_TAG: ${{ github.event.release.tag_name }}
steps:
- name: get latest ci id
id: get_ci_id
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${GH_EVENT_NAME}" == "workflow_dispatch" ]; then
id="${GH_EVENT_INPUTS_ACTION_ID}"
tag="${GH_EVENT_INPUTS_TAG}"
else
# get all runs of the ci workflow
json=$(gh api "repos/${GH_REPOSITORY}/actions/workflows/ci.yml/runs")
# find first run that is github sha and status is completed
id=$(echo "$json" | jq ".workflow_runs[] | select(.head_sha == \"${GH_SHA}\" and .status == \"completed\") | .id" | head -n 1)
if [ ! "$id" ]; then
echo "No completed runs found"
echo "ci_id=0" >> "$GITHUB_OUTPUT"
exit 0
fi
tag="${GH_TAG}"
fi
echo "ci_id=$id" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: get latest ci artifacts
if: steps.get_ci_id.outputs.ci_id != 0
uses: actions/download-artifact@v4
env:
GH_TOKEN: ${{ github.token }}
with:
merge-multiple: true
run-id: ${{ steps.get_ci_id.outputs.ci_id }}
github-token: ${{ github.token }}
- run: |
ls
- name: upload release assets
if: steps.get_ci_id.outputs.ci_id != 0
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.get_ci_id.outputs.tag }}
run: |
for file in $(find . -type f); do
case "$file" in
*json*) echo "Skipping $file...";;
*) echo "Uploading $file..."; gh release upload $TAG "$file" --clobber --repo="${GH_REPOSITORY}" || echo "Something went wrong, skipping.";;
esac
done
- name: upload release assets to website
if: steps.get_ci_id.outputs.ci_id != 0
env:
TAG: ${{ steps.get_ci_id.outputs.tag }}
run: |
mkdir -p -v ~/.ssh
echo "${{ secrets.WEB_UPLOAD_SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
echo "${{ secrets.WEB_UPLOAD_SSH_PRIVATE_KEY }}" >> ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
cat >>~/.ssh/config <<END
Host website
HostName ${{ secrets.WEB_UPLOAD_SSH_HOSTNAME }}
User ${{ secrets.WEB_UPLOAD_SSH_USERNAME }}
IdentityFile ~/.ssh/id_ed25519
StrictHostKeyChecking yes
AddKeysToAgent no
ForwardX11 no
BatchMode yes
END
echo "Creating tag directory on web server"
ssh -q website "rm -rf /var/www/girlboss.ceo/~strawberry/conduwuit/releases/$TAG/"
ssh -q website "mkdir -v /var/www/girlboss.ceo/~strawberry/conduwuit/releases/$TAG/"
for file in $(find . -type f); do
case "$file" in
*json*) echo "Skipping $file...";;
*) echo "Uploading $file to website"; scp $file website:/var/www/girlboss.ceo/~strawberry/conduwuit/releases/$TAG/$file;;
esac
done

View File

@@ -1,152 +0,0 @@
stages:
- ci
- artifacts
- publish
variables:
# Makes some things print in color
TERM: ansi
# Faster cache and artifact compression / decompression
FF_USE_FASTZIP: true
# Print progress reports for cache and artifact transfers
TRANSFER_METER_FREQUENCY: 5s
NIX_CONFIG: |
show-trace = true
extra-substituters = https://attic.kennel.juneis.dog/conduit https://attic.kennel.juneis.dog/conduwuit https://conduwuit.cachix.org
extra-trusted-public-keys = conduit:eEKoUwlQGDdYmAI/Q/0slVlegqh/QmAvQd7HBSm21Wk= conduwuit:BbycGUgTISsltcmH0qNjFR9dbrQNYgdIAcmViSGoVTE= conduwuit.cachix.org-1:MFRm6jcnfTf0jSAbmvLfhO3KBMt4px+1xaereWXp8Xg=
experimental-features = nix-command flakes
extra-experimental-features = nix-command flakes
accept-flake-config = true
# Avoid duplicate pipelines
# See: https://docs.gitlab.com/ee/ci/yaml/workflow.html#switch-between-branch-pipelines-and-merge-request-pipelines
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI
before_script:
# Enable nix-command and flakes
- if command -v nix > /dev/null; then echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-experimental-features = nix-command flakes" >> /etc/nix/nix.conf; fi
# Accept flake config from "untrusted" users
- if command -v nix > /dev/null; then echo "accept-flake-config = true" >> /etc/nix/nix.conf; fi
# 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: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: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
- if command -v nix > /dev/null && [ -n "$ATTIC_PUBLIC_KEY" ]; then echo "extra-trusted-public-keys = $ATTIC_PUBLIC_KEY" >> /etc/nix/nix.conf; fi
# Add crane binary cache
- if command -v nix > /dev/null; then echo "extra-substituters = https://crane.cachix.org" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = crane.cachix.org-1:8Scfpmn9w+hGdXH/Q9tTLiYAE/2dnJYRJP7kl80GuRk=" >> /etc/nix/nix.conf; fi
# Add nix-community binary cache
- if command -v nix > /dev/null; then echo "extra-substituters = https://nix-community.cachix.org" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" >> /etc/nix/nix.conf; fi
- if command -v nix > /dev/null; then echo "extra-substituters = https://aseipp-nix-cache.freetls.fastly.net" >> /etc/nix/nix.conf; fi
# Install direnv and nix-direnv
- if command -v nix > /dev/null; then nix-env -iA nixpkgs.direnv nixpkgs.nix-direnv; fi
# Allow .envrc
- if command -v nix > /dev/null; then direnv allow; fi
# Set CARGO_HOME to a cacheable path
- export CARGO_HOME="$(git rev-parse --show-toplevel)/.gitlab-ci.d/cargo"
ci:
stage: ci
image: nixos/nix:2.24.9
script:
# Cache CI dependencies
- ./bin/nix-build-and-cache ci
- direnv exec . engage
cache:
key: nix
paths:
- target
- .gitlab-ci.d
rules:
# CI on upstream runners (only available for maintainers)
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $IS_UPSTREAM_CI == "true"
# Manual CI on unprotected branches that are not MRs
- if: $CI_PIPELINE_SOURCE != "merge_request_event" && $CI_COMMIT_REF_PROTECTED == "false"
when: manual
# Manual CI on forks
- if: $IS_UPSTREAM_CI != "true"
when: manual
- if: $CI
interruptible: true
artifacts:
stage: artifacts
image: nixos/nix:2.24.9
script:
- ./bin/nix-build-and-cache just .#static-x86_64-linux-musl
- cp result/bin/conduit x86_64-linux-musl
- mkdir -p target/release
- cp result/bin/conduit target/release
- direnv exec . cargo deb --no-build --no-strip
- mv target/debian/*.deb x86_64-linux-musl.deb
# Since the OCI image package is based on the binary package, this has the
# fun side effect of uploading the normal binary too. Conduit users who are
# deploying with Nix can leverage this fact by adding our binary cache to
# their systems.
#
# Note that although we have an `oci-image-x86_64-linux-musl`
# output, we don't build it because it would be largely redundant to this
# one since it's all containerized anyway.
- ./bin/nix-build-and-cache just .#oci-image
- cp result oci-image-amd64.tar.gz
- ./bin/nix-build-and-cache just .#static-aarch64-linux-musl
- cp result/bin/conduit aarch64-linux-musl
- ./bin/nix-build-and-cache just .#oci-image-aarch64-linux-musl
- cp result oci-image-arm64v8.tar.gz
- ./bin/nix-build-and-cache just .#book
# We can't just copy the symlink, we need to dereference it https://gitlab.com/gitlab-org/gitlab/-/issues/19746
- cp -r --dereference result public
artifacts:
paths:
- x86_64-linux-musl
- aarch64-linux-musl
- x86_64-linux-musl.deb
- oci-image-amd64.tar.gz
- oci-image-arm64v8.tar.gz
- public
rules:
# CI required for all MRs
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# Optional CI on forks
- if: $IS_UPSTREAM_CI != "true"
when: manual
allow_failure: true
- if: $CI
interruptible: true
pages:
stage: publish
dependencies:
- artifacts
only:
- next
script:
- "true"
artifacts:
paths:
- public

View File

@@ -1,8 +0,0 @@
<!-- Please describe your changes here -->
-----------------------------------------------------------------------------
- [ ] I ran `cargo fmt`, `cargo clippy`, and `cargo test`
- [ ] I agree to release my code and all other changes of this MR under the Apache-2.0 license

View File

@@ -1,3 +0,0 @@
# Docs: Map markdown to html files
- source: /docs/(.+)\.md/
public: '\1.html'

15
.mailmap Normal file
View File

@@ -0,0 +1,15 @@
AlexPewMaster <git@alex.unbox.at> <68469103+AlexPewMaster@users.noreply.github.com>
Daniel Wiesenberg <weasy@hotmail.de> <weasy666@gmail.com>
Devin Ragotzy <devin.ragotzy@gmail.com> <d6ragotzy@wmich.edu>
Devin Ragotzy <devin.ragotzy@gmail.com> <dragotzy7460@mail.kvcc.edu>
Jonas Platte <jplatte+git@posteo.de> <jplatte+gitlab@posteo.de>
Jonas Zohren <git-pbkyr@jzohren.de> <gitlab-jfowl-0ux98@sh14.de>
Jonathan de Jong <jonathan@automatia.nl> <jonathandejong02@gmail.com>
June Clementine Strawberry <june@3.dog> <june@girlboss.ceo>
June Clementine Strawberry <june@3.dog> <strawberry@pupbrain.dev>
June Clementine Strawberry <june@3.dog> <strawberry@puppygock.gay>
Olivia Lee <olivia@computer.surgery> <benjamin@computer.surgery>
Rudi Floren <rudi.floren@gmail.com> <rudi.floren@googlemail.com>
Tamara Schmitz <tamara.zoe.schmitz@posteo.de> <15906939+tamara-schmitz@users.noreply.github.com>
Timo Kösters <timo@koesters.xyz>
x4u <xi.zhu@protonmail.ch> <14617923-x4u@users.noreply.gitlab.com>

11
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"cSpell.words": [
"Forgejo",
"appservice",
"appservices",
"conduwuit",
"continuwuity",
"homeserver",
"homeservers"
]
}

1366
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,25 +13,25 @@ authors = [
]
categories = ["network-programming"]
description = "a very cool Matrix chat homeserver written in Rust"
edition = "2021"
edition = "2024"
homepage = "https://conduwuit.puppyirl.gay/"
keywords = ["chat", "matrix", "networking", "server", "uwu"]
license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://github.com/girlbossceo/conduwuit"
rust-version = "1.84.0"
rust-version = "1.86.0"
version = "0.5.0"
[workspace.metadata.crane]
name = "conduwuit"
[workspace.dependencies.arrayvec]
version = "0.7.4"
version = "0.7.6"
features = ["serde"]
[workspace.dependencies.smallvec]
version = "1.13.2"
version = "1.14.0"
features = [
"const_generics",
"const_new",
@@ -40,8 +40,12 @@ features = [
"write",
]
[workspace.dependencies.smallstr]
version = "0.3"
features = ["ffi", "std", "union"]
[workspace.dependencies.const-str]
version = "0.5.7"
version = "0.6.2"
[workspace.dependencies.ctor]
version = "0.2.9"
@@ -77,13 +81,13 @@ version = "0.8.5"
# Used for the http request / response body type for Ruma endpoints used with reqwest
[workspace.dependencies.bytes]
version = "1.9.0"
version = "1.10.1"
[workspace.dependencies.http-body-util]
version = "0.1.2"
version = "0.1.3"
[workspace.dependencies.http]
version = "1.2.0"
version = "1.3.1"
[workspace.dependencies.regex]
version = "1.11.1"
@@ -107,7 +111,7 @@ default-features = false
features = ["typed-header", "tracing"]
[workspace.dependencies.axum-server]
version = "0.7.1"
version = "0.7.2"
default-features = false
# to listen on both HTTP and HTTPS if listening on TLS dierctly from conduwuit for complement or sytest
@@ -118,7 +122,7 @@ version = "0.7"
version = "0.6.1"
[workspace.dependencies.tower]
version = "0.5.1"
version = "0.5.2"
default-features = false
features = ["util"]
@@ -137,12 +141,12 @@ features = [
]
[workspace.dependencies.rustls]
version = "0.23.19"
version = "0.23.25"
default-features = false
features = ["aws_lc_rs"]
[workspace.dependencies.reqwest]
version = "0.12.9"
version = "0.12.15"
default-features = false
features = [
"rustls-tls-native-roots",
@@ -152,12 +156,12 @@ features = [
]
[workspace.dependencies.serde]
version = "1.0.216"
version = "1.0.219"
default-features = false
features = ["rc"]
[workspace.dependencies.serde_json]
version = "1.0.133"
version = "1.0.140"
default-features = false
features = ["raw_value"]
@@ -200,13 +204,13 @@ features = [
# logging
[workspace.dependencies.log]
version = "0.4.22"
version = "0.4.27"
default-features = false
[workspace.dependencies.tracing]
version = "0.1.41"
default-features = false
[workspace.dependencies.tracing-subscriber]
version = "=0.3.18"
version = "0.3.19"
default-features = false
features = ["env-filter", "std", "tracing", "tracing-log", "ansi", "fmt"]
[workspace.dependencies.tracing-core]
@@ -220,7 +224,7 @@ default-features = false
# used for conduwuit's CLI and admin room command parsing
[workspace.dependencies.clap]
version = "4.5.23"
version = "4.5.35"
default-features = false
features = [
"derive",
@@ -233,12 +237,12 @@ features = [
]
[workspace.dependencies.futures]
version = "0.3.30"
version = "0.3.31"
default-features = false
features = ["std", "async-await"]
[workspace.dependencies.tokio]
version = "1.42.0"
version = "1.44.2"
default-features = false
features = [
"fs",
@@ -271,7 +275,7 @@ features = ["alloc", "std"]
default-features = false
[workspace.dependencies.hyper]
version = "1.5.1"
version = "1.6.0"
default-features = false
features = [
"server",
@@ -280,8 +284,7 @@ features = [
]
[workspace.dependencies.hyper-util]
# hyper-util >=0.1.9 seems to have DNS issues
version = "=0.1.8"
version = "0.1.11"
default-features = false
features = [
"server-auto",
@@ -291,7 +294,7 @@ features = [
# to support multiple variations of setting a config option
[workspace.dependencies.either]
version = "1.13.0"
version = "1.15.0"
default-features = false
features = ["serde"]
@@ -302,22 +305,27 @@ default-features = false
features = ["env", "toml"]
[workspace.dependencies.hickory-resolver]
version = "0.24.2"
version = "0.25.1"
default-features = false
features = [
"serde",
"system-config",
"tokio",
]
# Used for conduwuit::Error type
[workspace.dependencies.thiserror]
version = "2.0.7"
version = "2.0.12"
default-features = false
# Used when hashing the state
[workspace.dependencies.ring]
version = "0.17.8"
version = "0.17.14"
default-features = false
# Used to make working with iterators easier, was already a transitive depdendency
[workspace.dependencies.itertools]
version = "0.13.0"
version = "0.14.0"
# to parse user-friendly time durations in admin commands
#TODO: overlaps chrono?
@@ -333,7 +341,7 @@ version = "0.4.0"
version = "2.3.1"
[workspace.dependencies.async-trait]
version = "0.1.83"
version = "0.1.88"
[workspace.dependencies.lru-cache]
version = "0.1.2"
@@ -342,7 +350,7 @@ version = "0.1.2"
[workspace.dependencies.ruma]
git = "https://github.com/girlbossceo/ruwuma"
#branch = "conduwuit-changes"
rev = "f5667c6292adb43fbe4725d31d6b5127a0cf60ce"
rev = "920148dca1076454ca0ca5d43b5ce1aa708381d4"
features = [
"compat",
"rand",
@@ -351,8 +359,6 @@ features = [
"federation-api",
"markdown",
"push-gateway-api-c",
"state-res",
"server-util",
"unstable-exhaustive-types",
"ring-compat",
"compat-upload-signatures",
@@ -369,24 +375,27 @@ features = [
"unstable-msc3381", # polls
"unstable-msc3489", # beacon / live location
"unstable-msc3575",
"unstable-msc3930", # polls push rules
"unstable-msc4075",
"unstable-msc4095",
"unstable-msc4121",
"unstable-msc4125",
"unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4210", # remove legacy mentions
"unstable-extensible-events",
"unstable-pdu",
]
[workspace.dependencies.rust-rocksdb]
path = "deps/rust-rocksdb"
package = "rust-rocksdb-uwu"
git = "https://github.com/girlbossceo/rust-rocksdb-zaidoon1"
rev = "1c267e0bf0cc7b7702e9a329deccd89de79ef4c3"
default-features = false
features = [
"multi-threaded-cf",
"mt_static",
"lz4",
"zstd",
"zlib",
"bzip2",
]
@@ -418,7 +427,7 @@ features = ["rt-tokio"]
# optional sentry metrics for crash/panic reporting
[workspace.dependencies.sentry]
version = "0.35.0"
version = "0.37.0"
default-features = false
features = [
"backtrace",
@@ -434,9 +443,9 @@ features = [
]
[workspace.dependencies.sentry-tracing]
version = "0.35.0"
version = "0.37.0"
[workspace.dependencies.sentry-tower]
version = "0.35.0"
version = "0.37.0"
# jemalloc usage
[workspace.dependencies.tikv-jemalloc-sys]
@@ -470,7 +479,7 @@ default-features = false
features = ["resource"]
[workspace.dependencies.sd-notify]
version = "0.4.3"
version = "0.4.5"
default-features = false
[workspace.dependencies.hardened_malloc-rs]
@@ -487,25 +496,25 @@ version = "0.4.3"
default-features = false
[workspace.dependencies.termimad]
version = "0.31.1"
version = "0.31.2"
default-features = false
[workspace.dependencies.checked_ops]
version = "0.1"
[workspace.dependencies.syn]
version = "2.0.90"
version = "2.0"
default-features = false
features = ["full", "extra-traits"]
[workspace.dependencies.quote]
version = "1.0.37"
version = "1.0"
[workspace.dependencies.proc-macro2]
version = "1.0.89"
version = "1.0"
[workspace.dependencies.bytesize]
version = "1.3.0"
version = "2.0"
[workspace.dependencies.core_affinity]
version = "0.8.1"
@@ -517,13 +526,16 @@ version = "0.2"
version = "0.2"
[workspace.dependencies.minicbor]
version = "0.25.1"
version = "0.26.3"
features = ["std"]
[workspace.dependencies.minicbor-serde]
version = "0.3.2"
version = "0.4.1"
features = ["std"]
[workspace.dependencies.maplit]
version = "1.0.2"
#
# Patches
#
@@ -533,16 +545,16 @@ features = ["std"]
# https://github.com/girlbossceo/tracing/commit/b348dca742af641c47bc390261f60711c2af573c
[patch.crates-io.tracing-subscriber]
git = "https://github.com/girlbossceo/tracing"
rev = "05825066a6d0e9ad6b80dcf29457eb179ff4768c"
rev = "1e64095a8051a1adf0d1faa307f9f030889ec2aa"
[patch.crates-io.tracing]
git = "https://github.com/girlbossceo/tracing"
rev = "05825066a6d0e9ad6b80dcf29457eb179ff4768c"
rev = "1e64095a8051a1adf0d1faa307f9f030889ec2aa"
[patch.crates-io.tracing-core]
git = "https://github.com/girlbossceo/tracing"
rev = "05825066a6d0e9ad6b80dcf29457eb179ff4768c"
rev = "1e64095a8051a1adf0d1faa307f9f030889ec2aa"
[patch.crates-io.tracing-log]
git = "https://github.com/girlbossceo/tracing"
rev = "05825066a6d0e9ad6b80dcf29457eb179ff4768c"
rev = "1e64095a8051a1adf0d1faa307f9f030889ec2aa"
# adds a tab completion callback: https://github.com/girlbossceo/rustyline-async/commit/de26100b0db03e419a3d8e1dd26895d170d1fe50
# adds event for CTRL+\: https://github.com/girlbossceo/rustyline-async/commit/67d8c49aeac03a5ef4e818f663eaa94dd7bf339b
@@ -558,10 +570,23 @@ rev = "fe4aebeeaae435af60087ddd56b573a2e0be671d"
git = "https://github.com/girlbossceo/async-channel"
rev = "92e5e74063bf2a3b10414bcc8a0d68b235644280"
# adds affinity masks for selecting more than one core at a time
[patch.crates-io.core_affinity]
git = "https://github.com/girlbossceo/core_affinity_rs"
rev = "9c8e51510c35077df888ee72a36b4b05637147da"
# reverts hyperium#148 conflicting with our delicate federation resolver hooks
[patch.crates-io.hyper-util]
git = "https://github.com/girlbossceo/hyper-util"
rev = "e4ae7628fe4fcdacef9788c4c8415317a4489941"
# allows no-aaaa option in resolv.conf
# bumps rust edition and toolchain to 1.86.0 and 2024
# use sat_add on line number errors
[patch.crates-io.resolv-conf]
git = "https://github.com/girlbossceo/resolv-conf"
rev = "200e958941d522a70c5877e3d846f55b5586c68d"
#
# Our crates
#
@@ -678,7 +703,7 @@ inherits = "release"
# To enable hot-reloading:
# 1. Uncomment all of the rustflags here.
# 2. Uncomment crate-type=dylib in src/*/Cargo.toml and deps/rust-rocksdb/Cargo.toml
# 2. Uncomment crate-type=dylib in src/*/Cargo.toml
#
# opt-level, mir-opt-level, validate-mir are not known to interfere with reloading
# and can be raised if build times are tolerable.
@@ -746,27 +771,6 @@ inherits = "dev"
# '-Clink-arg=-Wl,-z,lazy',
#]
[profile.dev.package.rust-rocksdb-uwu]
inherits = "dev"
debug = 'limited'
incremental = false
codegen-units = 1
opt-level = 'z'
#rustflags = [
# '--cfg', 'conduwuit_mods',
# '-Ztls-model=initial-exec',
# '-Cprefer-dynamic=true',
# '-Zstaticlib-prefer-dynamic=true',
# '-Zstaticlib-allow-rdylib-deps=true',
# '-Zpacked-bundled-libs=true',
# '-Zplt=true',
# '-Clink-arg=-Wl,--no-as-needed',
# '-Clink-arg=-Wl,--allow-shlib-undefined',
# '-Clink-arg=-Wl,-z,lazy',
# '-Clink-arg=-Wl,-z,nodlopen',
# '-Clink-arg=-Wl,-z,nodelete',
#]
[profile.dev.package.'*']
inherits = "dev"
debug = 'limited'
@@ -854,6 +858,9 @@ unused_crate_dependencies = "allow"
unsafe_code = "allow"
variant_size_differences = "allow"
# we check nightly clippy lints
unknown_lints = "allow"
#######################################
#
# Clippy lints
@@ -898,9 +905,11 @@ missing_docs_in_private_items = { level = "allow", priority = 1 }
missing_errors_doc = { level = "allow", priority = 1 }
missing_panics_doc = { level = "allow", priority = 1 }
module_name_repetitions = { level = "allow", priority = 1 }
needless_continue = { level = "allow", priority = 1 }
no_effect_underscore_binding = { level = "allow", priority = 1 }
similar_names = { level = "allow", priority = 1 }
single_match_else = { level = "allow", priority = 1 }
struct_excessive_bools = { level = "allow", priority = 1 }
struct_field_names = { level = "allow", priority = 1 }
unnecessary_wraps = { level = "allow", priority = 1 }
unused_async = { level = "allow", priority = 1 }
@@ -962,9 +971,13 @@ style = { level = "warn", priority = -1 }
# trivial assertions are quite alright
assertions_on_constants = { level = "allow", priority = 1 }
module_inception = { level = "allow", priority = 1 }
obfuscated_if_else = { level = "allow", priority = 1 }
###################
suspicious = { level = "warn", priority = -1 }
## some sadness
let_underscore_future = { level = "allow", priority = 1 }
# rust doesnt understand conduwuit's custom log macros
literal_string_with_formatting_args = { level = "allow", priority = 1 }

191
README.md
View File

@@ -1,146 +1,113 @@
# conduwuit
[![conduwuit main room](https://img.shields.io/matrix/conduwuit%3Apuppygock.gay?server_fqdn=matrix.transfem.dev&style=flat&logo=matrix&logoColor=%23f5b3ff&label=%23conduwuit%3Apuppygock.gay&color=%23f652ff)](https://matrix.to/#/#conduwuit:puppygock.gay) [![conduwuit space](https://img.shields.io/matrix/conduwuit-space%3Apuppygock.gay?server_fqdn=matrix.transfem.dev&style=flat&logo=matrix&logoColor=%23f5b3ff&label=%23conduwuit-space%3Apuppygock.gay&color=%23f652ff)](https://matrix.to/#/#conduwuit-space:puppygock.gay) [![CI and Artifacts](https://github.com/girlbossceo/conduwuit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/girlbossceo/conduwuit/actions/workflows/ci.yml)
# continuwuity
<!-- ANCHOR: catchphrase -->
### a very cool [Matrix](https://matrix.org/) chat homeserver written in Rust
## A community-driven [Matrix](https://matrix.org/) homeserver in Rust
<!-- ANCHOR_END: catchphrase -->
Visit the [conduwuit documentation](https://conduwuit.puppyirl.gay/) for more
information and how to deploy/setup conduwuit.
[continuwuity] is a Matrix homeserver written in Rust.
It's a community continuation of the [conduwuit](https://github.com/girlbossceo/conduwuit) homeserver.
<!-- ANCHOR: body -->
#### What is Matrix?
### Why does this exist?
The original conduwuit project has been archived and is no longer maintained. Rather than letting this Rust-based Matrix homeserver disappear, a group of community contributors have forked the project to continue its development, fix outstanding issues, and add new features.
We aim to provide a stable, well-maintained alternative for current Conduit users and welcome newcomers seeking a lightweight, efficient Matrix homeserver.
### Who are we?
We are a group of Matrix enthusiasts, developers and system administrators who have used conduwuit and believe in its potential. Our team includes both previous
contributors to the original project and new developers who want to help maintain and improve this important piece of Matrix infrastructure.
We operate as an open community project, welcoming contributions from anyone interested in improving continuwuity.
### What is Matrix?
[Matrix](https://matrix.org) is an open, federated, and extensible network for
decentralised communication. Users from any Matrix homeserver can chat with users from all
decentralized communication. Users from any Matrix homeserver can chat with users from all
other homeservers over federation. Matrix is designed to be extensible and built on top of.
You can even use bridges such as Matrix Appservices to communicate with users outside of Matrix, like a community on Discord.
#### What is the goal?
### What are the project's goals?
A high-performance, efficient, low-cost, and featureful Matrix homeserver that's
easy to set up and just works with minimal configuration needed.
Continuwuity aims to:
#### Can I try it out?
- Maintain a stable, reliable Matrix homeserver implementation in Rust
- Improve compatibility and specification compliance with the Matrix protocol
- Fix bugs and performance issues from the original conduwuit
- Add missing features needed by homeserver administrators
- Provide comprehensive documentation and easy deployment options
- Create a sustainable development model for long-term maintenance
- Keep a lightweight, efficient codebase that can run on modest hardware
An official conduwuit server ran by me is available at transfem.dev
([element.transfem.dev](https://element.transfem.dev) /
[cinny.transfem.dev](https://cinny.transfem.dev))
### Can I try it out?
transfem.dev is a public homeserver that can be used, it is not a "test only
homeserver". This means there are rules, so please read the rules:
[https://transfem.dev/homeserver_rules.txt](https://transfem.dev/homeserver_rules.txt)
Not right now. We've still got work to do!
transfem.dev is also listed at
[servers.joinmatrix.org](https://servers.joinmatrix.org/), which is a list of
popular public Matrix homeservers, including some others that run conduwuit.
#### What is the current status?
### What are we working on?
conduwuit is technically a hard fork of [Conduit](https://conduit.rs/), which is in beta.
The beta status initially was inherited from Conduit, however the huge amount of
codebase divergance, changes, fixes, and improvements have effectively made this
beta status not entirely applicable to us anymore.
We're working our way through all of the issues in the [Forgejo project](https://forgejo.ellis.link/continuwuation/continuwuity/issues).
conduwuit is very stable based on our rapidly growing userbase, has lots of features that users
expect, and very usable as a daily driver for small, medium, and upper-end medium sized homeservers.
- [Replacing old conduwuit links with working continuwuity links](https://forgejo.ellis.link/continuwuation/continuwuity/issues/742)
- [Getting CI and docs deployment working on the new Forgejo project](https://forgejo.ellis.link/continuwuation/continuwuity/issues/740)
- [Packaging & availability in more places](https://forgejo.ellis.link/continuwuation/continuwuity/issues/747)
- [Appservices bugs & features](https://forgejo.ellis.link/continuwuation/continuwuity/issues?q=&type=all&state=open&labels=178&milestone=0&assignee=0&poster=0)
- [Improving compatibility and spec compliance](https://forgejo.ellis.link/continuwuation/continuwuity/issues?labels=119)
- Automated testing
- [Admin API](https://forgejo.ellis.link/continuwuation/continuwuity/issues/748)
- [Policy-list controlled moderation](https://forgejo.ellis.link/continuwuation/continuwuity/issues/750)
A lot of critical stability and performance issues have been fixed, and a lot of
necessary groundwork has finished; making this project way better than it was
back in the start at ~early 2024.
### Can I migrate my data from x?
#### How is conduwuit funded? Is conduwuit sustainable?
conduwuit has no external funding. This is made possible purely in my freetime with
contributors, also in their free time, and only by user-curated donations.
conduwuit has existed since around November 2023, but [only became more publicly known
in March/April 2024](https://matrix.org/blog/2024/04/26/this-week-in-matrix-2024-04-26/#conduwuit-website)
and we have no plans in stopping or slowing down any time soon!
#### Can I migrate or switch from Conduit?
conduwuit is a complete drop-in replacement for Conduit. As long as you are using RocksDB,
the only "migration" you need to do is replace the binary or container image. There
is no harm or additional steps required for using conduwuit. See the
[Migrating from Conduit](https://conduwuit.puppyirl.gay/deploying/generic.html#migrating-from-conduit) section
on the generic deploying guide.
Note that as of conduwuit version 0.5.0, backwards compatibility with Conduit is
no longer supported. We only support migrating *from* Conduit, not back to
Conduit like before. If you are truly finding yourself wanting to migrate back
to Conduit, we would appreciate all your feedback and if we can assist with
any issues or concerns.
#### Can I migrate from Synapse or Dendrite?
Currently there is no known way to seamlessly migrate all user data from the old
homeserver to conduwuit. However it is perfectly acceptable to replace the old
homeserver software with conduwuit using the same server name and there will not
be any issues with federation.
There is an interest in developing a built-in seamless user data migration
method into conduwuit, however there is no concrete ETA or timeline for this.
- Conduwuit: Yes
- Conduit: No, database is now incompatible
- Grapevine: No, database is now incompatible
- Dendrite: No
- Synapse: No
We haven't written up a guide on migrating from incompatible homeservers yet. Reach out to us if you need to do this!
<!-- ANCHOR_END: body -->
## Contribution
### Development flow
- Features / changes must developed in a separate branch
- For each change, create a descriptive PR
- Your code will be reviewed by one or more of the continuwuity developers
- The branch will be deployed live on multiple tester's matrix servers to shake out bugs
- Once all testers and reviewers have agreed, the PR will be merged to the main branch
- The main branch will have nightly builds deployed to users on the cutting edge
- Every week or two, a new release is cut.
The main branch is always green!
### Policy on pulling from other forks
We welcome contributions from other forks of conduwuit, subject to our review process.
When incorporating code from other forks:
- All external contributions must go through our standard PR process
- Code must meet our quality standards and pass tests
- Code changes will require testing on multiple test servers before merging
- Attribution will be given to original authors and forks
- We prioritize stability and compatibility when evaluating external contributions
- Features that align with our project goals will be given priority consideration
<!-- ANCHOR: footer -->
#### Contact
[`#conduwuit:puppygock.gay`](https://matrix.to/#/#conduwuit:puppygock.gay)
is the official project Matrix room. You can get support here, ask questions or
concerns, get assistance setting up conduwuit, etc.
This room should stay relevant and focused on conduwuit. An offtopic general
chatter room can be found there as well.
Please keep the issue trackers focused on bug reports and enhancement requests.
General support is extremely difficult to be offered over an issue tracker, and
simple questions should be asked directly in an interactive platform like our
Matrix room above as they can turn into a relevant discussion and/or may not be
simple to answer. If you're not sure, just ask in the Matrix room.
If you have a bug or feature to request: [Open an issue on GitHub](https://github.com/girlbossceo/conduwuit/issues/new)
#### Donate
conduwuit development is purely made possible by myself and contributors. I do
not get paid to work on this, and I work on it in my free time. Donations are
heavily appreciated! 💜🥺
- Liberapay (preferred): <https://liberapay.com/girlbossceo>
- GitHub Sponsors (preferred): <https://github.com/sponsors/girlbossceo>
- Ko-fi: <https://ko-fi.com/puppygock>
I do not and will not accept cryptocurrency donations, including things related.
#### Logo
Original repo and Matrix room picture was from bran (<3). Current banner image
and logo is directly from [this cohost
post](https://web.archive.org/web/20241126004041/https://cohost.org/RatBaby/post/1028290-finally-a-flag-for).
#### Is it conduwuit or Conduwuit?
Both, but I prefer conduwuit.
#### Mirrors of conduwuit
If GitHub is unavailable in your country, or has poor connectivity, conduwuit's
source code is mirrored onto the following additional platforms I maintain:
- GitHub: <https://github.com/girlbossceo/conduwuit>
- GitLab: <https://gitlab.com/conduwuit/conduwuit>
- git.girlcock.ceo: <https://git.girlcock.ceo/strawberry/conduwuit>
- git.gay: <https://git.gay/june/conduwuit>
- mau.dev: <https://mau.dev/june/conduwuit>
- Codeberg: <https://codeberg.org/arf/conduwuit>
- sourcehut: <https://git.sr.ht/~girlbossceo/conduwuit>
<!-- TODO: contact details -->
<!-- ANCHOR_END: footer -->
[continuwuity]: https://forgejo.ellis.link/continuwuation/continuwuity

View File

@@ -4,6 +4,7 @@ Wants=network-online.target
After=network-online.target
Documentation=https://conduwuit.puppyirl.gay/
RequiresMountsFor=/var/lib/private/conduwuit
Alias=matrix-conduwuit.service
[Service]
DynamicUser=yes

View File

@@ -10,15 +10,15 @@ set -euo pipefail
COMPLEMENT_SRC="${COMPLEMENT_SRC:-$1}"
# A `.jsonl` file to write test logs to
LOG_FILE="$2"
LOG_FILE="${2:-complement_test_logs.jsonl}"
# A `.jsonl` file to write test results to
RESULTS_FILE="$3"
RESULTS_FILE="${3:-complement_test_results.jsonl}"
OCI_IMAGE="complement-conduwuit:main"
COMPLEMENT_BASE_IMAGE="${COMPLEMENT_BASE_IMAGE:-complement-conduwuit:main}"
# Complement tests that are skipped due to flakiness/reliability issues
SKIPPED_COMPLEMENT_TESTS='-skip=TestClientSpacesSummary.*|TestJoinFederatedRoomFromApplicationServiceBridgeUser.*|TestJumpToDateEndpoint.*|TestUnbanViaInvite.*'
# Complement tests that are skipped due to flakiness/reliability issues or we don't implement such features and won't for a long time
SKIPPED_COMPLEMENT_TESTS='TestPartialStateJoin.*|TestRoomDeleteAlias/Parallel/Regular_users_can_add_and_delete_aliases_when_m.*|TestRoomDeleteAlias/Parallel/Can_delete_canonical_alias|TestUnbanViaInvite.*|TestRoomState/Parallel/GET_/publicRooms_lists.*"|TestRoomDeleteAlias/Parallel/Users_with_sufficient_power-level_can_delete_other.*'
# $COMPLEMENT_SRC needs to be a directory to Complement source code
if [ -f "$COMPLEMENT_SRC" ]; then
@@ -34,18 +34,41 @@ toplevel="$(git rev-parse --show-toplevel)"
pushd "$toplevel" > /dev/null
#bin/nix-build-and-cache just .#linux-complement
bin/nix-build-and-cache just .#complement
if [ ! -f "complement_oci_image.tar.gz" ]; then
echo "building complement conduwuit image"
docker load < result
popd > /dev/null
# if using macOS, use linux-complement
#bin/nix-build-and-cache just .#linux-complement
bin/nix-build-and-cache just .#complement
#nix build -L .#complement
echo "complement conduwuit image tar.gz built at \"result\""
echo "loading into docker"
docker load < result
popd > /dev/null
else
echo "skipping building a complement conduwuit image as complement_oci_image.tar.gz was already found, loading this"
docker load < complement_oci_image.tar.gz
popd > /dev/null
fi
echo ""
echo "running go test with:"
echo "\$COMPLEMENT_SRC: $COMPLEMENT_SRC"
echo "\$COMPLEMENT_BASE_IMAGE: $COMPLEMENT_BASE_IMAGE"
echo "\$RESULTS_FILE: $RESULTS_FILE"
echo "\$LOG_FILE: $LOG_FILE"
echo ""
# It's okay (likely, even) that `go test` exits nonzero
# `COMPLEMENT_ENABLE_DIRTY_RUNS=1` reuses the same complement container for faster complement, at the possible expense of test environment pollution
set +o pipefail
env \
-C "$COMPLEMENT_SRC" \
COMPLEMENT_BASE_IMAGE="$OCI_IMAGE" \
go test -tags="conduwuit_blacklist" "$SKIPPED_COMPLEMENT_TESTS" -v -timeout 1h -json ./tests | tee "$LOG_FILE"
COMPLEMENT_BASE_IMAGE="$COMPLEMENT_BASE_IMAGE" \
go test -tags="conduwuit_blacklist" -skip="$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
@@ -55,3 +78,18 @@ cat "$LOG_FILE" | jq -s -c 'sort_by(.Test)[]' | jq -c '
and .Test != null
) | {Action: .Action, Test: .Test}
' > "$RESULTS_FILE"
#if command -v gotestfmt &> /dev/null; then
# echo "using gotestfmt on $LOG_FILE"
# grep '{"Time":' "$LOG_FILE" | gotestfmt > "complement_test_logs_gotestfmt.log"
#fi
echo ""
echo ""
echo "complement logs saved at $LOG_FILE"
echo "complement results saved at $RESULTS_FILE"
#if command -v gotestfmt &> /dev/null; then
# echo "complement logs in gotestfmt pretty format outputted at complement_test_logs_gotestfmt.log (use an editor/terminal/pager that interprets ANSI colours and UTF-8 emojis)"
#fi
echo ""
echo ""

View File

@@ -13,12 +13,15 @@ create-missing = true
extra-watch-dirs = ["debian", "docs"]
[rust]
edition = "2021"
edition = "2024"
[output.html]
git-repository-url = "https://github.com/girlbossceo/conduwuit"
edit-url-template = "https://github.com/girlbossceo/conduwuit/edit/main/{path}"
git-repository-icon = "fa-github-square"
[output.html.redirect]
"/differences.html" = "https://conduwuit.puppyirl.gay/#where-is-the-differences-page"
[output.html.search]
limit-results = 15

View File

@@ -2,9 +2,10 @@ 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 = 196608 # reduce me ALARA
stack-size-threshold = 196608 # TODO reduce me ALARA
too-many-lines-threshold = 780 # TODO reduce me to <= 100
type-complexity-threshold = 250 # reduce me to ~200
large-error-threshold = 256 # TODO reduce me ALARA
disallowed-macros = [
{ path = "log::error", reason = "use conduwuit_core::error" },

View File

@@ -195,14 +195,6 @@
#
#servernameevent_data_cache_capacity = varies by system
# This item is undocumented. Please contribute documentation for it.
#
#server_visibility_cache_capacity = varies by system
# This item is undocumented. Please contribute documentation for it.
#
#user_visibility_cache_capacity = varies by system
# This item is undocumented. Please contribute documentation for it.
#
#stateinfo_cache_capacity = varies by system
@@ -445,10 +437,19 @@
#
#allow_federation = true
# This item is undocumented. Please contribute documentation for it.
# Allows federation requests to be made to itself
#
# This isn't intended and is very likely a bug if federation requests are
# being sent to yourself. This currently mainly exists for development
# purposes.
#
#federation_loopback = false
# Always calls /forget on behalf of the user if leaving a room. This is a
# part of MSC4267 "Automatically forgetting rooms on leave"
#
#forget_forced_upon_leave = false
# Set this to true to require authentication on the normally
# unauthenticated profile retrieval endpoints (GET)
# "/_matrix/client/v3/profile/{userId}".
@@ -526,9 +527,9 @@
# Default room version conduwuit will create rooms with.
#
# Per spec, room version 10 is the default.
# Per spec, room version 11 is the default.
#
#default_room_version = 10
#default_room_version = 11
# This item is undocumented. Please contribute documentation for it.
#
@@ -593,7 +594,7 @@
# Currently, conduwuit doesn't support inbound batched key requests, so
# this list should only contain other Synapse servers.
#
# example: ["matrix.org", "envs.net", "constellatory.net", "tchncs.de"]
# example: ["matrix.org", "tchncs.de"]
#
#trusted_servers = ["matrix.org"]
@@ -821,7 +822,7 @@
# Type of RocksDB database compression to use.
#
# Available options are "zstd", "zlib", "bz2", "lz4", or "none".
# Available options are "zstd", "bz2", "lz4", or "none".
#
# It is best to use ZSTD as an overall good balance between
# speed/performance, storage, IO amplification, and CPU usage. For more
@@ -925,6 +926,13 @@
#
#rocksdb_checksums = true
# Enables the "atomic flush" mode in rocksdb. This option is not intended
# for users. It may be removed or ignored in future versions. Atomic flush
# may be enabled by the paranoid to possibly improve database integrity at
# the cost of performance.
#
#rocksdb_atomic_flush = false
# Database repair mode (for RocksDB SST corruption).
#
# Use this option when the server reports corruption while running or
@@ -1178,13 +1186,16 @@
#
#prune_missing_media = false
# Vector list of servers that conduwuit will refuse to download remote
# media from.
# Vector list of regex patterns of server names that conduwuit will refuse
# to download remote media from.
#
# example: ["badserver\.tld$", "badphrase", "19dollarfortnitecards"]
#
#prevent_media_downloads_from = []
# List of forbidden server names that we will block incoming AND outgoing
# federation with, and block client room joins / remote user invites.
# List of forbidden server names via regex patterns that we will block
# incoming AND outgoing federation with, and block client room joins /
# remote user invites.
#
# This check is applied on the room ID, room alias, sender server name,
# sender user's server name, inbound federation X-Matrix origin, and
@@ -1192,11 +1203,15 @@
#
# Basically "global" ACLs.
#
# example: ["badserver\.tld$", "badphrase", "19dollarfortnitecards"]
#
#forbidden_remote_server_names = []
# List of forbidden server names that we will block all outgoing federated
# room directory requests for. Useful for preventing our users from
# wandering into bad servers or spaces.
# List of forbidden server names via regex patterns that we will block all
# outgoing federated room directory requests for. Useful for preventing
# our users from wandering into bad servers or spaces.
#
# example: ["badserver\.tld$", "badphrase", "19dollarfortnitecards"]
#
#forbidden_remote_room_directory_server_names = []
@@ -1307,7 +1322,7 @@
# used, and startup as warnings if any room aliases in your database have
# a forbidden room alias/ID.
#
# example: ["19dollarfortnitecards", "b[4a]droom"]
# example: ["19dollarfortnitecards", "b[4a]droom", "badphrase"]
#
#forbidden_alias_names = []
@@ -1320,7 +1335,7 @@
# startup as warnings if any local users in your database have a forbidden
# username.
#
# example: ["administrator", "b[a4]dusernam[3e]"]
# example: ["administrator", "b[a4]dusernam[3e]", "badphrase"]
#
#forbidden_usernames = []
@@ -1413,7 +1428,7 @@
# Sentry reporting URL, if a custom one is desired.
#
#sentry_endpoint = "https://fe2eb4536aa04949e28eff3128d64757@o4506996327251968.ingest.us.sentry.io/4506996334657536"
#sentry_endpoint = ""
# Report your conduwuit server_name in Sentry.io crash reports and
# metrics.

View File

@@ -2,26 +2,14 @@
Description=conduwuit Matrix homeserver
Wants=network-online.target
After=network-online.target
Alias=matrix-conduwuit.service
Documentation=https://conduwuit.puppyirl.gay/
[Service]
DynamicUser=yes
User=conduwuit
Group=conduwuit
Type=notify-reload
ReloadSignal=SIGUSR1
TTYPath=/dev/tty25
DeviceAllow=char-tty
StandardInput=tty-force
StandardOutput=tty
StandardError=journal+console
TTYReset=yes
# uncomment to allow buffer to be cleared every restart
TTYVTDisallocate=no
TTYColumns=120
TTYRows=40
Type=notify
Environment="CONDUWUIT_CONFIG=/etc/conduwuit/conduwuit.toml"

View File

@@ -1,42 +0,0 @@
[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 = ["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/girlbossceo/rust-rocksdb-zaidoon1"
rev = "7b0e1bbe395a41ba8a11347a4921da590e3ad0d9"
#branch = "master"
default-features = false
[lib]
path = "lib.rs"
crate-type = [
"rlib",
# "dylib"
]
[lints]
workspace = true

View File

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

View File

@@ -1,7 +1,6 @@
# Summary
- [Introduction](introduction.md)
- [Differences from upstream Conduit](differences.md)
- [Configuration](configuration.md)
- [Examples](configuration/examples.md)
- [Deploying](deploying.md)

View File

@@ -0,0 +1,36 @@
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="100%"
viewBox="0 0 864 864"
enableBackground="new 0 0 864 864"
xmlSpace="preserve"
>
<path
fill="#EC008C"
opacity="1.000000"
stroke="none"
d="M0.999997,649.000000 C1.000000,433.052795 1.000000,217.105591 1.000000,1.079198 C288.876801,1.079198 576.753601,1.079198 865.000000,1.079198 C865.000000,73.025414 865.000000,145.051453 864.634888,217.500671 C852.362488,223.837280 840.447632,229.735275 828.549438,235.666794 C782.143677,258.801056 735.743225,281.945923 688.998657,304.980469 C688.122009,304.476532 687.580750,304.087708 687.053894,303.680206 C639.556946,266.944733 573.006775,291.446869 560.804199,350.179443 C560.141357,353.369446 559.717590,356.609131 559.195374,359.748962 C474.522705,359.748962 390.283478,359.748962 306.088135,359.748962 C298.804138,318.894806 265.253357,295.206024 231.834442,293.306793 C201.003021,291.554596 169.912033,310.230042 156.935104,338.792725 C149.905151,354.265930 147.884064,370.379944 151.151794,387.034515 C155.204453,407.689667 166.300507,423.954224 183.344437,436.516663 C181.938263,437.607025 180.887405,438.409576 179.849426,439.228516 C147.141953,465.032562 139.918045,510.888947 163.388611,545.322632 C167.274551,551.023804 172.285187,555.958313 176.587341,561.495728 C125.846893,587.012817 75.302292,612.295532 24.735992,637.534790 C16.874903,641.458496 8.914484,645.183228 0.999997,649.000000 z"
/>
<path
fill="#000000"
opacity="1.000000"
stroke="none"
d="M689.340759,305.086823 C735.743225,281.945923 782.143677,258.801056 828.549438,235.666794 C840.447632,229.735275 852.362488,223.837280 864.634888,217.961929 C865.000000,433.613190 865.000000,649.226379 865.000000,864.919800 C577.000000,864.919800 289.000000,864.919800 1.000000,864.919800 C1.000000,793.225708 1.000000,721.576721 0.999997,649.463867 C8.914484,645.183228 16.874903,641.458496 24.735992,637.534790 C75.302292,612.295532 125.846893,587.012817 176.939667,561.513062 C178.543060,562.085083 179.606812,562.886414 180.667526,563.691833 C225.656799,597.853394 291.232574,574.487244 304.462524,519.579773 C304.989105,517.394409 305.501068,515.205505 305.984619,513.166748 C391.466370,513.166748 476.422729,513.166748 561.331177,513.166748 C573.857727,555.764343 608.978149,572.880920 638.519897,572.672791 C671.048340,572.443665 700.623230,551.730408 711.658752,520.910583 C722.546875,490.502106 715.037842,453.265564 682.776733,429.447052 C683.966064,428.506866 685.119507,427.602356 686.265320,426.688232 C712.934143,405.412262 723.011475,370.684631 711.897339,338.686676 C707.312805,325.487671 699.185303,314.725128 689.340759,305.086823 z"
/>
<path
fill="#FEFBFC"
opacity="1.000000"
stroke="none"
d="M688.998657,304.980469 C699.185303,314.725128 707.312805,325.487671 711.897339,338.686676 C723.011475,370.684631 712.934143,405.412262 686.265320,426.688232 C685.119507,427.602356 683.966064,428.506866 682.776733,429.447052 C715.037842,453.265564 722.546875,490.502106 711.658752,520.910583 C700.623230,551.730408 671.048340,572.443665 638.519897,572.672791 C608.978149,572.880920 573.857727,555.764343 561.331177,513.166748 C476.422729,513.166748 391.466370,513.166748 305.984619,513.166748 C305.501068,515.205505 304.989105,517.394409 304.462524,519.579773 C291.232574,574.487244 225.656799,597.853394 180.667526,563.691833 C179.606812,562.886414 178.543060,562.085083 177.128418,561.264465 C172.285187,555.958313 167.274551,551.023804 163.388611,545.322632 C139.918045,510.888947 147.141953,465.032562 179.849426,439.228516 C180.887405,438.409576 181.938263,437.607025 183.344437,436.516663 C166.300507,423.954224 155.204453,407.689667 151.151794,387.034515 C147.884064,370.379944 149.905151,354.265930 156.935104,338.792725 C169.912033,310.230042 201.003021,291.554596 231.834442,293.306793 C265.253357,295.206024 298.804138,318.894806 306.088135,359.748962 C390.283478,359.748962 474.522705,359.748962 559.195374,359.748962 C559.717590,356.609131 560.141357,353.369446 560.804199,350.179443 C573.006775,291.446869 639.556946,266.944733 687.053894,303.680206 C687.580750,304.087708 688.122009,304.476532 688.998657,304.980469 M703.311279,484.370789 C698.954468,457.053253 681.951416,440.229645 656.413696,429.482330 C673.953552,421.977875 688.014709,412.074219 696.456482,395.642365 C704.862061,379.280853 706.487793,362.316345 700.947998,344.809204 C691.688965,315.548492 664.183716,296.954437 633.103516,298.838257 C618.467957,299.725372 605.538086,305.139557 594.588501,314.780121 C577.473999,329.848511 570.185486,349.121399 571.838501,371.750854 C479.166595,371.750854 387.082886,371.750854 294.582672,371.750854 C293.993011,354.662048 288.485260,339.622314 276.940491,327.118439 C265.392609,314.611176 251.082092,307.205322 234.093262,305.960541 C203.355347,303.708374 176.337585,320.898438 166.089890,348.816620 C159.557541,366.613007 160.527206,384.117401 168.756042,401.172516 C177.054779,418.372589 191.471954,428.832886 207.526581,435.632172 C198.407059,442.272583 188.815598,448.302246 180.383728,455.660675 C171.685028,463.251984 166.849655,473.658661 163.940216,484.838684 C161.021744,496.053375 161.212982,507.259705 164.178833,518.426208 C171.577927,546.284302 197.338104,566.588867 226.001465,567.336853 C240.828415,567.723816 254.357819,563.819092 266.385468,555.199646 C284.811554,541.994751 293.631104,523.530579 294.687347,501.238312 C387.354828,501.238312 479.461304,501.238312 571.531799,501.238312 C577.616638,543.189026 615.312866,566.342102 651.310059,559.044739 C684.973938,552.220398 708.263306,519.393127 703.311279,484.370789 z"
/>
<path
fill="#EC008C"
opacity="1.000000"
stroke="none"
d="M703.401855,484.804718 C708.263306,519.393127 684.973938,552.220398 651.310059,559.044739 C615.312866,566.342102 577.616638,543.189026 571.531799,501.238312 C479.461304,501.238312 387.354828,501.238312 294.687347,501.238312 C293.631104,523.530579 284.811554,541.994751 266.385468,555.199646 C254.357819,563.819092 240.828415,567.723816 226.001465,567.336853 C197.338104,566.588867 171.577927,546.284302 164.178833,518.426208 C161.212982,507.259705 161.021744,496.053375 163.940216,484.838684 C166.849655,473.658661 171.685028,463.251984 180.383728,455.660675 C188.815598,448.302246 198.407059,442.272583 207.526581,435.632172 C191.471954,428.832886 177.054779,418.372589 168.756042,401.172516 C160.527206,384.117401 159.557541,366.613007 166.089890,348.816620 C176.337585,320.898438 203.355347,303.708374 234.093262,305.960541 C251.082092,307.205322 265.392609,314.611176 276.940491,327.118439 C288.485260,339.622314 293.993011,354.662048 294.582672,371.750854 C387.082886,371.750854 479.166595,371.750854 571.838501,371.750854 C570.185486,349.121399 577.473999,329.848511 594.588501,314.780121 C605.538086,305.139557 618.467957,299.725372 633.103516,298.838257 C664.183716,296.954437 691.688965,315.548492 700.947998,344.809204 C706.487793,362.316345 704.862061,379.280853 696.456482,395.642365 C688.014709,412.074219 673.953552,421.977875 656.413696,429.482330 C681.951416,440.229645 698.954468,457.053253 703.401855,484.804718 z"
/>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -53,28 +53,6 @@ ### Compiling
You can build conduwuit using `cargo build --release --all-features`
## Migrating from Conduit
As mentioned in the README, there is little to no steps needed to migrate
from Conduit. As long as you are using the RocksDB database backend, just
replace the binary / container image / etc.
**WARNING**: As of conduwuit 0.5.0, all database and backwards compatibility
with Conduit is no longer supported. We only support migrating *from* Conduit,
not back to Conduit like before. If you are truly finding yourself wanting to
migrate back to Conduit, we would appreciate all your feedback and if we can
assist with any issues or concerns.
**Note**: If you are relying on Conduit's "automatic delegation" feature,
this will **NOT** work on conduwuit and you must configure delegation manually.
This is not a mistake and no support for this feature will be added.
If you are using SQLite, you **MUST** migrate to RocksDB. You can use this
tool to migrate from SQLite to RocksDB: <https://github.com/ShadowJonathan/conduit_toolbox/>
See the `[global.well_known]` config section, or configure your web server
appropriately to send the delegation responses.
## Adding a conduwuit user
While conduwuit can run as any user it is better to use dedicated users for
@@ -167,25 +145,32 @@ ## Setting the correct file permissions
## Setting up the Reverse Proxy
Refer to the documentation or various guides online of your chosen reverse proxy
software. There are many examples of basic Apache/Nginx reverse proxy setups
out there.
We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc transparently with proper defaults.
For other software, please refer to their respective documentation or online guides.
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).
### Caddy
Lighttpd is not supported as it seems to mess with the `X-Matrix` Authorization
header, making federation non-functional. If a workaround is found, feel free to share to get it added to the documentation here.
After installing Caddy via your preferred method, create `/etc/caddy/conf.d/conduwuit_caddyfile`
and enter this (substitute for your server name).
If using Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent this (note that Apache isn't very good as a general reverse proxy and we discourage the usage of it if you can).
```caddyfile
your.server.name, your.server.name:8448 {
# TCP reverse_proxy
reverse_proxy 127.0.0.1:6167
# UNIX socket
#reverse_proxy unix//run/conduwuit/conduwuit.sock
}
```
If using Nginx, you need to give conduwuit the request URI using `$request_uri`, or like so:
- `proxy_pass http://127.0.0.1:6167$request_uri;`
- `proxy_pass http://127.0.0.1:6167;`
That's it! Just start and enable the service and you're set.
Nginx users need to increase `client_max_body_size` (default is 1M) to match
`max_request_size` defined in conduwuit.toml.
```bash
sudo systemctl enable --now caddy
```
### Other Reverse Proxies
As we would prefer our users to use Caddy, we will not provide configuration files for other proxys.
You will need to reverse proxy everything under following routes:
- `/_matrix/` - core Matrix C-S and S-S APIs
@@ -208,25 +193,19 @@ ## Setting up the Reverse Proxy
- <https://puppygock.gay/.well-known/matrix/server>
- <https://puppygock.gay/.well-known/matrix/client>
### Caddy
For Apache and Nginx there are many examples available online.
Create `/etc/caddy/conf.d/conduwuit_caddyfile` and enter this (substitute for
your server name).
Lighttpd is not supported as it seems to mess with the `X-Matrix` Authorization
header, making federation non-functional. If a workaround is found, feel free to share to get it added to the documentation here.
```caddyfile
your.server.name, your.server.name:8448 {
# TCP reverse_proxy
reverse_proxy 127.0.0.1:6167
# UNIX socket
#reverse_proxy unix//run/conduwuit/conduwuit.sock
}
```
If using Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent httpd from messing with the `X-Matrix` header (note that Apache isn't very good as a general reverse proxy and we discourage the usage of it if you can).
That's it! Just start and enable the service and you're set.
If using Nginx, you need to give conduwuit the request URI using `$request_uri`, or like so:
- `proxy_pass http://127.0.0.1:6167$request_uri;`
- `proxy_pass http://127.0.0.1:6167;`
```bash
sudo systemctl enable --now caddy
```
Nginx users need to increase `client_max_body_size` (default is 1M) to match
`max_request_size` defined in conduwuit.toml.
## You're done

View File

@@ -1,5 +1,8 @@
# Hot Reloading ("Live" Development)
Note that hot reloading has not been refactored in quite a while and is not
guaranteed to work at this time.
### Summary
When developing in debug-builds with the nightly toolchain, conduwuit is modular

View File

@@ -5,12 +5,11 @@ ## Complement
Have a look at [Complement's repository][complement] for an explanation of what
it is.
To test against Complement, with Nix (or [Lix](https://lix.systems) and direnv
installed and set up, you can:
To test against Complement, with Nix (or [Lix](https://lix.systems) and
[direnv installed and set up][direnv] (run `direnv allow` after setting up the hook), you can:
* Run `./bin/complement "$COMPLEMENT_SRC" ./path/to/logs.jsonl
./path/to/results.jsonl` to build a Complement image, run the tests, and output
the logs and results to the specified paths. This will also output the OCI image
* Run `./bin/complement "$COMPLEMENT_SRC"` to build a Complement image, run
the tests, and output the logs and results to the specified paths. This will also output the OCI image
at `result`
* Run `nix build .#complement` from the root of the repository to just build a
Complement OCI image outputted to `result` (it's a `.tar.gz` file)
@@ -18,5 +17,15 @@ ## Complement
output from the commit/revision you want to test (e.g. from main)
[here][ci-workflows]
If you want to use your own prebuilt OCI image (such as from our CI) without needing
Nix installed, put the image at `complement_oci_image.tar.gz` in the root of the repo
and run the script.
If you're on macOS and need to build an image, run `nix build .#linux-complement`.
We have a Complement fork as some tests have needed to be fixed. This can be found
at: <https://github.com/girlbossceo/complement>
[ci-workflows]: https://github.com/girlbossceo/conduwuit/actions/workflows/ci.yml?query=event%3Apush+is%3Asuccess+actor%3Agirlbossceo
[complement]: https://github.com/matrix-org/complement
[direnv]: https://direnv.net/docs/hook.html

View File

@@ -1,379 +0,0 @@
#### **Note: This list may not up to date. There are rapidly more and more
improvements, fixes, changes, etc being made that it is becoming more difficult
to maintain this list. I recommend that you give conduwuit a try and see the
differences for yourself. If you have any concerns, feel free to join the
conduwuit Matrix room and ask any pre-usage questions.**
### list of features, bug fixes, etc that conduwuit does that Conduit does not
Outgoing typing indicators, outgoing read receipts, **and** outgoing presence!
## Performance
- Concurrency support for individual homeserver key fetching for faster remote
room joins and room joins that will error less frequently
- Send `Cache-Control` response header with `immutable` and 1 year cache length
for all media requests (download and thumbnail) to instruct clients to cache
media, and reduce server load from media requests that could be otherwise cached
- Add feature flags and config options to enable/build with zstd, brotli, and/or
gzip HTTP body compression (response and request)
- Eliminate all usage of the thread-blocking `getaddrinfo(3)` call upon DNS
queries, significantly improving federation latency/ping and cache DNS results
(NXDOMAINs, successful queries, etc) using hickory-dns / hickory-resolver
- Enable HTTP/2 support on all requests
- Vastly improve RocksDB default settings to use new features that help with
performance significantly, uses settings tailored to SSDs, various ways to tweak
RocksDB, and a conduwuit setting to tell RocksDB to use settings that are
tailored to HDDs or slow spinning rust storage or buggy filesystems.
- Implement database flush and cleanup conduwuit operations when using RocksDB
- Implement RocksDB write buffer corking and coalescing in database write-heavy
areas
- Perform connection pooling and keepalives where necessary to significantly
improve federation performance and latency
- Various config options to tweak connection pooling, request timeouts,
connection timeouts, DNS timeouts and settings, etc with good defaults which
also help huge with performance via reusing connections and retrying where
needed
- Properly get and use the amount of parallelism / tokio workers
- Implement building conduwuit with jemalloc (which extends to the RocksDB
jemalloc feature for maximum gains) or hardened_malloc light variant, and
io_uring support, and produce CI builds with jemalloc and io_uring by default
for performance (Nix doesn't seem to build
[hardened_malloc-rs](https://github.com/girlbossceo/hardened_malloc-rs)
properly)
- Add support for caching DNS results with hickory-dns / hickory-resolver in
conduwuit (not a replacement for a proper resolver cache, but still far better
than nothing), also properly falls back on TCP for UDP errors or if a SRV
response is too large
- Add config option for using DNS over TCP, and config option for controlling
A/AAAA record lookup strategy (e.g. don't query AAAA records if you don't have
IPv6 connectivity)
- Overall significant database, Client-Server, and federation performance and
latency improvements (check out the ping room leaderboards if you don't believe
me :>)
- Add config options for RocksDB compression and bottommost compression,
including choosing the algorithm and compression level
- Use [loole](https://github.com/mahdi-shojaee/loole) MPSC channels instead of
tokio MPSC channels for huge performance boosts in sending channels (mainly
relevant for federation) and presence channels
- Use `tracing`/`log`'s `release_max_level_info` feature to improve performance,
build speeds, binary size, and CPU usage in release builds by avoid compiling
debug/trace log level macros that users will generally never use (can be
disabled with a build-time feature flag)
- Remove some unnecessary checks on EDU handling for incoming transactions,
effectively speeding them up
- Simplify, dedupe, etc huge chunks of the codebase, including some that were
unnecessary overhead, binary bloats, or preventing compiler/linker optimisations
- Implement zero-copy RocksDB database accessors, substantially improving
performance caused by unnecessary memory allocations
## General Fixes/Features
- Add legacy Element client hack fixing password changes and deactivations on
legacy Element Android/iOS due to usage of an unspecced `user` field for UIAA
- Raise and improve all the various request timeouts making some things like
room joins and client bugs error less or none at all than they should, and make
them all user configurable
- Add missing `reason` field to user ban events (`/ban`)
- Safer and cleaner shutdowns across incoming/outgoing requests (graceful
shutdown) and the database
- Stop sending `make_join` requests on room joins if 15 servers respond with
`M_UNSUPPORTED_ROOM_VERSION` or `M_INVALID_ROOM_VERSION`
- Stop sending `make_join` requests if 50 servers cannot provide `make_join` for
us
- Respect *most* client parameters for `/media/` requests (`allow_redirect`
still needs work)
- Return joined member count of rooms for push rules/conditions instead of a
hardcoded value of 10
- Make `CONDUIT_CONFIG` optional, relevant for container users that configure
only by environment variables and no longer need to set `CONDUIT_CONFIG` to an
empty string.
- Allow HEAD and PATCH (MSC4138) HTTP requests in CORS for clients (despite not
being explicity mentioned in Matrix spec, HTTP spec says all HEAD requests need
to behave the same as GET requests, Synapse supports HEAD requests)
- Fix using conduwuit with flake-compat on NixOS
- Resolve and remove some "features" from upstream that result in concurrency
hazards, exponential backoff issues, or arbitrary performance limiters
- Find more servers for outbound federation `/hierarchy` requests instead of
just the room ID server name
- Support for suggesting servers to join through at
`/_matrix/client/v3/directory/room/{roomAlias}`
- Support for suggesting servers to join through us at
`/_matrix/federation/v1/query/directory`
- Misc edge-case search fixes (e.g. potentially missing some events)
- Misc `/sync` fixes (e.g. returning unnecessary data or incorrect/invalid
responses)
- Add `replaces_state` and `prev_sender` in `unsigned` for state event changes
which primarily makes Element's "See history" button on a state event functional
- Fix Conduit not allowing incoming federation requests for various world
readable rooms
- Fix Conduit not respecting the client-requested file name on media requests
- Prevent sending junk / non-membership events to `/send_join` and `/send_leave`
endpoints
- Only allow the requested membership type on `/send_join` and `/send_leave`
endpoints (e.g. don't allow leave memberships on join endpoints)
- Prevent state key impersonation on `/send_join` and `/send_leave` endpoints
- Validate `X-Matrix` origin and request body `"origin"` field on incoming
transactions
- Add `GET /_matrix/client/v1/register/m.login.registration_token/validity`
endpoint
- Explicitly define support for sliding sync at `/_matrix/client/versions`
(`org.matrix.msc3575`)
- Fix seeing empty status messages on user presences
## Moderation
- (Also see [Admin Room](#admin-room) for all the admin commands pertaining to
moderation, there's a lot!)
- Add support for room banning/blocking by ID using admin command
- Add support for serving `support` well-known from `[global.well_known]`
(MSC1929) (`/.well-known/matrix/support`)
- Config option to forbid publishing rooms to the room directory
(`lockdown_public_room_directory`) except for admins
- Admin commands to delete room aliases and unpublish rooms from our room
directory
- For all
[`/report`](https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3roomsroomidreporteventid)
requests: check if the reported event ID belongs to the reported room ID, raise
report reasoning character limit to 750, fix broken formatting, make a small
delayed random response per spec suggestion on privacy, and check if the sender
user is in the reported room.
- Support blocking servers from downloading remote media from, returning a 404
- Don't allow `m.call.invite` events to be sent in public rooms (prevents
calling the entire room)
- On new public room creations, only allow moderators to send `m.call.invite`,
`org.matrix.msc3401.call`, and `org.matrix.msc3401.call.member` events to
prevent unprivileged users from calling the entire room
- Add support for a "global ACLs" feature (`forbidden_remote_server_names`) that
blocks inbound remote room invites, room joins by room ID on server name, room
joins by room alias on server name, incoming federated joins, and incoming
federated room directory requests. This is very helpful for blocking servers
that are purely toxic/bad and serve no value in allowing our users to suffer
from things like room invite spam or such. Please note that this is not a
substitute for room ACLs.
- Add support for a config option to forbid our local users from sending
federated room directory requests for
(`forbidden_remote_room_directory_server_names`). Similar to above, useful for
blocking servers that help prevent our users from wandering into bad areas of
Matrix via room directories of those malicious servers.
- Add config option for auto remediating/deactivating local non-admin users who
attempt to join bad/forbidden rooms (`auto_deactivate_banned_room_attempts`)
- Deactivating users will remove their profile picture, blurhash, display name,
and leave all rooms by default just like Synapse and for additional privacy
- Reject some EDUs from ACL'd users such as read receipts and typing indicators
## Privacy/Security
- Add config option for device name federation with a privacy-friendly default
(disabled)
- Add config option for requiring authentication to the `/publicRooms` endpoint
(room directory) with a default enabled for privacy
- Add config option for federating `/publicRooms` endpoint (room directory) to
other servers with a default disabled for privacy
- Uses proper `argon2` crate by RustCrypto instead of questionable `rust-argon2`
crate
- Generate passwords with 25 characters instead of 15
- Config option `ip_range_denylist` to support refusing to send requests
(typically federation) to specific IP ranges, typically RFC 1918, non-routable,
testnet, etc addresses like Synapse for security (note: this is not a guaranteed
protection, and you should be using a firewall with zones if you want guaranteed
protection as doing this on the application level is prone to bypasses).
- Config option to block non-admin users from sending room invites or receiving
remote room invites. Admin users are still allowed.
- Config option to disable incoming and/or outgoing remote read receipts
- Config option to disable incoming and/or outgoing remote typing indicators
- Config option to disable incoming, outgoing, and/or local presence and for
timing out remote users
- Sanitise file names for the `Content-Disposition` header for all media
requests (thumbnails, downloads, uploads)
- Media repository on handling `Content-Disposition` and `Content-Type` is fully
spec compliant and secured
- Send secure default HTTP headers such as a strong restrictive CSP (see
MSC4149), deny iframes, disable `X-XSS-Protection`, disable interest cohort in
`Permission-Policy`, etc to mitigate any potential attack surface such as from
untrusted media
## Administration/Logging
- Commandline argument to specify the path to a config file instead of relying
on `CONDUIT_CONFIG`
- Revamped admin room infrastructure and commands
- Substantially clean up, improve, and fix logging (less noisy dead server
logging, registration attempts, more useful troubleshooting logging, proper
error propagation, etc)
- Configurable RocksDB logging (`LOG` files) with proper defaults (rotate, max
size, verbosity, etc) to stop LOG files from accumulating so much
- Explicit startup error if your configuration allows open registration without
a token or such like Synapse with a way to bypass it if needed
- Replace the lightning bolt emoji option with support for setting any arbitrary
text (e.g. another emoji) to suffix to all new user registrations, with a
conduwuit default of "🏳️‍⚧️"
- Implement config option to auto join rooms upon registration
- Warn on unknown config options specified
- Add `/_conduwuit/server_version` route to return the version of conduwuit
without relying on the federation API `/_matrix/federation/v1/version`
- Add `/_conduwuit/local_user_count` route to return the amount of registered
active local users on your homeserver *if federation is enabled*
- Add configurable RocksDB recovery modes to aid in recovering corrupted RocksDB
databases
- Support config options via `CONDUWUIT_` prefix and accessing non-global struct
config options with the `__` split (e.g. `CONDUWUIT_WELL_KNOWN__SERVER`)
- Add support for listening on multiple TCP ports and multiple addresses
- **Opt-in** Sentry.io telemetry and metrics, mainly used for crash reporting
- Log the client IP on various requests such as registrations, banned room join
attempts, logins, deactivations, federation transactions, etc
- Fix Conduit dropping some remote server federation response errors
## Maintenance/Stability
- GitLab CI ported to GitHub Actions
- Add support for the Matrix spec compliance test suite
[Complement](https://github.com/matrix-org/complement/) via the Nix flake and
various other fixes for it
- Implement running and diff'ing Complement results in CI and error if any
mismatch occurs to prevent large cases of conduwuit regressions
- Repo is (officially) mirrored to GitHub, GitLab, git.gay, git.girlcock.ceo,
sourcehut, and Codeberg (see README.md for their links)
- Docker container images published to GitLab Container Registry, GitHub
Container Registry, and Dockerhub
- Extensively revamp the example config to be extremely helpful and useful to
both new users and power users
- Fixed every single clippy (default lints) and rustc warnings, including some
that were performance related or potential safety issues / unsoundness
- Add a **lot** of other clippy and rustc lints and a rustfmt.toml file
- Repo uses [Renovate](https://docs.renovatebot.com/) and keeps ALL
dependencies as up to date as possible
- Purge unmaintained/irrelevant/broken database backends (heed, sled, persy) and
other unnecessary code or overhead
- webp support for images
- Add cargo audit support to CI
- Add documentation lints via lychee and markdownlint-cli to CI
- CI tests for all sorts of feature matrixes (jemalloc, non-defaullt, all
features, etc)
- Add static and dynamic linking smoke tests in CI to prevent any potential
linking regressions for Complement, static binaries, Nix devshells, etc
- Add timestamp by commit date when building OCI images for keeping image build
reproducibility and still have a meaningful "last modified date" for OCI image
- Add timestamp by commit date via `SOURCE_DATE_EPOCH` for Debian packages
- Startup check if conduwuit running in a container and is listening on
127.0.0.1 (generally containers are using NAT networking and 0.0.0.0 is the
intended listening address)
- Add a panic catcher layer to return panic messages in HTTP responses if a
panic occurs
- Add full compatibility support for SHA256 media file names instead of base64
file names to overcome filesystem file name length limitations (OS error file
name too long) while still retaining upstream database compatibility
- Remove SQLite support due to being very poor performance, difficult to
maintain against RocksDB, and is a blocker to significantly improved database
code
## Admin Room
- Add support for a console CLI interface that can issue admin commands and
output them in your terminal
- Add support for an admin-user-only commandline admin room interface that can
be issued in any room with the `\\!admin` or `\!admin` prefix and returns the
response as yourself in the same room
- Add admin commands for uptime, server startup, server shutdown, and server
restart
- Fix admin room handler to not panic/crash if the admin room command response
fails (e.g. too large message)
- Add command to dynamically change conduwuit's tracing log level filter on the
fly
- Add admin command to fetch a server's `/.well-known/matrix/support` file
- Add debug admin command to force update user device lists (could potentially
resolve some E2EE flukes)
- Implement **RocksDB online backups**, listing RocksDB backups, and listing
database file counts all via admin commands
- Add various database visibility commands such as being able to query the
getters and iterators used in conduwuit, a very helpful online debugging utility
- Forbid the admin room from being made public or world readable history
- Add `!admin` as a way to call the admin bot
- Extend clear cache admin command to support clearing more caches such as DNS
and TLS name overrides
- Admin debug command to send a federation request/ping to a server's
`/_matrix/federation/v1/version` endpoint and measures the latency it took
- Add admin command to bulk delete media via a codeblock list of MXC URLs.
- Add admin command to delete both the thumbnail and media MXC URLs from an
event ID (e.g. from an abuse report)
- Add admin command to list all the rooms a local user is joined in
- Add admin command to list joined members in a room
- Add admin command to view the room topic of a room
- Add admin command to delete all remote media in the past X minutes as a form
of deleting media that you don't want on your server that a remote user posted
in a room, a `--force` flag to ignore errors, and support for reading `last
modified time` instead of `creation time` for filesystems that don't support
file created metadata
- Add admin command to return a room's full/complete state
- Admin debug command to fetch a PDU from a remote server and inserts it into
our database/timeline as backfill
- Add admin command to delete media via a specific MXC. This deletes the MXC
from our database, and the file locally.
- Add admin commands for banning (blocking) room IDs from our local users
joining (admins are always allowed) and evicts all our local users from that
room, in addition to bulk room banning support, and blocks room invites (remote
and local) to the banned room, as a moderation feature
- Add admin commands to output jemalloc memory stats and memory usage
- Add admin command to get rooms a *remote* user shares with us
- Add debug admin commands to get the earliest and latest PDU in a room
- Add debug admin command to echo a message
- Add admin command to insert rooms tags for a user, most useful for inserting
the `m.server_notice` tag on your admin room to make it "persistent" in the
"System Alerts" section of Element
- Add experimental admin debug command for Dendrite's `AdminDownloadState`
(`/admin/downloadState/{serverName}/{roomID}`) admin API endpoint to download
and use a remote server's room state in the room
- Disable URL previews by default in the admin room due to various command
outputs having "URLs" in them that clients may needlessly render/request
- Extend memory usage admin server command to support showing memory allocator
stats such as jemalloc's
- Add admin debug command to see memory allocator's full extended debug
statistics such as jemalloc's
## Misc
- Add guest support for accessing TURN servers via `turn_allow_guests` like
Synapse
- Support for creating rooms with custom room IDs like Maunium Synapse
(`room_id` request body field to `/createRoom`)
- Query parameter `?format=event|content` for returning either the room state
event's content (default) for the full room state event on
`/_matrix/client/v3/rooms/{roomId}/state/{eventType}[/{stateKey}]` requests (see
<https://github.com/matrix-org/matrix-spec/issues/1047>)
- Send a User-Agent on all of our requests
- Send `avatar_url` on invite room membership events/changes
- Support sending [`well_known` response to client login
responses](https://spec.matrix.org/v1.10/client-server-api/#post_matrixclientv3login)
if using config option `[well_known.client]`
- Implement `include_state` search criteria support for `/search` requests
(response now can include room states)
- Declare various missing Matrix versions and features at
`/_matrix/client/versions`
- Implement legacy Matrix `/v1/` media endpoints that some clients and servers
may still call
- Config option to change Conduit's behaviour of homeserver key fetching
(`query_trusted_key_servers_first`). This option sets whether conduwuit will
query trusted notary key servers first before the individual homeserver(s), or
vice versa which may help in joining certain rooms.
- Implement unstable MSC2666 support for querying mutual rooms with a user
- Implement unstable MSC3266 room summary API support
- Implement unstable MSC4125 support for specifying servers to join via on
federated invites
- Make conduwuit build and be functional under Nix + macOS
- Log out all sessions after unsetting the emergency password
- Assume well-knowns are broken if they exceed past 12288 characters.
- Add support for listening on both HTTP and HTTPS if using direct TLS with
conduwuit for usecases such as Complement
- 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, moderation documentation,
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
- No cryptocurrency donations allowed, conduwuit is fully maintained by
independent queer maintainers, and with a strong priority on inclusitivity and
comfort for protected groups 🏳️‍⚧️
- [Add a community Code of Conduct for all conduwuit community spaces, primarily
the Matrix space](https://conduwuit.puppyirl.gay/conduwuit_coc.html)

View File

@@ -4,10 +4,6 @@ # conduwuit
{{#include ../README.md:body}}
#### What's different about your fork than upstream Conduit?
See the [differences](differences.md) page
#### How can I deploy my own?
- [Deployment options](deploying.md)

View File

@@ -18,12 +18,12 @@ script = "direnv --version"
[[task]]
name = "rustc"
group = "versions"
script = "rustc --version"
script = "rustc --version -v"
[[task]]
name = "cargo"
group = "versions"
script = "cargo --version"
script = "cargo --version -v"
[[task]]
name = "cargo-fmt"
@@ -60,15 +60,10 @@ name = "markdownlint"
group = "versions"
script = "markdownlint --version"
[[task]]
name = "dpkg"
group = "versions"
script = "dpkg --version"
[[task]]
name = "cargo-audit"
group = "security"
script = "cargo audit -D warnings -D unmaintained -D unsound -D yanked"
script = "cargo audit --color=always -D warnings -D unmaintained -D unsound -D yanked"
[[task]]
name = "cargo-fmt"
@@ -166,24 +161,6 @@ name = "markdownlint"
group = "lints"
script = "markdownlint docs *.md || true" # TODO: fix the ton of markdown lints so we can drop `|| true`
[[task]]
name = "cargo/all"
group = "tests"
script = """
env DIRENV_DEVSHELL=all-features \
direnv exec . \
cargo test \
--workspace \
--locked \
--profile test \
--all-targets \
--no-fail-fast \
--all-features \
--color=always \
-- \
--color=always
"""
[[task]]
name = "cargo/default"
group = "tests"
@@ -201,24 +178,6 @@ env DIRENV_DEVSHELL=default \
--color=always
"""
[[task]]
name = "cargo/no-features"
group = "tests"
script = """
env DIRENV_DEVSHELL=no-features \
direnv exec . \
cargo test \
--workspace \
--locked \
--profile test \
--all-targets \
--no-fail-fast \
--no-default-features \
--color=always \
-- \
--color=always
"""
# Checks if the generated example config differs from the checked in repo's
# example config.
[[task]]
@@ -228,22 +187,3 @@ depends = ["cargo/default"]
script = """
git diff --exit-code conduwuit-example.toml
"""
# 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 = """
env DIRENV_DEVSHELL=dynamic \
CARGO_PROFILE="test" \
direnv exec . \
bin/nix-build-and-cache just .#default-test
env DIRENV_DEVSHELL=dynamic \
CARGO_PROFILE="test" \
direnv exec . \
nix run -L .#default-test -- --help && nix run -L .#default-test -- --version
"""

50
flake.lock generated
View File

@@ -10,11 +10,11 @@
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1731270564,
"narHash": "sha256-6KMC/NH/VWP5Eb+hA56hz0urel3jP6Y6cF2PX6xaTkk=",
"lastModified": 1738524606,
"narHash": "sha256-hPYEJ4juK3ph7kbjbvv7PlU1D9pAkkhl+pwx8fZY53U=",
"owner": "zhaofengli",
"repo": "attic",
"rev": "47752427561f1c34debb16728a210d378f0ece36",
"rev": "ff8a897d1f4408ebbf4d45fa9049c06b3e1e3f4e",
"type": "github"
},
"original": {
@@ -80,11 +80,11 @@
"complement": {
"flake": false,
"locked": {
"lastModified": 1734303596,
"narHash": "sha256-HjDRyLR4MBqQ3IjfMM6eE+8ayztXlbz3gXdyDmFla68=",
"lastModified": 1741891349,
"narHash": "sha256-YvrzOWcX7DH1drp5SGa+E/fc7wN3hqFtPbqPjZpOu1Q=",
"owner": "girlbossceo",
"repo": "complement",
"rev": "14cc5be797b774f1a2b9f826f38181066d4952b8",
"rev": "e587b3df569cba411aeac7c20b6366d03c143745",
"type": "github"
},
"original": {
@@ -117,11 +117,11 @@
},
"crane_2": {
"locked": {
"lastModified": 1737689766,
"narHash": "sha256-ivVXYaYlShxYoKfSo5+y5930qMKKJ8CLcAoIBPQfJ6s=",
"lastModified": 1739936662,
"narHash": "sha256-x4syUjNUuRblR07nDPeLDP7DpphaBVbUaSoeZkFbGSk=",
"owner": "ipetkov",
"repo": "crane",
"rev": "6fe74265bbb6d016d663b1091f015e2976c4a527",
"rev": "19de14aaeb869287647d9461cbd389187d8ecdb7",
"type": "github"
},
"original": {
@@ -170,11 +170,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1737786656,
"narHash": "sha256-ubCW9Jy7ZUOF354bWxTgLDpVnTvIpNr6qR4H/j7I0oo=",
"lastModified": 1740724364,
"narHash": "sha256-D1jLIueJx1dPrP09ZZwTrPf4cubV+TsFMYbpYYTVj6A=",
"owner": "nix-community",
"repo": "fenix",
"rev": "2f721f527886f801403f389a9cabafda8f1e3b7f",
"rev": "edf7d9e431cda8782e729253835f178a356d3aab",
"type": "github"
},
"original": {
@@ -364,11 +364,11 @@
"liburing": {
"flake": false,
"locked": {
"lastModified": 1737600516,
"narHash": "sha256-EKyLQ3pbcjoU5jH5atge59F4fzuhTsb6yalUj6Ve2t8=",
"lastModified": 1740613216,
"narHash": "sha256-NpPOBqNND3Qe9IwqYs0mJLGTmIx7e6FgUEBAnJ+1ZLA=",
"owner": "axboe",
"repo": "liburing",
"rev": "6c509e2b0c881a13b83b259a221bf15fc9b3f681",
"rev": "e1003e496e66f9b0ae06674869795edf772d5500",
"type": "github"
},
"original": {
@@ -550,11 +550,11 @@
},
"nixpkgs_5": {
"locked": {
"lastModified": 1737717945,
"narHash": "sha256-ET91TMkab3PmOZnqiJQYOtSGvSTvGeHoegAv4zcTefM=",
"lastModified": 1740547748,
"narHash": "sha256-Ly2fBL1LscV+KyCqPRufUBuiw+zmWrlJzpWOWbahplg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ecd26a469ac56357fd333946a99086e992452b6a",
"rev": "3a05eebede89661660945da1f151959900903b6a",
"type": "github"
},
"original": {
@@ -567,16 +567,16 @@
"rocksdb": {
"flake": false,
"locked": {
"lastModified": 1737828695,
"narHash": "sha256-8Ev6zzhNPU798JNvU27a7gj5X+6SDG3jBweUkQ59DbA=",
"lastModified": 1741308171,
"narHash": "sha256-YdBvdQ75UJg5ffwNjxizpviCVwVDJnBkM8ZtGIduMgY=",
"owner": "girlbossceo",
"repo": "rocksdb",
"rev": "a4d9230dcc9d03be428b9a728133f8f646c0065c",
"rev": "3ce04794bcfbbb0d2e6f81ae35fc4acf688b6986",
"type": "github"
},
"original": {
"owner": "girlbossceo",
"ref": "v9.9.3",
"ref": "v9.11.1",
"repo": "rocksdb",
"type": "github"
}
@@ -599,11 +599,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1737728869,
"narHash": "sha256-U4pl3Hi0lT6GP4ecN3q9wdD2sdaKMbmD/5NJ1NdJ9AM=",
"lastModified": 1740691488,
"narHash": "sha256-Fs6vBrByuiOf2WO77qeMDMTXcTGzrIMqLBv+lNeywwM=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "6e4c29f7ce18cea7d3d31237a4661ab932eab636",
"rev": "fe3eda77d3a7ce212388bda7b6cec8bffcc077e5",
"type": "github"
},
"original": {

View File

@@ -9,7 +9,7 @@
flake-utils.url = "github:numtide/flake-utils?ref=main";
nix-filter.url = "github:numtide/nix-filter?ref=main";
nixpkgs.url = "github:NixOS/nixpkgs?ref=nixpkgs-unstable";
rocksdb = { url = "github:girlbossceo/rocksdb?ref=v9.9.3"; flake = false; };
rocksdb = { url = "github:girlbossceo/rocksdb?ref=v9.11.1"; flake = false; };
liburing = { url = "github:axboe/liburing?ref=master"; flake = false; };
};
@@ -26,7 +26,7 @@
file = ./rust-toolchain.toml;
# See also `rust-toolchain.toml`
sha256 = "sha256-lMLAupxng4Fd9F1oDw8gx+qA0RuF7ou7xhNU8wgs0PU=";
sha256 = "sha256-X/4ZBHO3iW0fOenQ3foEvscgAPJYl2abspaBThDOukI=";
};
mkScope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
@@ -64,8 +64,10 @@
patches = [];
cmakeFlags = pkgs.lib.subtractLists
[
# no real reason to have snappy, no one uses this
# no real reason to have snappy or zlib, no one uses this
"-DWITH_SNAPPY=1"
"-DZLIB=1"
"-DWITH_ZLIB=1"
# we dont need to use ldb or sst_dump (core_tools)
"-DWITH_CORE_TOOLS=1"
# we dont need to build rocksdb tests
@@ -82,6 +84,8 @@
++ [
# no real reason to have snappy, no one uses this
"-DWITH_SNAPPY=0"
"-DZLIB=0"
"-DWITH_ZLIB=0"
# we dont need to use ldb or sst_dump (core_tools)
"-DWITH_CORE_TOOLS=0"
# we dont need trace tools
@@ -140,23 +144,26 @@
toolchain
]
++ (with pkgsHost.pkgs; [
engage
cargo-audit
# Required by hardened-malloc.rs dep
binutils
cargo-audit
cargo-auditable
# Needed for producing Debian packages
cargo-deb
# Needed for CI to check validity of produced Debian packages (dpkg-deb)
dpkg
engage
# Needed for Complement
go
# Needed for our script for Complement
jq
gotestfmt
# Needed for finding broken markdown links
lychee
@@ -171,7 +178,8 @@
sccache
]
# liburing is Linux-exclusive
++ lib.optional stdenv.hostPlatform.isLinux liburing)
++ lib.optional stdenv.hostPlatform.isLinux liburing
++ lib.optional stdenv.hostPlatform.isLinux numactl)
++ scope.main.buildInputs
++ scope.main.propagatedBuildInputs
++ scope.main.nativeBuildInputs;

View File

@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDfzCCAmegAwIBAgIUcrZdSPmCh33Evys/U6mTPpShqdcwDQYJKoZIhvcNAQEL
BQAwPzELMAkGA1UEBhMCNjkxCzAJBgNVBAgMAjQyMRUwEwYDVQQKDAx3b29mZXJz
IGluYy4xDDAKBgNVBAMMA2hzMTAgFw0yNTAzMTMxMjU4NTFaGA8yMDUyMDcyODEy
NTg1MVowPzELMAkGA1UEBhMCNjkxCzAJBgNVBAgMAjQyMRUwEwYDVQQKDAx3b29m
ZXJzIGluYy4xDDAKBgNVBAMMA2hzMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBANL+h2ZmK/FqN5uLJPtIy6Feqcyb6EX7MQBEtxuJ56bTAbjHuCLZLpYt
/wOWJ91drHqZ7Xd5iTisGdMu8YS803HSnHkzngf4VXKhVrdzW2YDrpZRxmOhtp88
awOHmP7mqlJyBbCOQw8aDVrT0KmEIWzA7g+nFRQ5Ff85MaP+sQrHGKZbo61q8HBp
L0XuaqNckruUKtxnEqrm5xx5sYyYKg7rrSFE5JMFoWKB1FNWJxyWT42BhGtnJZsK
K5c+NDSOU4TatxoN6mpNSBpCz/a11PiQHMEfqRk6JA4g3911dqPTfZBevUdBh8gl
8maIzqeZGhvyeKTmull1Y0781yyuj98CAwEAAaNxMG8wCQYDVR0TBAIwADALBgNV
HQ8EBAMCBPAwNgYDVR0RBC8wLYIRKi5kb2NrZXIuaW50ZXJuYWyCA2hzMYIDaHMy
ggNoczOCA2hzNIcEfwAAATAdBgNVHQ4EFgQUr4VYrmW1d+vjBTJewvy7fJYhLDYw
DQYJKoZIhvcNAQELBQADggEBADkYqkjNYxjWX8hUUAmFHNdCwzT1CpYe/5qzLiyJ
irDSdMlC5g6QqMUSrpu7nZxo1lRe1dXGroFVfWpoDxyCjSQhplQZgtYqtyLfOIx+
HQ7cPE/tUU/KsTGc0aL61cETB6u8fj+rQKUGdfbSlm0Rpu4v0gC8RnDj06X/hZ7e
VkWU+dOBzxlqHuLlwFFtVDgCyyTatIROx5V+GpMHrVqBPO7HcHhwqZ30k2kMM8J3
y1CWaliQM85jqtSZV+yUHKQV8EksSowCFJuguf+Ahz0i0/koaI3i8m4MRN/1j13d
jbTaX5a11Ynm3A27jioZdtMRty6AJ88oCp18jxVzqTxNNO4=
-----END CERTIFICATE-----

View File

@@ -6,7 +6,7 @@ allow_public_room_directory_over_federation = true
allow_public_room_directory_without_auth = true
allow_registration = true
database_path = "/database"
log = "trace,h2=warn,hyper=warn"
log = "trace,h2=debug,hyper=debug"
port = [8008, 8448]
trusted_servers = []
only_query_trusted_key_servers = false
@@ -19,11 +19,11 @@ url_preview_domain_explicit_denylist = ["*"]
media_compat_file_link = false
media_startup_check = true
prune_missing_media = true
log_colors = false
log_colors = true
admin_room_notices = false
allow_check_for_updates = false
intentionally_unknown_config_option_for_testing = true
rocksdb_log_level = "debug"
rocksdb_log_level = "info"
rocksdb_max_log_files = 1
rocksdb_recovery_mode = 0
rocksdb_paranoid_file_checks = true
@@ -32,6 +32,8 @@ allow_legacy_media = true
startup_netburst = true
startup_netburst_keep = -1
allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure = true
# valgrind makes things so slow
dns_timeout = 60
dns_attempts = 20
@@ -45,6 +47,4 @@ sender_idle_timeout = 300
sender_retry_backoff_limit = 300
[global.tls]
certs = "/certificate.crt"
dual_protocol = true
key = "/private_key.key"

View File

@@ -3,10 +3,8 @@
, buildEnv
, coreutils
, dockerTools
, gawk
, lib
, main
, openssl
, stdenv
, tini
, writeShellScriptBin
@@ -42,28 +40,6 @@ let
start = writeShellScriptBin "start" ''
set -euxo pipefail
${lib.getExe openssl} genrsa -out private_key.key 2048
${lib.getExe openssl} req \
-new \
-sha256 \
-key private_key.key \
-subj "/C=US/ST=CA/O=MyOrg, Inc./CN=$SERVER_NAME" \
-out signing_request.csr
cp ${./v3.ext} v3.ext
echo "DNS.1 = $SERVER_NAME" >> v3.ext
echo "IP.1 = $(${lib.getExe gawk} 'END{print $1}' /etc/hosts)" \
>> v3.ext
${lib.getExe openssl} x509 \
-req \
-extfile v3.ext \
-in signing_request.csr \
-CA /complement/ca/ca.crt \
-CAkey /complement/ca/ca.key \
-CAcreateserial \
-out certificate.crt \
-days 1 \
-sha256
${lib.getExe' coreutils "env"} \
CONDUWUIT_SERVER_NAME="$SERVER_NAME" \
${lib.getExe main'}
@@ -99,7 +75,8 @@ dockerTools.buildImage {
else [];
Env = [
"SSL_CERT_FILE=/complement/ca/ca.crt"
"CONDUWUIT_TLS__KEY=${./private_key.key}"
"CONDUWUIT_TLS__CERTS=${./certificate.crt}"
"CONDUWUIT_CONFIG=${./config.toml}"
"RUST_BACKTRACE=full"
];

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDS/odmZivxajeb
iyT7SMuhXqnMm+hF+zEARLcbieem0wG4x7gi2S6WLf8DlifdXax6me13eYk4rBnT
LvGEvNNx0px5M54H+FVyoVa3c1tmA66WUcZjobafPGsDh5j+5qpScgWwjkMPGg1a
09CphCFswO4PpxUUORX/OTGj/rEKxximW6OtavBwaS9F7mqjXJK7lCrcZxKq5ucc
ebGMmCoO660hROSTBaFigdRTVicclk+NgYRrZyWbCiuXPjQ0jlOE2rcaDepqTUga
Qs/2tdT4kBzBH6kZOiQOIN/ddXaj032QXr1HQYfIJfJmiM6nmRob8nik5rpZdWNO
/Ncsro/fAgMBAAECggEAITCCkfv+a5I+vwvrPE/eIDso0JOxvNhfg+BLQVy3AMnu
WmeoMmshZeREWgcTrEGg8QQnk4Sdrjl8MnkO6sddJ2luza3t7OkGX+q7Hk5aETkB
DIo+f8ufU3sIhlydF3OnVSK0fGpUaBq8AQ6Soyeyrk3G5NVufmjgae5QPbDBnqUb
piOGyfcwagL4JtCbZsMk8AT7vQSynLm6zaWsVzWNd71jummLqtVV063K95J9PqVN
D8meEcP3WR5kQrvf+mgy9RVgWLRtVWN8OLZfJ9yrnl4Efj62elrldUj4jaCFezGQ
8f0W+d8jjt038qhmEdymw2MWQ+X/b0R79lJar1Up8QKBgQD1DtHxauhl+JUoI3y+
3eboqXl7YPJt1/GTnChb4b6D1Z1hvLsOKUa7hjGEfruYGbsWXBCRMICdfzp+iWcq
/lEOp7/YU9OaW4lQMoG4sXMoBWd9uLgg0E+aH6VDJOBvxsfafqM4ufmtspzwEm90
FU1cq6oImomFnPChSq4X+3+YpwKBgQDcalaK9llCcscWA8HAP8WVVNTjCOqiDp9q
td61E9IO/FIB/gW5y+JkaFRrA2CN1zY3s3K92uveLTNYTArecWlDcPNNFDuaYu2M
Roz4bC104HGh+zztJ0iPVzELL81Lgg6wHhLONN+eVi4gTftJxzJFXybyb+xVT25A
91ynKXB+CQKBgQC+Ub43MoI+/6pHvBfb3FbDByvz6D0flgBmVXb6tP3TQYmzKHJV
8zSd2wCGGC71V7Z3DRVIzVR1/SOetnPLbivhp+JUzfWfAcxI3pDksdvvjxLrDxTh
VycbWcxtsywjY0w/ou581eLVRcygnpC0pP6qJCAwAmUfwd0YRvmiYo6cLQKBgHIW
UIlJDdaJFmdctnLOD3VGHZMOUHRlYTqYvJe5lKbRD5mcZFZRI/OY1Ok3LEj+tj+K
kL+YizHK76KqaY3N4hBYbHbfHCLDRfWvptQHGlg+vFJ9eoG+LZ6UIPyLV5XX0cZz
KoS1dXG9Zc6uznzXsDucDsq6B/f4TzctUjXsCyARAoGAOKb4HtuNyYAW0jUlujR7
IMHwUesOGlhSXqFtP9aTvk6qJgvV0+3CKcWEb4y02g+uYftP8BLNbJbIt9qOqLYh
tOVyzCoamAi8araAhjA0w4dXvqDCDK7k/gZFkojmKQtRijoxTHnWcDc3vAjYCgaM
9MVtdgSkuh2gwkD/mMoAJXM=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIChDCCAWwCAQAwPzELMAkGA1UEBhMCNjkxCzAJBgNVBAgMAjQyMRUwEwYDVQQK
DAx3b29mZXJzIGluYy4xDDAKBgNVBAMMA2hzMTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBANL+h2ZmK/FqN5uLJPtIy6Feqcyb6EX7MQBEtxuJ56bTAbjH
uCLZLpYt/wOWJ91drHqZ7Xd5iTisGdMu8YS803HSnHkzngf4VXKhVrdzW2YDrpZR
xmOhtp88awOHmP7mqlJyBbCOQw8aDVrT0KmEIWzA7g+nFRQ5Ff85MaP+sQrHGKZb
o61q8HBpL0XuaqNckruUKtxnEqrm5xx5sYyYKg7rrSFE5JMFoWKB1FNWJxyWT42B
hGtnJZsKK5c+NDSOU4TatxoN6mpNSBpCz/a11PiQHMEfqRk6JA4g3911dqPTfZBe
vUdBh8gl8maIzqeZGhvyeKTmull1Y0781yyuj98CAwEAAaAAMA0GCSqGSIb3DQEB
CwUAA4IBAQDR/gjfxN0IID1MidyhZB4qpdWn3m6qZnEQqoTyHHdWalbfNXcALC79
ffS+Smx40N5hEPvqy6euR89N5YuYvt8Hs+j7aWNBn7Wus5Favixcm2JcfCTJn2R3
r8FefuSs2xGkoyGsPFFcXE13SP/9zrZiwvOgSIuTdz/Pbh6GtEx7aV4DqHJsrXnb
XuPxpQleoBqKvQgSlmaEBsJg13TQB+Fl2foBVUtqAFDQiv+RIuircf0yesMCKJaK
MPH4Oo+r3pR8lI8ewfJPreRhCoV+XrGYMubaakz003TJ1xlOW8M+N9a6eFyMVh76
U1nY/KP8Ua6Lgaj9PRz7JCRzNoshZID/
-----END CERTIFICATE REQUEST-----

View File

@@ -4,3 +4,9 @@ keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.docker.internal
DNS.2 = hs1
DNS.3 = hs2
DNS.4 = hs3
DNS.5 = hs4
IP.1 = 127.0.0.1

View File

@@ -155,25 +155,19 @@ commonAttrs = {
# Keep sorted
include = [
".cargo"
"Cargo.lock"
"Cargo.toml"
"deps"
"src"
];
};
# This is redundant with CI
doCheck = false;
doCheck = true;
cargoTestCommand = "cargo test --locked ";
cargoExtraArgs = "--no-default-features --locked "
+ lib.optionalString
(features'' != [])
"--features " + (builtins.concatStringsSep "," features'');
cargoTestExtraArgs = "--no-default-features --locked "
+ lib.optionalString
(features'' != [])
"--features " + (builtins.concatStringsSep "," features'');
dontStrip = profile == "dev" || profile == "test";
dontPatchELF = profile == "dev" || profile == "test";
@@ -209,18 +203,12 @@ craneLib.buildPackage ( commonAttrs // {
env = buildDepsOnlyEnv;
});
# This is redundant with CI
doCheck = false;
doCheck = true;
cargoTestCommand = "cargo test --locked ";
cargoExtraArgs = "--no-default-features --locked "
+ lib.optionalString
(features'' != [])
"--features " + (builtins.concatStringsSep "," features'');
cargoTestExtraArgs = "--no-default-features --locked "
+ lib.optionalString
(features'' != [])
"--features " + (builtins.concatStringsSep "," features'');
env = buildPackageEnv;

View File

@@ -9,7 +9,7 @@
# If you're having trouble making the relevant changes, bug a maintainer.
[toolchain]
channel = "1.84.0"
channel = "1.86.0"
profile = "minimal"
components = [
# For rust-analyzer
@@ -24,5 +24,6 @@ targets = [
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-musl",
"aarch64-unknown-linux-gnu",
#"aarch64-apple-darwin",
]

View File

@@ -2,7 +2,7 @@ array_width = 80
chain_width = 60
comment_width = 80
condense_wildcard_suffixes = true
edition = "2024"
style_edition = "2024"
fn_call_width = 80
fn_single_line = true
format_code_in_doc_comments = true

View File

@@ -62,7 +62,7 @@ pub(super) async fn process(command: AdminCommand, context: &Command<'_>) -> Res
| Debug(command) => debug::process(command, context).await?,
| Query(command) => query::process(command, context).await?,
| Check(command) => check::process(command, context).await?,
};
}
Ok(())
}

View File

@@ -1,6 +1,6 @@
use ruma::{api::appservice::Registration, events::room::message::RoomMessageEventContent};
use crate::{admin_command, Result};
use crate::{Result, admin_command};
#[admin_command]
pub(super) async fn register(&self) -> Result<RoomMessageEventContent> {

View File

@@ -3,9 +3,9 @@
use conduwuit::Result;
use conduwuit_service::Services;
use futures::{
Future, FutureExt,
io::{AsyncWriteExt, BufWriter},
lock::Mutex,
Future, FutureExt,
};
use ruma::EventId;
@@ -21,7 +21,7 @@ impl Command<'_> {
pub(crate) fn write_fmt(
&self,
arguments: fmt::Arguments<'_>,
) -> impl Future<Output = Result> + Send + '_ {
) -> impl Future<Output = Result> + Send + '_ + use<'_> {
let buf = format!("{arguments}");
self.output.lock().then(|mut output| async move {
output.write_all(buf.as_bytes()).await.map_err(Into::into)

View File

@@ -6,19 +6,21 @@
};
use conduwuit::{
debug_error, err, info, trace, utils,
Error, Result, debug_error, err, info,
matrix::pdu::{PduEvent, PduId, RawPduId},
trace, utils,
utils::{
stream::{IterStream, ReadyExt},
string::EMPTY,
},
warn, Error, PduEvent, PduId, RawPduId, Result,
warn,
};
use futures::{FutureExt, StreamExt, TryStreamExt};
use ruma::{
api::{client::error::ErrorKind, federation::event::get_room_state},
events::room::message::RoomMessageEventContent,
CanonicalJsonObject, EventId, OwnedEventId, OwnedRoomOrAliasId, RoomId, RoomVersionId,
ServerName,
api::{client::error::ErrorKind, federation::event::get_room_state},
events::room::message::RoomMessageEventContent,
};
use service::rooms::{
short::{ShortEventId, ShortRoomId},
@@ -209,18 +211,21 @@ pub(super) async fn get_remote_pdu_list(
for pdu in list {
if force {
if let Err(e) = self.get_remote_pdu(Box::from(pdu), server.clone()).await {
failed_count = failed_count.saturating_add(1);
self.services
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Failed to get remote PDU, ignoring error: {e}"
)))
.await
.ok();
warn!("Failed to get remote PDU, ignoring error: {e}");
} else {
success_count = success_count.saturating_add(1);
match self.get_remote_pdu(Box::from(pdu), server.clone()).await {
| Err(e) => {
failed_count = failed_count.saturating_add(1);
self.services
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Failed to get remote PDU, ignoring error: {e}"
)))
.await
.ok();
warn!("Failed to get remote PDU, ignoring error: {e}");
},
| _ => {
success_count = success_count.saturating_add(1);
},
}
} else {
self.get_remote_pdu(Box::from(pdu), server.clone()).await?;
@@ -957,7 +962,7 @@ pub(super) async fn database_stats(
self.services
.db
.iter()
.filter(|(&name, _)| map_name.is_empty() || map_name == name)
.filter(|&(&name, _)| map_name.is_empty() || map_name == name)
.try_stream()
.try_for_each(|(&name, map)| {
let res = map.property(&property).expect("invalid property");

View File

@@ -1,7 +1,7 @@
use conduwuit::Err;
use ruma::events::room::message::RoomMessageEventContent;
use crate::{admin_command, admin_command_dispatch, Result};
use crate::{Result, admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[derive(Debug, clap::Subcommand)]

View File

@@ -3,7 +3,7 @@
use conduwuit::Result;
use futures::StreamExt;
use ruma::{
events::room::message::RoomMessageEventContent, OwnedRoomId, RoomId, ServerName, UserId,
OwnedRoomId, RoomId, ServerName, UserId, events::room::message::RoomMessageEventContent,
};
use crate::{admin_command, get_room_info};

View File

@@ -1,12 +1,12 @@
use std::time::Duration;
use conduwuit::{
debug, debug_info, debug_warn, error, info, trace, utils::time::parse_timepoint_ago, Result,
Result, debug, debug_info, debug_warn, error, info, trace, utils::time::parse_timepoint_ago,
};
use conduwuit_service::media::Dim;
use ruma::{
events::room::message::RoomMessageEventContent, EventId, Mxc, MxcUri, OwnedMxcUri,
OwnedServerName, ServerName,
EventId, Mxc, MxcUri, OwnedMxcUri, OwnedServerName, ServerName,
events::room::message::RoomMessageEventContent,
};
use crate::{admin_command, utils::parse_local_user_id};
@@ -41,103 +41,106 @@ pub(super) async fn delete(
let mut mxc_urls = Vec::with_capacity(4);
// parsing the PDU for any MXC URLs begins here
if let Ok(event_json) = self.services.rooms.timeline.get_pdu_json(&event_id).await {
if let Some(content_key) = event_json.get("content") {
debug!("Event ID has \"content\".");
let content_obj = content_key.as_object();
match self.services.rooms.timeline.get_pdu_json(&event_id).await {
| Ok(event_json) => {
if let Some(content_key) = event_json.get("content") {
debug!("Event ID has \"content\".");
let content_obj = content_key.as_object();
if let Some(content) = content_obj {
// 1. attempts to parse the "url" key
debug!("Attempting to go into \"url\" key for main media file");
if let Some(url) = content.get("url") {
debug!("Got a URL in the event ID {event_id}: {url}");
if let Some(content) = content_obj {
// 1. attempts to parse the "url" key
debug!("Attempting to go into \"url\" key for main media file");
if let Some(url) = content.get("url") {
debug!("Got a URL in the event ID {event_id}: {url}");
if url.to_string().starts_with("\"mxc://") {
debug!("Pushing URL {url} to list of MXCs to delete");
let final_url = url.to_string().replace('"', "");
mxc_urls.push(final_url);
} else {
info!(
"Found a URL in the event ID {event_id} but did not start with \
mxc://, ignoring"
);
}
}
// 2. attempts to parse the "info" key
debug!("Attempting to go into \"info\" key for thumbnails");
if let Some(info_key) = content.get("info") {
debug!("Event ID has \"info\".");
let info_obj = info_key.as_object();
if let Some(info) = info_obj {
if let Some(thumbnail_url) = info.get("thumbnail_url") {
debug!("Found a thumbnail_url in info key: {thumbnail_url}");
if thumbnail_url.to_string().starts_with("\"mxc://") {
debug!(
"Pushing thumbnail URL {thumbnail_url} to list of MXCs \
to delete"
);
let final_thumbnail_url =
thumbnail_url.to_string().replace('"', "");
mxc_urls.push(final_thumbnail_url);
} else {
info!(
"Found a thumbnail URL in the event ID {event_id} but \
did not start with mxc://, ignoring"
);
}
if url.to_string().starts_with("\"mxc://") {
debug!("Pushing URL {url} to list of MXCs to delete");
let final_url = url.to_string().replace('"', "");
mxc_urls.push(final_url);
} else {
info!(
"No \"thumbnail_url\" key in \"info\" key, assuming no \
thumbnails."
"Found a URL in the event ID {event_id} but did not start \
with mxc://, ignoring"
);
}
}
}
// 3. attempts to parse the "file" key
debug!("Attempting to go into \"file\" key");
if let Some(file_key) = content.get("file") {
debug!("Event ID has \"file\".");
let file_obj = file_key.as_object();
// 2. attempts to parse the "info" key
debug!("Attempting to go into \"info\" key for thumbnails");
if let Some(info_key) = content.get("info") {
debug!("Event ID has \"info\".");
let info_obj = info_key.as_object();
if let Some(file) = file_obj {
if let Some(url) = file.get("url") {
debug!("Found url in file key: {url}");
if let Some(info) = info_obj {
if let Some(thumbnail_url) = info.get("thumbnail_url") {
debug!("Found a thumbnail_url in info key: {thumbnail_url}");
if url.to_string().starts_with("\"mxc://") {
debug!("Pushing URL {url} to list of MXCs to delete");
let final_url = url.to_string().replace('"', "");
mxc_urls.push(final_url);
if thumbnail_url.to_string().starts_with("\"mxc://") {
debug!(
"Pushing thumbnail URL {thumbnail_url} to list of \
MXCs to delete"
);
let final_thumbnail_url =
thumbnail_url.to_string().replace('"', "");
mxc_urls.push(final_thumbnail_url);
} else {
info!(
"Found a thumbnail URL in the event ID {event_id} \
but did not start with mxc://, ignoring"
);
}
} else {
info!(
"Found a URL in the event ID {event_id} but did not \
start with mxc://, ignoring"
"No \"thumbnail_url\" key in \"info\" key, assuming no \
thumbnails."
);
}
} else {
info!("No \"url\" key in \"file\" key.");
}
}
// 3. attempts to parse the "file" key
debug!("Attempting to go into \"file\" key");
if let Some(file_key) = content.get("file") {
debug!("Event ID has \"file\".");
let file_obj = file_key.as_object();
if let Some(file) = file_obj {
if let Some(url) = file.get("url") {
debug!("Found url in file key: {url}");
if url.to_string().starts_with("\"mxc://") {
debug!("Pushing URL {url} to list of MXCs to delete");
let final_url = url.to_string().replace('"', "");
mxc_urls.push(final_url);
} else {
info!(
"Found a URL in the event ID {event_id} but did not \
start with mxc://, ignoring"
);
}
} else {
info!("No \"url\" key in \"file\" key.");
}
}
}
} else {
return Ok(RoomMessageEventContent::text_plain(
"Event ID does not have a \"content\" key or failed parsing the \
event ID JSON.",
));
}
} else {
return Ok(RoomMessageEventContent::text_plain(
"Event ID does not have a \"content\" key or failed parsing the event \
ID JSON.",
"Event ID does not have a \"content\" key, this is not a message or an \
event type that contains media.",
));
}
} else {
},
| _ => {
return Ok(RoomMessageEventContent::text_plain(
"Event ID does not have a \"content\" key, this is not a message or an \
event type that contains media.",
"Event ID does not exist or is not known to us.",
));
}
} else {
return Ok(RoomMessageEventContent::text_plain(
"Event ID does not exist or is not known to us.",
));
},
}
if mxc_urls.is_empty() {

View File

@@ -1,3 +1,4 @@
#![allow(rustdoc::broken_intra_doc_links)]
mod commands;
use clap::Subcommand;
@@ -27,18 +28,18 @@ pub(super) enum MediaCommand {
DeleteList,
/// - Deletes all remote (and optionally local) media created before or
/// after \[duration] time using filesystem metadata first created at
/// date, or fallback to last modified date. This will always ignore
/// errors by default.
/// after [duration] time using filesystem metadata first created at date,
/// or fallback to last modified date. This will always ignore errors by
/// default.
DeletePastRemoteMedia {
/// - The relative time (e.g. 30s, 5m, 7d) within which to search
duration: String,
/// - Only delete media created more recently than \[duration] ago
/// - Only delete media created before [duration] ago
#[arg(long, short)]
before: bool,
/// - Only delete media created after \[duration] ago
/// - Only delete media created after [duration] ago
#[arg(long, short)]
after: bool,

View File

@@ -8,7 +8,7 @@
use clap::{CommandFactory, Parser};
use conduwuit::{
debug, error,
Error, Result, debug, error,
log::{
capture,
capture::Capture,
@@ -16,24 +16,24 @@
},
trace,
utils::string::{collect_stream, common_prefix},
warn, Error, Result,
warn,
};
use futures::{future::FutureExt, io::BufWriter, AsyncWriteExt};
use futures::{AsyncWriteExt, future::FutureExt, io::BufWriter};
use ruma::{
EventId,
events::{
relation::InReplyTo,
room::message::{Relation::Reply, RoomMessageEventContent},
},
EventId,
};
use service::{
admin::{CommandInput, CommandOutput, ProcessorFuture, ProcessorResult},
Services,
admin::{CommandInput, CommandOutput, ProcessorFuture, ProcessorResult},
};
use tracing::Level;
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
use tracing_subscriber::{EnvFilter, filter::LevelFilter};
use crate::{admin, admin::AdminCommand, Command};
use crate::{Command, admin, admin::AdminCommand};
#[must_use]
pub(super) fn complete(line: &str) -> String { complete_command(AdminCommand::command(), line) }
@@ -91,6 +91,7 @@ async fn process_command(services: Arc<Services>, input: &CommandInput) -> Proce
}
}
#[allow(clippy::result_large_err)]
fn handle_panic(error: &Error, command: &CommandInput) -> ProcessorResult {
let link =
"Please submit a [bug report](https://github.com/girlbossceo/conduwuit/issues/new). 🥺";
@@ -100,7 +101,7 @@ fn handle_panic(error: &Error, command: &CommandInput) -> ProcessorResult {
Err(reply(content, command.reply_id.as_deref()))
}
// Parse and process a message from the admin room
/// Parse and process a message from the admin room
async fn process(
context: &Command<'_>,
command: AdminCommand,
@@ -164,7 +165,8 @@ fn capture_create(context: &Command<'_>) -> (Arc<Capture>, Arc<Mutex<String>>) {
(capture, logs)
}
// Parse chat messages from the admin room into an AdminCommand object
/// Parse chat messages from the admin room into an AdminCommand object
#[allow(clippy::result_large_err)]
fn parse<'a>(
services: &Arc<Services>,
input: &'a CommandInput,
@@ -232,7 +234,7 @@ fn complete_command(mut cmd: clap::Command, line: &str) -> String {
ret.join(" ")
}
// Parse chat messages from the admin room into an AdminCommand object
/// Parse chat messages from the admin room into an AdminCommand object
fn parse_line(command_line: &str) -> Vec<String> {
let mut argv = command_line
.split_whitespace()

View File

@@ -1,7 +1,7 @@
use clap::Subcommand;
use conduwuit::Result;
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, RoomId, UserId};
use ruma::{RoomId, UserId, events::room::message::RoomMessageEventContent};
use crate::{admin_command, admin_command_dispatch};

View File

@@ -1,16 +1,16 @@
use std::{borrow::Cow, collections::BTreeMap, ops::Deref};
use std::{borrow::Cow, collections::BTreeMap, ops::Deref, sync::Arc};
use clap::Subcommand;
use conduwuit::{
apply, at, is_zero,
Err, Result, apply, at, is_zero,
utils::{
stream::{ReadyExt, TryIgnore, TryParallelExt},
stream::{IterStream, ReadyExt, TryIgnore, TryParallelExt},
string::EMPTY,
IterStream,
},
Err, Result,
};
use futures::{FutureExt, StreamExt, TryStreamExt};
use conduwuit_database::Map;
use conduwuit_service::Services;
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
use ruma::events::room::message::RoomMessageEventContent;
use tokio::time::Instant;
@@ -173,22 +173,18 @@ pub(super) async fn compact(
) -> Result<RoomMessageEventContent> {
use conduwuit_database::compact::Options;
let default_all_maps = map
.is_none()
.then(|| {
self.services
.db
.keys()
.map(Deref::deref)
.map(ToOwned::to_owned)
})
.into_iter()
.flatten();
let default_all_maps: Option<_> = map.is_none().then(|| {
self.services
.db
.keys()
.map(Deref::deref)
.map(ToOwned::to_owned)
});
let maps: Vec<_> = map
.unwrap_or_default()
.into_iter()
.chain(default_all_maps)
.chain(default_all_maps.into_iter().flatten())
.map(|map| self.services.db.get(&map))
.filter_map(Result::ok)
.cloned()
@@ -238,25 +234,8 @@ pub(super) async fn raw_count(
) -> Result<RoomMessageEventContent> {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let default_all_maps = map
.is_none()
.then(|| self.services.db.keys().map(Deref::deref))
.into_iter()
.flatten();
let maps: Vec<_> = map
.iter()
.map(String::as_str)
.chain(default_all_maps)
.map(|map| self.services.db.get(map))
.filter_map(Result::ok)
.cloned()
.collect();
let timer = Instant::now();
let count = maps
.iter()
.stream()
let count = with_maps_or(map.as_deref(), self.services)
.then(|map| map.raw_count_prefix(&prefix))
.ready_fold(0_usize, usize::saturating_add)
.await;
@@ -301,25 +280,8 @@ pub(super) async fn raw_keys_sizes(
) -> Result<RoomMessageEventContent> {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let default_all_maps = map
.is_none()
.then(|| self.services.db.keys().map(Deref::deref))
.into_iter()
.flatten();
let maps: Vec<_> = map
.iter()
.map(String::as_str)
.chain(default_all_maps)
.map(|map| self.services.db.get(map))
.filter_map(Result::ok)
.cloned()
.collect();
let timer = Instant::now();
let result = maps
.iter()
.stream()
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_keys_prefix(&prefix))
.flatten()
.ignore_err()
@@ -346,25 +308,8 @@ pub(super) async fn raw_keys_total(
) -> Result<RoomMessageEventContent> {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let default_all_maps = map
.is_none()
.then(|| self.services.db.keys().map(Deref::deref))
.into_iter()
.flatten();
let maps: Vec<_> = map
.iter()
.map(String::as_str)
.chain(default_all_maps)
.map(|map| self.services.db.get(map))
.filter_map(Result::ok)
.cloned()
.collect();
let timer = Instant::now();
let result = maps
.iter()
.stream()
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_keys_prefix(&prefix))
.flatten()
.ignore_err()
@@ -388,25 +333,8 @@ pub(super) async fn raw_vals_sizes(
) -> Result<RoomMessageEventContent> {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let default_all_maps = map
.is_none()
.then(|| self.services.db.keys().map(Deref::deref))
.into_iter()
.flatten();
let maps: Vec<_> = map
.iter()
.map(String::as_str)
.chain(default_all_maps)
.map(|map| self.services.db.get(map))
.filter_map(Result::ok)
.cloned()
.collect();
let timer = Instant::now();
let result = maps
.iter()
.stream()
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_stream_prefix(&prefix))
.flatten()
.ignore_err()
@@ -434,25 +362,8 @@ pub(super) async fn raw_vals_total(
) -> Result<RoomMessageEventContent> {
let prefix = prefix.as_deref().unwrap_or(EMPTY);
let default_all_maps = map
.is_none()
.then(|| self.services.db.keys().map(Deref::deref))
.into_iter()
.flatten();
let maps: Vec<_> = map
.iter()
.map(String::as_str)
.chain(default_all_maps)
.map(|map| self.services.db.get(map))
.filter_map(Result::ok)
.cloned()
.collect();
let timer = Instant::now();
let result = maps
.iter()
.stream()
let result = with_maps_or(map.as_deref(), self.services)
.map(|map| map.raw_stream_prefix(&prefix))
.flatten()
.ignore_err()
@@ -574,3 +485,20 @@ pub(super) async fn raw_maps(&self) -> Result<RoomMessageEventContent> {
Ok(RoomMessageEventContent::notice_markdown(format!("{list:#?}")))
}
fn with_maps_or<'a>(
map: Option<&'a str>,
services: &'a Services,
) -> impl Stream<Item = &'a Arc<Map>> + Send + 'a {
let default_all_maps = map
.is_none()
.then(|| services.db.keys().map(Deref::deref))
.into_iter()
.flatten();
map.into_iter()
.chain(default_all_maps)
.map(|map| services.db.get(map))
.filter_map(Result::ok)
.stream()
}

View File

@@ -1,7 +1,7 @@
use clap::Subcommand;
use conduwuit::{utils::time, Result};
use conduwuit::{Result, utils::time};
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, OwnedServerName};
use ruma::{OwnedServerName, events::room::message::RoomMessageEventContent};
use crate::{admin_command, admin_command_dispatch};

View File

@@ -1,7 +1,7 @@
use clap::Subcommand;
use conduwuit::{Error, Result};
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, RoomId, ServerName, UserId};
use ruma::{RoomId, ServerName, UserId, events::room::message::RoomMessageEventContent};
use crate::Command;

View File

@@ -1,7 +1,7 @@
use clap::Subcommand;
use conduwuit::{utils::stream::TryTools, PduCount, Result};
use conduwuit::{PduCount, Result, utils::stream::TryTools};
use futures::TryStreamExt;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomOrAliasId};
use ruma::{OwnedRoomOrAliasId, events::room::message::RoomMessageEventContent};
use crate::{admin_command, admin_command_dispatch};

View File

@@ -1,7 +1,7 @@
use clap::Subcommand;
use conduwuit::Result;
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, ServerName, UserId};
use ruma::{ServerName, UserId, events::room::message::RoomMessageEventContent};
use service::sending::Destination;
use crate::Command;

View File

@@ -1,6 +1,6 @@
use clap::Subcommand;
use conduwuit::Result;
use ruma::{events::room::message::RoomMessageEventContent, OwnedEventId, OwnedRoomOrAliasId};
use ruma::{OwnedEventId, OwnedRoomOrAliasId, events::room::message::RoomMessageEventContent};
use crate::{admin_command, admin_command_dispatch};

View File

@@ -2,7 +2,7 @@
use conduwuit::Result;
use futures::stream::StreamExt;
use ruma::{
events::room::message::RoomMessageEventContent, OwnedDeviceId, OwnedRoomId, OwnedUserId,
OwnedDeviceId, OwnedRoomId, OwnedUserId, events::room::message::RoomMessageEventContent,
};
use crate::{admin_command, admin_command_dispatch};

View File

@@ -4,10 +4,10 @@
use conduwuit::Result;
use futures::StreamExt;
use ruma::{
events::room::message::RoomMessageEventContent, OwnedRoomAliasId, OwnedRoomId, RoomId,
OwnedRoomAliasId, OwnedRoomId, RoomId, events::room::message::RoomMessageEventContent,
};
use crate::{escape_html, Command};
use crate::{Command, escape_html};
#[derive(Debug, Subcommand)]
pub(crate) enum RoomAliasCommand {
@@ -66,10 +66,11 @@ pub(super) async fn reprocess(
format!("#{}:{}", room_alias_localpart, services.globals.server_name());
let room_alias = match OwnedRoomAliasId::parse(room_alias_str) {
| Ok(alias) => alias,
| Err(err) =>
| Err(err) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to parse alias: {err}"
))),
)));
},
};
match command {
| RoomAliasCommand::Set { force, room_id, .. } => {

View File

@@ -1,8 +1,8 @@
use conduwuit::Result;
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
use ruma::{OwnedRoomId, events::room::message::RoomMessageEventContent};
use crate::{admin_command, get_room_info, PAGE_SIZE};
use crate::{PAGE_SIZE, admin_command, get_room_info};
#[admin_command]
pub(super) async fn list_rooms(
@@ -42,7 +42,7 @@ pub(super) async fn list_rooms(
if rooms.is_empty() {
return Ok(RoomMessageEventContent::text_plain("No more rooms."));
};
}
let output_plain = format!(
"Rooms ({}):\n```\n{}\n```",

View File

@@ -1,9 +1,9 @@
use clap::Subcommand;
use conduwuit::Result;
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, RoomId};
use ruma::{RoomId, events::room::message::RoomMessageEventContent};
use crate::{get_room_info, Command, PAGE_SIZE};
use crate::{Command, PAGE_SIZE, get_room_info};
#[derive(Debug, Subcommand)]
pub(crate) enum RoomDirectoryCommand {
@@ -67,7 +67,7 @@ pub(super) async fn reprocess(
if rooms.is_empty() {
return Ok(RoomMessageEventContent::text_plain("No more rooms."));
};
}
let output = format!(
"Rooms (page {page}):\n```\n{}\n```",

View File

@@ -1,7 +1,7 @@
use clap::Subcommand;
use conduwuit::{utils::ReadyExt, Result};
use conduwuit::{Result, utils::ReadyExt};
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, RoomId};
use ruma::{RoomId, events::room::message::RoomMessageEventContent};
use crate::{admin_command, admin_command_dispatch};

View File

@@ -1,14 +1,14 @@
use api::client::leave_room;
use clap::Subcommand;
use conduwuit::{
debug, error, info,
Result, debug,
utils::{IterStream, ReadyExt},
warn, Result,
warn,
};
use futures::StreamExt;
use ruma::{
events::room::message::RoomMessageEventContent, OwnedRoomId, RoomAliasId, RoomId,
RoomOrAliasId,
OwnedRoomId, RoomAliasId, RoomId, RoomOrAliasId,
events::room::message::RoomMessageEventContent,
};
use crate::{admin_command, admin_command_dispatch, get_room_info};
@@ -17,51 +17,23 @@
#[derive(Debug, Subcommand)]
pub(crate) enum RoomModerationCommand {
/// - Bans a room from local users joining and evicts all our local users
/// (including server
/// admins)
/// from the room. Also blocks any invites (local and remote) for the
/// banned room.
///
/// Server admins (users in the conduwuit admin room) will not be evicted
/// and server admins can still join the room. To evict admins too, use
/// --force (also ignores errors) To disable incoming federation of the
/// room, use --disable-federation
/// banned room, and disables federation entirely with it.
BanRoom {
#[arg(short, long)]
/// Evicts admins out of the room and ignores any potential errors when
/// making our local users leave the room
force: bool,
#[arg(long)]
/// Disables incoming federation of the room after banning and evicting
/// users
disable_federation: bool,
/// The room in the format of `!roomid:example.com` or a room alias in
/// the format of `#roomalias:example.com`
room: Box<RoomOrAliasId>,
},
/// - Bans a list of rooms (room IDs and room aliases) from a newline
/// delimited codeblock similar to `user deactivate-all`
BanListOfRooms {
#[arg(short, long)]
/// Evicts admins out of the room and ignores any potential errors when
/// making our local users leave the room
force: bool,
#[arg(long)]
/// Disables incoming federation of the room after banning and evicting
/// users
disable_federation: bool,
},
/// delimited codeblock similar to `user deactivate-all`. Applies the same
/// steps as ban-room
BanListOfRooms,
/// - Unbans a room to allow local users to join again
///
/// To re-enable incoming federation of the room, use --enable-federation
UnbanRoom {
#[arg(long)]
/// Enables incoming federation of the room after unbanning
enable_federation: bool,
/// The room in the format of `!roomid:example.com` or a room alias in
/// the format of `#roomalias:example.com`
room: Box<RoomOrAliasId>,
@@ -77,12 +49,7 @@ pub(crate) enum RoomModerationCommand {
}
#[admin_command]
async fn ban_room(
&self,
force: bool,
disable_federation: bool,
room: Box<RoomOrAliasId>,
) -> Result<RoomMessageEventContent> {
async fn ban_room(&self, room: Box<RoomOrAliasId>) -> Result<RoomMessageEventContent> {
debug!("Got room alias or ID: {}", room);
let admin_room_alias = &self.services.globals.admin_alias;
@@ -96,12 +63,13 @@ async fn ban_room(
let room_id = if room.is_room_id() {
let room_id = match RoomId::parse(&room) {
| Ok(room_id) => room_id,
| Err(e) =>
| 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");
@@ -111,12 +79,13 @@ async fn ban_room(
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
| Ok(room_alias) => room_alias,
| Err(e) =>
| 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!(
@@ -124,41 +93,42 @@ async fn ban_room(
locally, if not using get_alias_helper to fetch room ID remotely"
);
let room_id = if let Ok(room_id) = self
let room_id = match self
.services
.rooms
.alias
.resolve_local_alias(room_alias)
.await
{
room_id
} else {
debug!(
"We don't have this room alias to a room ID locally, attempting to fetch room \
ID over federation"
);
| Ok(room_id) => room_id,
| _ => {
debug!(
"We don't have this room alias to a room ID locally, attempting to fetch \
room ID over federation"
);
match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
{
| Ok((room_id, servers)) => {
debug!(
?room_id,
?servers,
"Got federation response fetching room ID for {room_id}"
);
room_id
},
| Err(e) => {
return Ok(RoomMessageEventContent::notice_plain(format!(
"Failed to resolve room alias {room_alias} to a room ID: {e}"
)));
},
}
match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
{
| Ok((room_id, servers)) => {
debug!(
?room_id,
?servers,
"Got federation response fetching room ID for {room_id}"
);
room_id
},
| Err(e) => {
return Ok(RoomMessageEventContent::notice_plain(format!(
"Failed to resolve room alias {room_alias} to a room ID: {e}"
)));
},
}
},
};
self.services.rooms.metadata.ban_room(&room_id, true);
@@ -172,98 +142,56 @@ async fn ban_room(
));
};
debug!("Making all users leave the room {}", &room);
if force {
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
debug!("Making all users leave the room {room_id} and forgetting it");
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
while let Some(local_user) = users.next().await {
debug!(
"Attempting leave for user {local_user} in room {room_id} (forced, ignoring all \
errors, evicting admins too)",
);
while let Some(ref user_id) = users.next().await {
debug!(
"Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \
evicting admins too)",
);
if let Err(e) = leave_room(self.services, local_user, &room_id, None).await {
warn!(%e, "Failed to leave room");
}
if let Err(e) = leave_room(self.services, user_id, &room_id, None).await {
warn!("Failed to leave room: {e}");
}
} else {
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
while let Some(local_user) = users.next().await {
if self.services.users.is_admin(local_user).await {
continue;
}
debug!("Attempting leave for user {} in room {}", &local_user, &room_id);
if let Err(e) = leave_room(self.services, 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
)));
}
}
self.services.rooms.state_cache.forget(&room_id, user_id);
}
// remove any local aliases, ignore errors
for local_alias in &self
.services
self.services
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await
{
_ = self
.services
.rooms
.alias
.remove_alias(local_alias, &self.services.globals.server_user)
.await;
}
.for_each(|local_alias| async move {
self.services
.rooms
.alias
.remove_alias(&local_alias, &self.services.globals.server_user)
.await
.ok();
})
.await;
// unpublish from room directory, ignore errors
// unpublish from room directory
self.services.rooms.directory.set_not_public(&room_id);
if disable_federation {
self.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.",
));
}
self.services.rooms.metadata.disable_room(&room_id, true);
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.",
"Room banned, removed all our local users, and disabled incoming federation with room.",
))
}
#[admin_command]
async fn ban_list_of_rooms(
&self,
force: bool,
disable_federation: bool,
) -> Result<RoomMessageEventContent> {
async fn ban_list_of_rooms(&self) -> Result<RoomMessageEventContent> {
if self.body.len() < 2
|| !self.body[0].trim().starts_with("```")
|| self.body.last().unwrap_or(&"").trim() != "```"
@@ -290,7 +218,7 @@ async fn ban_list_of_rooms(
if let Ok(admin_room_id) = self.services.admin.get_admin_room().await {
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");
warn!("User specified admin room in bulk ban list, ignoring");
continue;
}
}
@@ -299,19 +227,12 @@ async fn ban_list_of_rooms(
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}"
)));
// ignore rooms we failed to parse
warn!(
"Error parsing room \"{room}\" during bulk room banning, \
ignoring error and logging here: {e}"
);
continue;
},
};
@@ -321,87 +242,65 @@ async fn ban_list_of_rooms(
if room_alias_or_id.is_room_alias_id() {
match RoomAliasId::parse(room_alias_or_id) {
| Ok(room_alias) => {
let room_id = if let Ok(room_id) = self
let room_id = match self
.services
.rooms
.alias
.resolve_local_alias(room_alias)
.await
{
room_id
} else {
debug!(
"We don't have this room alias to a room ID locally, \
attempting to fetch room ID over federation"
);
| Ok(room_id) => room_id,
| _ => {
debug!(
"We don't have this room alias to a room ID locally, \
attempting to fetch room ID over federation"
);
match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
{
| Ok((room_id, servers)) => {
debug!(
?room_id,
?servers,
"Got federation response fetching room ID for {room}",
);
room_id
},
| Err(e) => {
// don't fail if force blocking
if force {
match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
{
| Ok((room_id, servers)) => {
debug!(
?room_id,
?servers,
"Got federation response fetching room ID for \
{room}",
);
room_id
},
| Err(e) => {
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}"
)));
warn!(
"Error parsing room \"{room}\" during bulk room banning, \
ignoring error and logging here: {e}"
);
continue;
},
}
}
},
| 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}"
)));
warn!(
"Error parsing room \"{room}\" during bulk room banning, ignoring error and \
logging here: {e}"
);
continue;
},
}
}
@@ -412,56 +311,27 @@ async fn ban_list_of_rooms(
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 {
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
debug!("Making all users leave the room {room_id} and forgetting it");
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
while let Some(local_user) = users.next().await {
debug!(
"Attempting leave for user {local_user} in room {room_id} (forced, ignoring \
all errors, evicting admins too)",
);
while let Some(ref user_id) = users.next().await {
debug!(
"Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \
evicting admins too)",
);
if let Err(e) = leave_room(self.services, local_user, &room_id, None).await {
warn!(%e, "Failed to leave room");
}
if let Err(e) = leave_room(self.services, user_id, &room_id, None).await {
warn!("Failed to leave room: {e}");
}
} else {
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
while let Some(local_user) = users.next().await {
if self.services.users.is_admin(local_user).await {
continue;
}
debug!("Attempting leave for user {local_user} in room {room_id}");
if let Err(e) = leave_room(self.services, local_user, &room_id, None).await {
error!(
"Error attempting to make local user {local_user} leave room {room_id} \
during bulk room banning: {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
)));
}
}
self.services.rooms.state_cache.forget(&room_id, user_id);
}
// remove any local aliases, ignore errors
@@ -483,38 +353,27 @@ async fn ban_list_of_rooms(
// unpublish from room directory, ignore errors
self.services.rooms.directory.set_not_public(&room_id);
if disable_federation {
self.services.rooms.metadata.disable_room(&room_id, true);
}
self.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."
)))
}
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."
)))
}
#[admin_command]
async fn unban_room(
&self,
enable_federation: bool,
room: Box<RoomOrAliasId>,
) -> Result<RoomMessageEventContent> {
async fn unban_room(&self, room: Box<RoomOrAliasId>) -> Result<RoomMessageEventContent> {
let room_id = if room.is_room_id() {
let room_id = match RoomId::parse(&room) {
| Ok(room_id) => room_id,
| Err(e) =>
| 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");
@@ -524,12 +383,13 @@ async fn unban_room(
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
| Ok(room_alias) => room_alias,
| Err(e) =>
| 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!(
@@ -537,41 +397,42 @@ async fn unban_room(
locally, if not using get_alias_helper to fetch room ID remotely"
);
let room_id = if let Ok(room_id) = self
let room_id = match self
.services
.rooms
.alias
.resolve_local_alias(room_alias)
.await
{
room_id
} else {
debug!(
"We don't have this room alias to a room ID locally, attempting to fetch room \
ID over federation"
);
| Ok(room_id) => room_id,
| _ => {
debug!(
"We don't have this room alias to a room ID locally, attempting to fetch \
room ID over federation"
);
match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
{
| Ok((room_id, servers)) => {
debug!(
?room_id,
?servers,
"Got federation response fetching room ID for room {room}"
);
room_id
},
| Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to resolve room alias {room} to a room ID: {e}"
)));
},
}
match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
{
| Ok((room_id, servers)) => {
debug!(
?room_id,
?servers,
"Got federation response fetching room ID for room {room}"
);
room_id
},
| Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to resolve room alias {room} to a room ID: {e}"
)));
},
}
},
};
self.services.rooms.metadata.ban_room(&room_id, false);
@@ -585,15 +446,8 @@ async fn unban_room(
));
};
if enable_federation {
self.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.",
))
self.services.rooms.metadata.disable_room(&room_id, false);
Ok(RoomMessageEventContent::text_plain("Room unbanned and federation re-enabled."))
}
#[admin_command]

View File

@@ -1,6 +1,6 @@
use std::{fmt::Write, path::PathBuf, sync::Arc};
use conduwuit::{info, utils::time, warn, Err, Result};
use conduwuit::{Err, Result, info, utils::time, warn};
use ruma::events::room::message::RoomMessageEventContent;
use crate::admin_command;

View File

@@ -2,23 +2,24 @@
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room};
use conduwuit::{
debug_warn, error, info, is_equal_to,
Result, debug, debug_warn, error, info, is_equal_to,
matrix::pdu::PduBuilder,
utils::{self, ReadyExt},
warn, PduBuilder, Result,
warn,
};
use conduwuit_api::client::{leave_all_rooms, update_avatar_url, update_displayname};
use futures::StreamExt;
use ruma::{
EventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, RoomId, UserId,
events::{
RoomAccountDataEventType, StateEventType,
room::{
message::RoomMessageEventContent,
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
redaction::RoomRedactionEventContent,
},
tag::{TagEvent, TagEventContent, TagInfo},
RoomAccountDataEventType, StateEventType,
},
EventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, RoomId, UserId,
};
use crate::{
@@ -57,16 +58,16 @@ pub(super) async fn create_user(
// Validate user id
let user_id = parse_local_user_id(self.services, &username)?;
if self.services.users.exists(&user_id).await {
return Ok(RoomMessageEventContent::text_plain(format!(
"Userid {user_id} already exists"
)));
if let Err(e) = user_id.validate_strict() {
if self.services.config.emergency_password.is_none() {
return Ok(RoomMessageEventContent::text_plain(format!(
"Username {user_id} contains disallowed characters or spaces: {e}"
)));
}
}
if user_id.is_historical() {
return Ok(RoomMessageEventContent::text_plain(format!(
"User ID {user_id} does not conform to new Matrix identifier spec"
)));
if self.services.users.exists(&user_id).await {
return Ok(RoomMessageEventContent::text_plain(format!("User {user_id} already exists")));
}
let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
@@ -166,7 +167,7 @@ pub(super) async fn create_user(
"Failed to automatically join room {room} for user {user_id}: {e}"
);
},
};
}
}
}
}
@@ -185,12 +186,12 @@ pub(super) async fn create_user(
.is_ok_and(is_equal_to!(1))
{
self.services.admin.make_user_admin(&user_id).await?;
warn!("Granting {user_id} admin privileges as the first user");
}
} else {
debug!("create_user admin command called without an admin room being available");
}
// Inhibit login does not work for guests
Ok(RoomMessageEventContent::text_plain(format!(
"Created user with user_id: {user_id} and password: `{password}`"
)))
@@ -550,7 +551,7 @@ pub(super) async fn force_join_list_of_local_users(
debug_warn!("Failed force joining {user_id} to {room_id} during bulk join: {e}");
failed_joins = failed_joins.saturating_add(1);
},
};
}
}
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -646,7 +647,7 @@ pub(super) async fn force_join_all_local_users(
debug_warn!("Failed force joining {user_id} to {room_id} during bulk join: {e}");
failed_joins = failed_joins.saturating_add(1);
},
};
}
}
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -694,6 +695,19 @@ pub(super) async fn force_leave_room(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if !self
.services
.rooms
.state_cache
.is_joined(&user_id, &room_id)
.await
{
return Ok(RoomMessageEventContent::notice_markdown(format!(
"{user_id} is not joined in the room"
)));
}
leave_room(self.services, &user_id, &room_id, None).await?;
Ok(RoomMessageEventContent::notice_markdown(format!(

View File

@@ -1,4 +1,4 @@
use conduwuit_core::{err, Err, Result};
use conduwuit_core::{Err, Result, err};
use ruma::{OwnedRoomId, OwnedUserId, RoomId, UserId};
use service::Services;

View File

@@ -35,6 +35,7 @@ brotli_compression = [
]
[dependencies]
async-trait.workspace = true
axum-client-ip.workspace = true
axum-extra.workspace = true
axum.workspace = true

View File

@@ -3,34 +3,38 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
debug_info, error, info, is_equal_to, utils, utils::ReadyExt, warn, Error, PduBuilder, Result,
Err, Error, Result, debug_info, err, error, info, is_equal_to,
matrix::pdu::PduBuilder,
utils,
utils::{ReadyExt, stream::BroadbandExt},
warn,
};
use conduwuit_service::Services;
use futures::{FutureExt, StreamExt};
use register::RegistrationKind;
use ruma::{
OwnedRoomId, UserId,
api::client::{
account::{
change_password, check_registration_token_validity, deactivate, get_3pids,
get_username_availability,
ThirdPartyIdRemovalStatus, 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,
whoami,
},
error::ErrorKind,
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
events::{
GlobalAccountDataEventType, StateEventType,
room::{
message::RoomMessageEventContent,
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
GlobalAccountDataEventType, StateEventType,
},
push, OwnedRoomId, UserId,
push,
};
use service::Services;
use super::{join_room_by_id_helper, DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
use crate::Ruma;
const RANDOM_USER_ID_LENGTH: usize = 10;
@@ -59,6 +63,14 @@ pub(crate) async fn get_register_available_route(
|| appservice.registration.id.contains("matrix_appservice_irc")
});
if services
.globals
.forbidden_usernames()
.is_match(&body.username)
{
return Err!(Request(Forbidden("Username is forbidden")));
}
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
body.username.clone()
@@ -67,30 +79,45 @@ pub(crate) async fn get_register_available_route(
};
// Validate user id
let user_id = UserId::parse_with_server_name(body_username, services.globals.server_name())
.ok()
.filter(|user_id| {
(!user_id.is_historical() || is_matrix_appservice_irc)
&& services.globals.user_is_local(user_id)
})
.ok_or(Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
let user_id =
match UserId::parse_with_server_name(&body_username, services.globals.server_name()) {
| Ok(user_id) => {
if let Err(e) = user_id.validate_strict() {
// unless the username is from the broken matrix appservice IRC bridge, we
// should follow synapse's behaviour on not allowing things like spaces
// and UTF-8 characters in usernames
if !is_matrix_appservice_irc {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} contains disallowed characters or spaces: \
{e}"
))));
}
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} is not valid: {e}"
))));
},
};
// Check if username is creative enough
if services.users.exists(&user_id).await {
return Err(Error::BadRequest(ErrorKind::UserInUse, "Desired user ID is already taken."));
return Err!(Request(UserInUse("User ID is not available.")));
}
if services
.globals
.forbidden_usernames()
.is_match(user_id.localpart())
{
return Err(Error::BadRequest(ErrorKind::Unknown, "Username is forbidden."));
if let Some(ref info) = body.appservice_info {
if !info.is_user_match(&user_id) {
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
}
}
// TODO add check for appservice namespaces
if services.appservice.is_exclusive_user_id(&user_id).await {
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
// If no if check is true we have an username that's available to be used.
Ok(get_username_availability::v3::Response { available: true })
}
@@ -118,20 +145,31 @@ pub(crate) async fn register_route(
InsecureClientIp(client): 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 \"{}\"",
body.username.as_deref().unwrap_or("")
);
return Err(Error::BadRequest(ErrorKind::forbidden(), "Registration has been disabled."));
let is_guest = body.kind == RegistrationKind::Guest;
let emergency_mode_enabled = services.config.emergency_password.is_some();
if !services.config.allow_registration && body.appservice_info.is_none() {
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
| (Some(username), Some(device_display_name)) => {
info!(%is_guest, user = %username, device_name = %device_display_name, "Rejecting registration attempt as registration is disabled");
},
| (Some(username), _) => {
info!(%is_guest, user = %username, "Rejecting registration attempt as registration is disabled");
},
| (_, Some(device_display_name)) => {
info!(%is_guest, device_name = %device_display_name, "Rejecting registration attempt as registration is disabled");
},
| (None, _) => {
info!(%is_guest, "Rejecting registration attempt as registration is disabled");
},
}
return Err!(Request(Forbidden("Registration has been disabled.")));
}
let is_guest = body.kind == RegistrationKind::Guest;
if is_guest
&& (!services.globals.allow_guest_registration()
|| (services.globals.allow_registration()
&& (!services.config.allow_guest_registration
|| (services.config.allow_registration
&& services.globals.registration_token.is_some()))
{
info!(
@@ -139,10 +177,7 @@ pub(crate) async fn register_route(
rejecting guest registration attempt, initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err(Error::BadRequest(
ErrorKind::GuestAccessForbidden,
"Guest registration is disabled.",
));
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
}
// forbid guests from registering if there is not a real admin user yet. give
@@ -153,13 +188,10 @@ pub(crate) async fn register_route(
rejecting registration. Guest's initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Registration temporarily disabled.",
));
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
}
let user_id = match (&body.username, is_guest) {
let user_id = match (body.username.as_ref(), is_guest) {
| (Some(username), false) => {
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
let is_matrix_appservice_irc =
@@ -169,6 +201,12 @@ pub(crate) async fn register_route(
|| appservice.registration.id.contains("matrix_appservice_irc")
});
if services.globals.forbidden_usernames().is_match(username)
&& !emergency_mode_enabled
{
return Err!(Request(Forbidden("Username is forbidden")));
}
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
username.clone()
@@ -176,31 +214,34 @@ pub(crate) async fn register_route(
username.to_lowercase()
};
let proposed_user_id =
UserId::parse_with_server_name(body_username, services.globals.server_name())
.ok()
.filter(|user_id| {
(!user_id.is_historical() || is_matrix_appservice_irc)
&& services.globals.user_is_local(user_id)
})
.ok_or(Error::BadRequest(
ErrorKind::InvalidUsername,
"Username is invalid.",
))?;
let proposed_user_id = match UserId::parse_with_server_name(
&body_username,
services.globals.server_name(),
) {
| Ok(user_id) => {
if let Err(e) = user_id.validate_strict() {
// unless the username is from the broken matrix appservice IRC bridge, or
// we are in emergency mode, we should follow synapse's behaviour on
// not allowing things like spaces and UTF-8 characters in usernames
if !is_matrix_appservice_irc && !emergency_mode_enabled {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} contains disallowed characters or \
spaces: {e}"
))));
}
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} is not valid: {e}"
))));
},
};
if services.users.exists(&proposed_user_id).await {
return Err(Error::BadRequest(
ErrorKind::UserInUse,
"Desired user ID is already taken.",
));
}
if services
.globals
.forbidden_usernames()
.is_match(proposed_user_id.localpart())
{
return Err(Error::BadRequest(ErrorKind::Unknown, "Username is forbidden."));
return Err!(Request(UserInUse("User ID is not available.")));
}
proposed_user_id
@@ -218,15 +259,20 @@ pub(crate) async fn register_route(
};
if body.body.login_type == Some(LoginType::ApplicationService) {
if let Some(ref info) = body.appservice_info {
if !info.is_user_match(&user_id) {
return Err(Error::BadRequest(ErrorKind::Exclusive, "User is not in namespace."));
}
} else {
return Err(Error::BadRequest(ErrorKind::MissingToken, "Missing appservice token."));
match body.appservice_info {
| Some(ref info) =>
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
return Err!(Request(Exclusive(
"Username is not in an appservice namespace."
)));
},
| _ => {
return Err!(Request(MissingToken("Missing appservice token.")));
},
}
} else if services.appservice.is_exclusive_user_id(&user_id).await {
return Err(Error::BadRequest(ErrorKind::Exclusive, "User ID reserved by appservice."));
} else if services.appservice.is_exclusive_user_id(&user_id).await && !emergency_mode_enabled
{
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
// UIAA
@@ -256,33 +302,39 @@ pub(crate) async fn register_route(
};
if !skip_auth {
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(
&UserId::parse_with_server_name("", services.globals.server_name())
.expect("we know this is valid"),
"".into(),
auth,
&uiaainfo,
)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services.uiaa.create(
&UserId::parse_with_server_name("", services.globals.server_name())
.expect("we know this is valid"),
"".into(),
&uiaainfo,
&json,
);
return Err(Error::Uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(
&UserId::parse_with_server_name("", services.globals.server_name())
.unwrap(),
"".into(),
auth,
&uiaainfo,
)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services.uiaa.create(
&UserId::parse_with_server_name("", services.globals.server_name())
.unwrap(),
"".into(),
&uiaainfo,
json,
);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
}
@@ -323,8 +375,12 @@ pub(crate) async fn register_route(
)
.await?;
// Inhibit login does not work for guests
if !is_guest && body.inhibit_login {
if (!is_guest && body.inhibit_login)
|| body
.appservice_info
.as_ref()
.is_some_and(|appservice| appservice.registration.device_management)
{
return Ok(register::v3::Response {
access_token: None,
user_id,
@@ -391,8 +447,8 @@ pub(crate) async fn register_route(
}
// 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.");
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
debug_info!("New guest user \"{user_id}\" registered on this server.");
if !device_display_name.is_empty() {
if services.server.config.admin_room_notices {
@@ -421,7 +477,8 @@ pub(crate) async fn register_route(
}
// If this is the first real user, grant them admin privileges except for guest
// users Note: the server user, @conduit:servername, is generated first
// users
// Note: the server user is generated first
if !is_guest {
if let Ok(admin_room) = services.admin.get_admin_room().await {
if services
@@ -439,7 +496,7 @@ pub(crate) async fn register_route(
if body.appservice_info.is_none()
&& !services.server.config.auto_join_rooms.is_empty()
&& (services.globals.allow_guests_auto_join_rooms() || !is_guest)
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
{
for room in &services.server.config.auto_join_rooms {
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
@@ -463,7 +520,7 @@ pub(crate) async fn register_route(
}
if let Some(room_server_name) = room.server_name() {
if let Err(e) = join_room_by_id_helper(
match join_room_by_id_helper(
&services,
&user_id,
&room_id,
@@ -475,11 +532,16 @@ pub(crate) async fn register_route(
.boxed()
.await
{
// don't return this error so we don't fail registrations
error!("Failed to automatically join room {room} for user {user_id}: {e}");
} else {
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}"
);
},
| _ => {
info!("Automatically joined room {room} for user {user_id}");
},
}
}
}
}
@@ -521,8 +583,8 @@ pub(crate) async fn change_password_route(
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))?;
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let sender_device = body.sender_device();
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
@@ -532,26 +594,32 @@ pub(crate) async fn change_password_route(
auth_error: None,
};
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json);
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
services
@@ -563,9 +631,29 @@ pub(crate) async fn change_password_route(
services
.users
.all_device_ids(sender_user)
.ready_filter(|id| id != sender_device)
.ready_filter(|id| *id != sender_device)
.for_each(|id| services.users.remove_device(sender_user, id))
.await;
// Remove all pushers except the ones associated with this session
services
.pusher
.get_pushkeys(sender_user)
.map(ToOwned::to_owned)
.broad_filter_map(|pushkey| async move {
services
.pusher
.get_pusher_device(&pushkey)
.await
.ok()
.filter(|pusher_device| pusher_device != sender_device)
.is_some()
.then_some(pushkey)
})
.for_each(|pushkey| async move {
services.pusher.delete_pusher(sender_user, &pushkey).await;
})
.await;
}
info!("User {sender_user} changed their password.");
@@ -625,8 +713,8 @@ pub(crate) async fn deactivate_route(
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))?;
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let sender_device = body.sender_device();
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
@@ -636,25 +724,31 @@ pub(crate) async fn deactivate_route(
auth_error: None,
};
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json);
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
// Remove profile pictures and display name
@@ -711,10 +805,7 @@ pub(crate) async fn third_party_route(
pub(crate) async fn request_3pid_management_token_via_email_route(
_body: Ruma<request_3pid_management_token_via_email::v3::Request>,
) -> Result<request_3pid_management_token_via_email::v3::Response> {
Err(Error::BadRequest(
ErrorKind::ThreepidDenied,
"Third party identifier is not allowed",
))
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
}
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
@@ -727,10 +818,7 @@ pub(crate) async fn request_3pid_management_token_via_email_route(
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
Err(Error::BadRequest(
ErrorKind::ThreepidDenied,
"Third party identifier is not allowed",
))
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
}
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
@@ -744,10 +832,7 @@ 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.registration_token.clone() else {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Server does not allow token registration.",
));
return Err!(Request(Forbidden("Server does not allow token registration")));
};
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })
@@ -809,7 +894,7 @@ pub async fn full_user_deactivate(
power_levels_content.users.remove(user_id);
// ignore errors so deactivation doesn't fail
if let Err(e) = services
match services
.rooms
.timeline
.build_and_append_pdu(
@@ -820,9 +905,12 @@ pub async fn full_user_deactivate(
)
.await
{
warn!(%room_id, %user_id, "Failed to demote user's own power level: {e}");
} else {
info!("Demoted {user_id} in {room_id} as part of account deactivation");
| Err(e) => {
warn!(%room_id, %user_id, "Failed to demote user's own power level: {e}");
},
| _ => {
info!("Demoted {user_id} in {room_id} as part of account deactivation");
},
}
}
}

View File

@@ -1,6 +1,8 @@
use axum::extract::State;
use conduwuit::{err, Err};
use conduwuit::{Err, Result, err};
use conduwuit_service::Services;
use ruma::{
RoomId, UserId,
api::client::config::{
get_global_account_data, get_room_account_data, set_global_account_data,
set_room_account_data,
@@ -10,12 +12,11 @@
GlobalAccountDataEventType, RoomAccountDataEventType,
},
serde::Raw,
RoomId, UserId,
};
use serde::Deserialize;
use serde_json::{json, value::RawValue as RawJsonValue};
use crate::{service::Services, Result, Ruma};
use crate::Ruma;
/// # `PUT /_matrix/client/r0/user/{userId}/account_data/{type}`
///

View File

@@ -1,12 +1,12 @@
use axum::extract::State;
use conduwuit::{debug, Err, Result};
use conduwuit::{Err, Result, debug};
use conduwuit_service::Services;
use futures::StreamExt;
use rand::seq::SliceRandom;
use ruma::{
api::client::alias::{create_alias, delete_alias, get_alias},
OwnedServerName, RoomAliasId, RoomId,
api::client::alias::{create_alias, delete_alias, get_alias},
};
use service::Services;
use crate::Ruma;
@@ -128,18 +128,26 @@ async fn room_available_servers(
// insert our server as the very first choice if in list, else check if we can
// prefer the room alias server first
if let Some(server_index) = servers
match servers
.iter()
.position(|server_name| services.globals.server_is_ours(server_name))
{
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.swap_remove(alias_server_index);
servers.insert(0, room_alias.server_name().into());
| Some(server_index) => {
servers.swap_remove(server_index);
servers.insert(0, services.globals.server_name().to_owned());
},
| _ => {
match servers
.iter()
.position(|server| server == room_alias.server_name())
{
| Some(alias_server_index) => {
servers.swap_remove(alias_server_index);
servers.insert(0, room_alias.server_name().into());
},
| _ => {},
}
},
}
servers

View File

@@ -1,5 +1,5 @@
use axum::extract::State;
use conduwuit::{err, Err, Result};
use conduwuit::{Err, Result, err};
use ruma::api::{appservice::ping, client::appservice::request_ping};
use crate::Ruma;
@@ -22,7 +22,13 @@ pub(crate) async fn appservice_ping(
)));
}
if appservice_info.registration.url.is_none() {
if appservice_info.registration.url.is_none()
|| appservice_info
.registration
.url
.as_ref()
.is_some_and(|url| url.is_empty() || url == "null")
{
return Err!(Request(UrlNotSet(
"Appservice does not have a URL set, there is nothing to ping."
)));

View File

@@ -1,6 +1,9 @@
use std::cmp::Ordering;
use axum::extract::State;
use conduwuit::{err, Err};
use conduwuit::{Err, Result, err};
use ruma::{
UInt,
api::client::backup::{
add_backup_keys, add_backup_keys_for_room, add_backup_keys_for_session,
create_backup_version, delete_backup_keys, delete_backup_keys_for_room,
@@ -8,10 +11,9 @@
get_backup_keys_for_room, get_backup_keys_for_session, get_latest_backup_info,
update_backup_version,
},
UInt,
};
use crate::{Result, Ruma};
use crate::Ruma;
/// # `POST /_matrix/client/r0/room_keys/version`
///
@@ -232,16 +234,77 @@ pub(crate) async fn add_backup_keys_for_session_route(
)));
}
services
// Check if we already have a better key
let mut ok_to_replace = true;
if let Some(old_key) = &services
.key_backups
.add_key(
body.sender_user(),
&body.version,
&body.room_id,
&body.session_id,
&body.session_data,
)
.await?;
.get_session(body.sender_user(), &body.version, &body.room_id, &body.session_id)
.await
.ok()
{
let old_is_verified = old_key
.get_field::<bool>("is_verified")?
.unwrap_or_default();
let new_is_verified = body
.session_data
.get_field::<bool>("is_verified")?
.ok_or_else(|| err!(Request(BadJson("`is_verified` field should exist"))))?;
// Prefer key that `is_verified`
if old_is_verified != new_is_verified {
if old_is_verified {
ok_to_replace = false;
}
} else {
// If both have same `is_verified`, prefer the one with lower
// `first_message_index`
let old_first_message_index = old_key
.get_field::<UInt>("first_message_index")?
.unwrap_or(UInt::MAX);
let new_first_message_index = body
.session_data
.get_field::<UInt>("first_message_index")?
.ok_or_else(|| {
err!(Request(BadJson("`first_message_index` field should exist")))
})?;
ok_to_replace = match new_first_message_index.cmp(&old_first_message_index) {
| Ordering::Less => true,
| Ordering::Greater => false,
| Ordering::Equal => {
// If both have same `first_message_index`, prefer the one with lower
// `forwarded_count`
let old_forwarded_count = old_key
.get_field::<UInt>("forwarded_count")?
.unwrap_or(UInt::MAX);
let new_forwarded_count = body
.session_data
.get_field::<UInt>("forwarded_count")?
.ok_or_else(|| {
err!(Request(BadJson("`forwarded_count` field should exist")))
})?;
new_forwarded_count < old_forwarded_count
},
};
}
}
if ok_to_replace {
services
.key_backups
.add_key(
body.sender_user(),
&body.version,
&body.room_id,
&body.session_id,
&body.session_data,
)
.await?;
}
Ok(add_backup_keys_for_session::v3::Response {
count: services

View File

@@ -3,11 +3,11 @@
use axum::extract::State;
use conduwuit::{Result, Server};
use ruma::{
RoomVersionId,
api::client::discovery::get_capabilities::{
self, Capabilities, GetLoginTokenCapability, RoomVersionStability,
RoomVersionsCapability, ThirdPartyIdChangesCapability,
},
RoomVersionId,
};
use serde_json::json;
@@ -42,5 +42,12 @@ pub(crate) async fn get_capabilities_route(
.set("uk.tcpip.msc4133.profile_fields", json!({"enabled": true}))
.expect("this is valid JSON we created");
capabilities
.set(
"org.matrix.msc4267.forget_forced_upon_leave",
json!({"enabled": services.config.forget_forced_upon_leave}),
)
.expect("valid JSON we created");
Ok(get_capabilities::v3::Response { capabilities })
}

View File

@@ -1,23 +1,24 @@
use axum::extract::State;
use conduwuit::{
at, err, ref_at,
Err, Result, at, debug_warn, err,
matrix::pdu::PduEvent,
ref_at,
utils::{
IterStream,
future::TryExtExt,
stream::{BroadbandExt, ReadyExt, TryIgnore, WidebandExt},
IterStream,
},
Err, PduEvent, Result,
};
use conduwuit_service::rooms::{lazy_loading, lazy_loading::Options, short::ShortStateKey};
use futures::{
future::{join, join3, try_join3, OptionFuture},
FutureExt, StreamExt, TryFutureExt, TryStreamExt,
future::{OptionFuture, join, join3, try_join3},
};
use ruma::{api::client::context::get_context, events::StateEventType, OwnedEventId, UserId};
use service::rooms::{lazy_loading, lazy_loading::Options, short::ShortStateKey};
use ruma::{OwnedEventId, UserId, api::client::context::get_context, events::StateEventType};
use crate::{
client::message::{event_filter, ignored_filter, lazy_loading_witness, visibility_filter},
Ruma,
client::message::{event_filter, ignored_filter, lazy_loading_witness, visibility_filter},
};
const LIMIT_MAX: usize = 100;
@@ -36,8 +37,13 @@ pub(crate) async fn get_context_route(
let sender = body.sender();
let (sender_user, sender_device) = sender;
let room_id = &body.room_id;
let event_id = &body.event_id;
let filter = &body.filter;
if !services.rooms.metadata.exists(room_id).await {
return Err!(Request(Forbidden("Room does not exist to this server")));
}
// Use limit or else 10, with maximum 100
let limit: usize = body
.limit
@@ -48,29 +54,30 @@ pub(crate) async fn get_context_route(
let base_id = services
.rooms
.timeline
.get_pdu_id(&body.event_id)
.get_pdu_id(event_id)
.map_err(|_| err!(Request(NotFound("Event not found."))));
let base_pdu = services
.rooms
.timeline
.get_pdu(&body.event_id)
.get_pdu(event_id)
.map_err(|_| err!(Request(NotFound("Base event not found."))));
let visible = services
.rooms
.state_accessor
.user_can_see_event(sender_user, &body.room_id, &body.event_id)
.user_can_see_event(sender_user, room_id, event_id)
.map(Ok);
let (base_id, base_pdu, visible) = try_join3(base_id, base_pdu, visible).await?;
if base_pdu.room_id != body.room_id || base_pdu.event_id != body.event_id {
if base_pdu.room_id != *room_id || base_pdu.event_id != *event_id {
return Err!(Request(NotFound("Base event not found.")));
}
if !visible {
return Err!(Request(Forbidden("You don't have permission to view this event.")));
debug_warn!(req_evt = ?event_id, ?base_id, ?room_id, "Event requested by {sender_user} but is not allowed to see it, returning 404");
return Err!(Request(NotFound("Event not found.")));
}
let base_count = base_id.pdu_count();
@@ -100,7 +107,7 @@ pub(crate) async fn get_context_route(
.collect();
let (base_event, events_before, events_after): (_, Vec<_>, Vec<_>) =
join3(base_event, events_before, events_after).await;
join3(base_event, events_before, events_after).boxed().await;
let lazy_loading_context = lazy_loading::Context {
user_id: sender_user,
@@ -177,7 +184,7 @@ pub(crate) async fn get_context_route(
.await;
Ok(get_context::v3::Response {
event: base_event.map(at!(1)).as_ref().map(PduEvent::to_room_event),
event: base_event.map(at!(1)).map(PduEvent::into_room_event),
start: events_before
.last()
@@ -196,13 +203,13 @@ pub(crate) async fn get_context_route(
events_before: events_before
.into_iter()
.map(at!(1))
.map(|pdu| pdu.to_room_event())
.map(PduEvent::into_room_event)
.collect(),
events_after: events_after
.into_iter()
.map(at!(1))
.map(|pdu| pdu.to_room_event())
.map(PduEvent::into_room_event)
.collect(),
state,

View File

@@ -1,18 +1,18 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{err, Err};
use conduwuit::{Err, Error, Result, debug, err, utils};
use futures::StreamExt;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedDeviceId,
api::client::{
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
error::ErrorKind,
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
MilliSecondsSinceUnixEpoch,
};
use super::SESSION_ID_LENGTH;
use crate::{utils, Error, Result, Ruma};
use crate::{Ruma, client::DEVICE_ID_LENGTH};
/// # `GET /_matrix/client/r0/devices`
///
@@ -59,26 +59,58 @@ pub(crate) async fn update_device_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<update_device::v3::Request>,
) -> Result<update_device::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_user = body.sender_user();
let appservice = body.appservice_info.as_ref();
let mut device = services
match services
.users
.get_device_metadata(sender_user, &body.device_id)
.await
.map_err(|_| err!(Request(NotFound("Device not found."))))?;
{
| Ok(mut device) => {
device.display_name.clone_from(&body.display_name);
device.last_seen_ip.clone_from(&Some(client.to_string()));
device
.last_seen_ts
.clone_from(&Some(MilliSecondsSinceUnixEpoch::now()));
device.display_name.clone_from(&body.display_name);
device.last_seen_ip.clone_from(&Some(client.to_string()));
device
.last_seen_ts
.clone_from(&Some(MilliSecondsSinceUnixEpoch::now()));
services
.users
.update_device_metadata(sender_user, &body.device_id, &device)
.await?;
services
.users
.update_device_metadata(sender_user, &body.device_id, &device)
.await?;
Ok(update_device::v3::Response {})
},
| Err(_) => {
let Some(appservice) = appservice else {
return Err!(Request(NotFound("Device not found.")));
};
if !appservice.registration.device_management {
return Err!(Request(NotFound("Device not found.")));
}
Ok(update_device::v3::Response {})
debug!(
"Creating new device for {sender_user} from appservice {} as MSC4190 is enabled \
and device ID does not exist",
appservice.registration.id
);
let device_id = OwnedDeviceId::from(utils::random_string(DEVICE_ID_LENGTH));
services
.users
.create_device(
sender_user,
&device_id,
&appservice.registration.as_token,
None,
Some(client.to_string()),
)
.await?;
return Ok(update_device::v3::Response {});
},
}
}
/// # `DELETE /_matrix/client/r0/devices/{deviceId}`
@@ -95,8 +127,21 @@ pub(crate) async fn delete_device_route(
State(services): State<crate::State>,
body: Ruma<delete_device::v3::Request>,
) -> Result<delete_device::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
let (sender_user, sender_device) = body.sender();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
);
services
.users
.remove_device(sender_user, &body.device_id)
.await;
return Ok(delete_device::v3::Response {});
}
// UIAA
let mut uiaainfo = UiaaInfo {
@@ -107,25 +152,31 @@ pub(crate) async fn delete_device_route(
auth_error: None,
};
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err!(Uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json);
if !worked {
return Err!(Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
return Err!(Uiaa(uiaainfo));
} else {
return Err!(Request(NotJson("Not json.")));
return Err!(Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("Not json.")));
},
},
}
services
@@ -136,11 +187,12 @@ pub(crate) async fn delete_device_route(
Ok(delete_device::v3::Response {})
}
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
/// # `POST /_matrix/client/v3/delete_devices`
///
/// Deletes the given device.
/// Deletes the given list of devices.
///
/// - Requires UIAA to verify user password
/// - Requires UIAA to verify user password unless from an appservice with
/// MSC4190 enabled.
///
/// For each device:
/// - Invalidates access token
@@ -152,8 +204,20 @@ pub(crate) async fn delete_devices_route(
State(services): State<crate::State>,
body: Ruma<delete_devices::v3::Request>,
) -> Result<delete_devices::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
let (sender_user, sender_device) = body.sender();
let appservice = body.appservice_info.as_ref();
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
);
for device_id in &body.devices {
services.users.remove_device(sender_user, device_id).await;
}
return Ok(delete_devices::v3::Response {});
}
// UIAA
let mut uiaainfo = UiaaInfo {
@@ -164,25 +228,31 @@ pub(crate) async fn delete_devices_route(
auth_error: None,
};
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json);
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
},
},
}
for device_id in &body.devices {

View File

@@ -1,30 +1,41 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{info, warn, Err, Error, Result};
use futures::{StreamExt, TryFutureExt};
use conduwuit::{
Err, Result, err, info,
utils::{
TryFutureExtExt,
math::Expected,
result::FlatOk,
stream::{ReadyExt, WidebandExt},
},
};
use conduwuit_service::Services;
use futures::{
FutureExt, StreamExt, TryFutureExt,
future::{join, join4, join5},
};
use ruma::{
OwnedRoomId, RoomId, ServerName, UInt, UserId,
api::{
client::{
directory::{
get_public_rooms, get_public_rooms_filtered, get_room_visibility,
set_room_visibility,
},
error::ErrorKind,
room,
},
federation,
},
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork},
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
events::{
StateEventType,
room::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
StateEventType,
},
uint, OwnedRoomId, RoomId, ServerName, UInt, UserId,
uint,
};
use service::Services;
use crate::Ruma;
@@ -41,10 +52,13 @@ pub(crate) async fn get_public_rooms_filtered_route(
) -> Result<get_public_rooms_filtered::v3::Response> {
if let Some(server) = &body.server {
if services
.server
.config
.forbidden_remote_room_directory_server_names
.contains(server)
.is_match(server.host())
|| services
.config
.forbidden_remote_server_names
.is_match(server.host())
{
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
@@ -60,11 +74,7 @@ pub(crate) async fn get_public_rooms_filtered_route(
)
.await
.map_err(|e| {
warn!(?body.server, "Failed to return /publicRooms: {e}");
Error::BadRequest(
ErrorKind::Unknown,
"Failed to return the requested server's public room list.",
)
err!(Request(Unknown(warn!(?body.server, "Failed to return /publicRooms: {e}"))))
})?;
Ok(response)
@@ -83,10 +93,13 @@ pub(crate) async fn get_public_rooms_route(
) -> Result<get_public_rooms::v3::Response> {
if let Some(server) = &body.server {
if services
.server
.config
.forbidden_remote_room_directory_server_names
.contains(server)
.is_match(server.host())
|| services
.config
.forbidden_remote_server_names
.is_match(server.host())
{
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
@@ -102,11 +115,7 @@ pub(crate) async fn get_public_rooms_route(
)
.await
.map_err(|e| {
warn!(?body.server, "Failed to return /publicRooms: {e}");
Error::BadRequest(
ErrorKind::Unknown,
"Failed to return the requested server's public room list.",
)
err!(Request(Unknown(warn!(?body.server, "Failed to return /publicRooms: {e}"))))
})?;
Ok(get_public_rooms::v3::Response {
@@ -126,11 +135,11 @@ pub(crate) async fn set_room_visibility_route(
InsecureClientIp(client): 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");
let sender_user = body.sender_user();
if !services.rooms.metadata.exists(&body.room_id).await {
// Return 404 if the room doesn't exist
return Err(Error::BadRequest(ErrorKind::NotFound, "Room not found"));
return Err!(Request(NotFound("Room not found")));
}
if services
@@ -144,10 +153,7 @@ pub(crate) async fn set_room_visibility_route(
}
if !user_can_publish_room(&services, sender_user, &body.room_id).await? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"User is not allowed to publish this room",
));
return Err!(Request(Forbidden("User is not allowed to publish this room")));
}
match &body.visibility {
@@ -173,10 +179,9 @@ pub(crate) async fn set_room_visibility_route(
.await;
}
return Err(Error::BadRequest(
ErrorKind::forbidden(),
return Err!(Request(Forbidden(
"Publishing rooms to the room directory is not allowed",
));
)));
}
services.rooms.directory.set_public(&body.room_id);
@@ -194,10 +199,7 @@ pub(crate) async fn set_room_visibility_route(
},
| room::Visibility::Private => services.rooms.directory.set_not_public(&body.room_id),
| _ => {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Room visibility type is not supported.",
));
return Err!(Request(InvalidParam("Room visibility type is not supported.",)));
},
}
@@ -213,7 +215,7 @@ pub(crate) async fn get_room_visibility_route(
) -> Result<get_room_visibility::v3::Response> {
if !services.rooms.metadata.exists(&body.room_id).await {
// Return 404 if the room doesn't exist
return Err(Error::BadRequest(ErrorKind::NotFound, "Room not found"));
return Err!(Request(NotFound("Room not found")));
}
Ok(get_room_visibility::v3::Response {
@@ -261,22 +263,23 @@ pub(crate) async fn get_public_rooms_filtered_helper(
}
// Use limit or else 10, with maximum 100
let limit = limit.map_or(10, u64::from);
let mut num_since: u64 = 0;
let limit: usize = limit.map_or(10_u64, u64::from).try_into()?;
let mut num_since: usize = 0;
if let Some(s) = &since {
let mut characters = s.chars();
let backwards = match characters.next() {
| Some('n') => false,
| Some('p') => true,
| _ =>
return Err(Error::BadRequest(ErrorKind::InvalidParam, "Invalid `since` token")),
| _ => {
return Err!(Request(InvalidParam("Invalid `since` token")));
},
};
num_since = characters
.collect::<String>()
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `since` token."))?;
.map_err(|_| err!(Request(InvalidParam("Invalid `since` token."))))?;
if backwards {
num_since = num_since.saturating_sub(limit);
@@ -288,8 +291,12 @@ pub(crate) async fn get_public_rooms_filtered_helper(
.directory
.public_rooms()
.map(ToOwned::to_owned)
.then(|room_id| public_rooms_chunk(services, room_id))
.filter_map(|chunk| async move {
.wide_then(|room_id| public_rooms_chunk(services, room_id))
.ready_filter_map(|chunk| {
if !filter.room_types.is_empty() && !filter.room_types.contains(&RoomTypeFilter::from(chunk.room_type.clone())) {
return None;
}
if let Some(query) = filter.generic_search_term.as_ref().map(|q| q.to_lowercase()) {
if let Some(name) = &chunk.name {
if name.as_str().to_lowercase().contains(&query) {
@@ -321,40 +328,24 @@ pub(crate) async fn get_public_rooms_filtered_helper(
all_rooms.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members));
let total_room_count_estimate = UInt::try_from(all_rooms.len()).unwrap_or_else(|_| uint!(0));
let total_room_count_estimate = UInt::try_from(all_rooms.len())
.unwrap_or_else(|_| uint!(0))
.into();
let chunk: Vec<_> = all_rooms
.into_iter()
.skip(
num_since
.try_into()
.expect("num_since should not be this high"),
)
.take(limit.try_into().expect("limit should not be this high"))
.collect();
let chunk: Vec<_> = all_rooms.into_iter().skip(num_since).take(limit).collect();
let prev_batch = if num_since == 0 {
None
} else {
Some(format!("p{num_since}"))
};
let prev_batch = num_since.ne(&0).then_some(format!("p{num_since}"));
let next_batch = if chunk.len() < limit.try_into().unwrap() {
None
} else {
Some(format!(
"n{}",
num_since
.checked_add(limit)
.expect("num_since and limit should not be that large")
))
};
let next_batch = chunk
.len()
.ge(&limit)
.then_some(format!("n{}", num_since.expected_add(limit)));
Ok(get_public_rooms_filtered::v3::Response {
chunk,
prev_batch,
next_batch,
total_room_count_estimate: Some(total_room_count_estimate),
total_room_count_estimate,
})
}
@@ -365,88 +356,88 @@ async fn user_can_publish_room(
user_id: &UserId,
room_id: &RoomId,
) -> Result<bool> {
if let Ok(event) = services
match services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
.await
{
serde_json::from_str(event.content.get())
.map_err(|_| Error::bad_database("Invalid event content for m.room.power_levels"))
| Ok(event) => serde_json::from_str(event.content.get())
.map_err(|_| err!(Database("Invalid event content for m.room.power_levels")))
.map(|content: RoomPowerLevelsEventContent| {
RoomPowerLevels::from(content)
.user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
})
} else if let Ok(event) = services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await
{
Ok(event.sender == user_id)
} else {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"User is not allowed to publish this room",
));
}),
| _ => {
match services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await
{
| Ok(event) => Ok(event.sender == user_id),
| _ => Err!(Request(Forbidden("User is not allowed to publish this room"))),
}
},
}
}
async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> PublicRoomsChunk {
let name = services.rooms.state_accessor.get_name(&room_id).ok();
let room_type = services.rooms.state_accessor.get_room_type(&room_id).ok();
let canonical_alias = services
.rooms
.state_accessor
.get_canonical_alias(&room_id)
.ok();
let avatar_url = services.rooms.state_accessor.get_avatar(&room_id);
let topic = services.rooms.state_accessor.get_room_topic(&room_id).ok();
let world_readable = services.rooms.state_accessor.is_world_readable(&room_id);
let join_rule = services
.rooms
.state_accessor
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
| JoinRule::Public => PublicRoomJoinRule::Public,
| JoinRule::Knock => "knock".into(),
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
| _ => "invite".into(),
});
let guest_can_join = services.rooms.state_accessor.guest_can_join(&room_id);
let num_joined_members = services.rooms.state_cache.room_joined_count(&room_id);
let (
(avatar_url, canonical_alias, guest_can_join, join_rule, name),
(num_joined_members, room_type, topic, world_readable),
) = join(
join5(avatar_url, canonical_alias, guest_can_join, join_rule, name),
join4(num_joined_members, room_type, topic, world_readable),
)
.boxed()
.await;
PublicRoomsChunk {
canonical_alias: services
.rooms
.state_accessor
.get_canonical_alias(&room_id)
.await
.ok(),
name: services.rooms.state_accessor.get_name(&room_id).await.ok(),
num_joined_members: services
.rooms
.state_cache
.room_joined_count(&room_id)
.await
.unwrap_or(0)
.try_into()
.expect("joined count overflows ruma UInt"),
topic: services
.rooms
.state_accessor
.get_room_topic(&room_id)
.await
.ok(),
world_readable: services
.rooms
.state_accessor
.is_world_readable(&room_id)
.await,
guest_can_join: services.rooms.state_accessor.guest_can_join(&room_id).await,
avatar_url: services
.rooms
.state_accessor
.get_avatar(&room_id)
.await
.into_option()
.unwrap_or_default()
.url,
join_rule: services
.rooms
.state_accessor
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
| JoinRule::Public => PublicRoomJoinRule::Public,
| JoinRule::Knock => "knock".into(),
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
| _ => "invite".into(),
})
.await
.unwrap_or_default(),
room_type: services
.rooms
.state_accessor
.get_room_type(&room_id)
.await
.ok(),
avatar_url: avatar_url.into_option().unwrap_or_default().url,
canonical_alias,
guest_can_join,
join_rule: join_rule.unwrap_or_default(),
name,
num_joined_members: num_joined_members
.map(TryInto::try_into)
.map(Result::ok)
.flat_ok()
.unwrap_or_else(|| uint!(0)),
room_id,
room_type,
topic,
world_readable,
}
}

View File

@@ -1,8 +1,8 @@
use axum::extract::State;
use conduwuit::err;
use conduwuit::{Result, err};
use ruma::api::client::filter::{create_filter, get_filter};
use crate::{Result, Ruma};
use crate::Ruma;
/// # `GET /_matrix/client/r0/user/{userId}/filter/{filterId}`
///

View File

@@ -1,30 +1,30 @@
use std::collections::{BTreeMap, HashMap, HashSet};
use axum::extract::State;
use conduwuit::{err, utils, Error, Result};
use futures::{stream::FuturesUnordered, StreamExt};
use conduwuit::{Err, Error, Result, debug, debug_warn, err, result::NotFound, utils};
use conduwuit_service::{Services, users::parse_master_key};
use futures::{StreamExt, stream::FuturesUnordered};
use ruma::{
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
api::{
client::{
error::ErrorKind,
keys::{
claim_keys, get_key_changes, get_keys, upload_keys, upload_signatures,
claim_keys, get_key_changes, get_keys, upload_keys,
upload_signatures::{self},
upload_signing_keys,
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
federation,
},
encryption::CrossSigningKey,
serde::Raw,
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
};
use serde_json::json;
use super::SESSION_ID_LENGTH;
use crate::{
service::{users::parse_master_key, Services},
Ruma,
};
use crate::Ruma;
/// # `POST /_matrix/client/r0/keys/upload`
///
@@ -40,6 +40,20 @@ pub(crate) async fn upload_keys_route(
let (sender_user, sender_device) = body.sender();
for (key_id, one_time_key) in &body.one_time_keys {
if one_time_key
.deserialize()
.inspect_err(|e| {
debug_warn!(
?key_id,
?one_time_key,
"Invalid one time key JSON submitted by client, skipping: {e}"
);
})
.is_err()
{
continue;
}
services
.users
.add_one_time_key(sender_user, sender_device, key_id, one_time_key)
@@ -47,14 +61,44 @@ pub(crate) async fn upload_keys_route(
}
if let Some(device_keys) = &body.device_keys {
// TODO: merge this and the existing event?
// This check is needed to assure that signatures are kept
if services
let deser_device_keys = device_keys.deserialize().map_err(|e| {
err!(Request(BadJson(debug_warn!(
?device_keys,
"Invalid device keys JSON uploaded by client: {e}"
))))
})?;
if deser_device_keys.user_id != sender_user {
return Err!(Request(Unknown(
"User ID in keys uploaded does not match your own user ID"
)));
}
if deser_device_keys.device_id != sender_device {
return Err!(Request(Unknown(
"Device ID in keys uploaded does not match your own device ID"
)));
}
if let Ok(existing_keys) = services
.users
.get_device_keys(sender_user, sender_device)
.await
.is_err()
{
if existing_keys.json().get() == device_keys.json().get() {
debug!(
?sender_user,
?sender_device,
?device_keys,
"Ignoring user uploaded keys as they are an exact copy already in the \
database"
);
} else {
services
.users
.add_device_keys(sender_user, sender_device, device_keys)
.await;
}
} else {
services
.users
.add_device_keys(sender_user, sender_device, device_keys)
@@ -125,93 +169,198 @@ pub(crate) async fn upload_signing_keys_route(
auth_error: None,
};
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
match check_for_new_keys(
services,
sender_user,
body.self_signing_key.as_ref(),
body.user_signing_key.as_ref(),
body.master_key.as_ref(),
)
.await
.inspect_err(|e| debug!(?e))
{
| Ok(exists) => {
if let Some(result) = exists {
// No-op, they tried to reupload the same set of keys
// (lost connection for example)
return Ok(result);
}
debug!(
"Skipping UIA in accordance with MSC3967, the user didn't have any existing keys"
);
// Some of the keys weren't found, so we let them upload
},
| _ => {
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json);
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
},
| _ => match body.json_body {
| Some(json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json);
return Err(Error::Uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
},
},
}
},
}
if let Some(master_key) = &body.master_key {
services
.users
.add_cross_signing_keys(
sender_user,
master_key,
&body.self_signing_key,
&body.user_signing_key,
true, // notify so that other users see the new keys
)
.await?;
}
services
.users
.add_cross_signing_keys(
sender_user,
&body.master_key,
&body.self_signing_key,
&body.user_signing_key,
true, // notify so that other users see the new keys
)
.await?;
Ok(upload_signing_keys::v3::Response {})
}
async fn check_for_new_keys(
services: crate::State,
user_id: &UserId,
self_signing_key: Option<&Raw<CrossSigningKey>>,
user_signing_key: Option<&Raw<CrossSigningKey>>,
master_signing_key: Option<&Raw<CrossSigningKey>>,
) -> Result<Option<upload_signing_keys::v3::Response>> {
debug!("checking for existing keys");
let mut empty = false;
if let Some(master_signing_key) = master_signing_key {
let (key, value) = parse_master_key(user_id, master_signing_key)?;
let result = services
.users
.get_master_key(None, user_id, &|_| true)
.await;
if result.is_not_found() {
empty = true;
} else {
let existing_master_key = result?;
let (existing_key, existing_value) = parse_master_key(user_id, &existing_master_key)?;
if existing_key != key || existing_value != value {
return Err!(Request(Forbidden(
"Tried to change an existing master key, UIA required"
)));
}
}
}
if let Some(user_signing_key) = user_signing_key {
let key = services.users.get_user_signing_key(user_id).await;
if key.is_not_found() && !empty {
return Err!(Request(Forbidden(
"Tried to update an existing user signing key, UIA required"
)));
}
if !key.is_not_found() {
let existing_signing_key = key?.deserialize()?;
if existing_signing_key != user_signing_key.deserialize()? {
return Err!(Request(Forbidden(
"Tried to change an existing user signing key, UIA required"
)));
}
}
}
if let Some(self_signing_key) = self_signing_key {
let key = services
.users
.get_self_signing_key(None, user_id, &|_| true)
.await;
if key.is_not_found() && !empty {
debug!(?key);
return Err!(Request(Forbidden(
"Tried to add a new signing key independently from the master key"
)));
}
if !key.is_not_found() {
let existing_signing_key = key?.deserialize()?;
if existing_signing_key != self_signing_key.deserialize()? {
return Err!(Request(Forbidden(
"Tried to update an existing self signing key, UIA required"
)));
}
}
}
if empty {
return Ok(None);
}
Ok(Some(upload_signing_keys::v3::Response {}))
}
/// # `POST /_matrix/client/r0/keys/signatures/upload`
///
/// Uploads end-to-end key signatures from the sender user.
///
/// TODO: clean this timo-code up more and integrate failures. tried to improve
/// it a bit to stop exploding the entire request on bad sigs, but needs way
/// more work.
pub(crate) async fn upload_signatures_route(
State(services): State<crate::State>,
body: Ruma<upload_signatures::v3::Request>,
) -> Result<upload_signatures::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if body.signed_keys.is_empty() {
debug!("Empty signed_keys sent in key signature upload");
return Ok(upload_signatures::v3::Response::new());
}
let sender_user = body.sender_user();
for (user_id, keys) in &body.signed_keys {
for (key_id, key) in keys {
let key = serde_json::to_value(key)
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid key JSON"))?;
let Ok(key) = serde_json::to_value(key)
.inspect_err(|e| debug_warn!(?key_id, "Invalid \"key\" JSON: {e}"))
else {
continue;
};
for signature in key
.get("signatures")
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Missing signatures field."))?
.get(sender_user.to_string())
.ok_or(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid user in signatures field.",
))?
.as_object()
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Invalid signature."))?
.clone()
{
// Signature validation?
let signature = (
signature.0,
signature
.1
.as_str()
.ok_or(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid signature value.",
))?
.to_owned(),
);
let Some(signatures) = key.get("signatures") else {
continue;
};
services
let Some(sender_user_val) = signatures.get(sender_user.to_string()) else {
continue;
};
let Some(sender_user_object) = sender_user_val.as_object() else {
continue;
};
for (signature, val) in sender_user_object.clone() {
let Some(val) = val.as_str().map(ToOwned::to_owned) else {
continue;
};
let signature = (signature, val);
if let Err(_e) = services
.users
.sign_key(user_id, key_id, signature, sender_user)
.await?;
.await
.inspect_err(|e| debug_warn!("{e}"))
{
continue;
}
}
}
}
Ok(upload_signatures::v3::Response {
failures: BTreeMap::new(), // TODO: integrate
})
Ok(upload_signatures::v3::Response { failures: BTreeMap::new() })
}
/// # `POST /_matrix/client/r0/keys/changes`
@@ -385,35 +534,40 @@ pub(crate) async fn get_keys_helper<F>(
.collect();
while let Some((server, response)) = futures.next().await {
if let Ok(response) = response {
for (user, master_key) in response.master_keys {
let (master_key_id, mut master_key) = parse_master_key(&user, &master_key)?;
match response {
| Ok(response) => {
for (user, master_key) in response.master_keys {
let (master_key_id, mut master_key) = parse_master_key(&user, &master_key)?;
if let Ok(our_master_key) = services
.users
.get_key(&master_key_id, sender_user, &user, &allowed_signatures)
.await
{
let (_, mut our_master_key) = parse_master_key(&user, &our_master_key)?;
master_key.signatures.append(&mut our_master_key.signatures);
if let Ok(our_master_key) = services
.users
.get_key(&master_key_id, sender_user, &user, &allowed_signatures)
.await
{
let (_, mut our_master_key) = parse_master_key(&user, &our_master_key)?;
master_key.signatures.append(&mut our_master_key.signatures);
}
let json = serde_json::to_value(master_key).expect("to_value always works");
let raw = serde_json::from_value(json).expect("Raw::from_value always works");
services
.users
.add_cross_signing_keys(
&user, &raw, &None, &None,
false, /* Dont notify. A notification would trigger another key
* request resulting in an endless loop */
)
.await?;
if let Some(raw) = raw {
master_keys.insert(user.clone(), raw);
}
}
let json = serde_json::to_value(master_key).expect("to_value always works");
let raw = serde_json::from_value(json).expect("Raw::from_value always works");
services
.users
.add_cross_signing_keys(
&user, &raw, &None, &None,
false, /* Dont notify. A notification would trigger another key request
* resulting in an endless loop */
)
.await?;
master_keys.insert(user.clone(), raw);
}
self_signing_keys.extend(response.self_signing_keys);
device_keys.extend(response.device_keys);
} else {
failures.insert(server.to_string(), json!({}));
self_signing_keys.extend(response.self_signing_keys);
device_keys.extend(response.device_keys);
},
| _ => {
failures.insert(server.to_string(), json!({}));
},
}
}

View File

@@ -3,16 +3,16 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
err,
Err, Result, err,
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
Err, Result,
};
use conduwuit_service::{
media::{Dim, FileMeta, CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, MXC_LENGTH},
Services,
media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta, MXC_LENGTH},
};
use reqwest::Url;
use ruma::{
Mxc, UserId,
api::client::{
authenticated_media::{
get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
@@ -20,7 +20,6 @@
},
media::create_content,
},
Mxc, UserId,
};
use crate::Ruma;

View File

@@ -3,21 +3,20 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
err,
Err, Result, err,
utils::{content_disposition::make_content_disposition, math::ruma_from_usize},
Err, Result,
};
use conduwuit_service::media::{Dim, FileMeta, CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN};
use conduwuit_service::media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta};
use reqwest::Url;
use ruma::{
Mxc,
api::client::media::{
create_content, get_content, get_content_as_filename, get_content_thumbnail,
get_media_config, get_media_preview,
},
Mxc,
};
use crate::{client::create_content_route, Ruma, RumaResponse};
use crate::{Ruma, RumaResponse, client::create_content_route};
/// # `GET /_matrix/media/v3/config`
///
@@ -142,46 +141,52 @@ pub(crate) async fn get_content_legacy_route(
media_id: &body.media_id,
};
if let Some(FileMeta {
content,
content_type,
content_disposition,
}) = services.media.get(&mxc).await?
{
let content_disposition =
make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
match services.media.get(&mxc).await? {
| Some(FileMeta {
content,
content_type,
content_disposition,
}) => {
let content_disposition = make_content_disposition(
content_disposition.as_ref(),
content_type.as_deref(),
None,
);
Ok(get_content::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
.await
.map_err(|e| {
err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
})?;
Ok(get_content::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
},
| _ =>
if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
.await
.map_err(|e| {
err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
})?;
let content_disposition = make_content_disposition(
response.content_disposition.as_ref(),
response.content_type.as_deref(),
None,
);
let content_disposition = make_content_disposition(
response.content_disposition.as_ref(),
response.content_type.as_deref(),
None,
);
Ok(get_content::v3::Response {
file: response.file,
content_type: response.content_type,
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else {
Err!(Request(NotFound("Media not found.")))
Ok(get_content::v3::Response {
file: response.file,
content_type: response.content_type,
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else {
Err!(Request(NotFound("Media not found.")))
},
}
}
@@ -227,49 +232,52 @@ pub(crate) async fn get_content_as_filename_legacy_route(
media_id: &body.media_id,
};
if let Some(FileMeta {
content,
content_type,
content_disposition,
}) = services.media.get(&mxc).await?
{
let content_disposition = make_content_disposition(
content_disposition.as_ref(),
content_type.as_deref(),
Some(&body.filename),
);
match services.media.get(&mxc).await? {
| Some(FileMeta {
content,
content_type,
content_disposition,
}) => {
let content_disposition = make_content_disposition(
content_disposition.as_ref(),
content_type.as_deref(),
Some(&body.filename),
);
Ok(get_content_as_filename::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
.await
.map_err(|e| {
err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
})?;
Ok(get_content_as_filename::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
},
| _ =>
if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_content_legacy(&mxc, body.allow_redirect, body.timeout_ms)
.await
.map_err(|e| {
err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
})?;
let content_disposition = make_content_disposition(
response.content_disposition.as_ref(),
response.content_type.as_deref(),
None,
);
let content_disposition = make_content_disposition(
response.content_disposition.as_ref(),
response.content_type.as_deref(),
None,
);
Ok(get_content_as_filename::v3::Response {
content_disposition: Some(content_disposition),
content_type: response.content_type,
file: response.file,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else {
Err!(Request(NotFound("Media not found.")))
Ok(get_content_as_filename::v3::Response {
content_disposition: Some(content_disposition),
content_type: response.content_type,
file: response.file,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else {
Err!(Request(NotFound("Media not found.")))
},
}
}
@@ -315,46 +323,52 @@ pub(crate) async fn get_content_thumbnail_legacy_route(
};
let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
if let Some(FileMeta {
content,
content_type,
content_disposition,
}) = services.media.get_thumbnail(&mxc, &dim).await?
{
let content_disposition =
make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
match services.media.get_thumbnail(&mxc, &dim).await? {
| Some(FileMeta {
content,
content_type,
content_disposition,
}) => {
let content_disposition = make_content_disposition(
content_disposition.as_ref(),
content_type.as_deref(),
None,
);
Ok(get_content_thumbnail::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
} else if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_thumbnail_legacy(&body)
.await
.map_err(|e| {
err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
})?;
Ok(get_content_thumbnail::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
},
| _ =>
if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
let response = services
.media
.fetch_remote_thumbnail_legacy(&body)
.await
.map_err(|e| {
err!(Request(NotFound(debug_warn!(%mxc, "Fetching media failed: {e:?}"))))
})?;
let content_disposition = make_content_disposition(
response.content_disposition.as_ref(),
response.content_type.as_deref(),
None,
);
let content_disposition = make_content_disposition(
response.content_disposition.as_ref(),
response.content_type.as_deref(),
None,
);
Ok(get_content_thumbnail::v3::Response {
file: response.file,
content_type: response.content_type,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
} else {
Err!(Request(NotFound("Media not found.")))
Ok(get_content_thumbnail::v3::Response {
file: response.file,
content_type: response.content_type,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
} else {
Err!(Request(NotFound("Media not found.")))
},
}
}

View File

@@ -9,51 +9,55 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
at, debug, debug_info, debug_warn, err, info,
pdu::{gen_event_id_canonical_json, PduBuilder},
result::FlatOk,
Err, Result, at, debug, debug_info, debug_warn, err, error, info,
matrix::{
StateKey,
pdu::{PduBuilder, PduEvent, gen_event_id, gen_event_id_canonical_json},
state_res,
},
result::{FlatOk, NotFound},
trace,
utils::{self, shuffle, IterStream, ReadyExt},
warn, Err, PduEvent, Result,
utils::{self, IterStream, ReadyExt, shuffle},
warn,
};
use futures::{join, FutureExt, StreamExt, TryFutureExt};
use conduwuit_service::{
Services,
appservice::RegistrationInfo,
rooms::{
state::RoomMutexGuard,
state_compressor::{CompressedState, HashSetCompressStateEvent},
},
};
use futures::{FutureExt, StreamExt, TryFutureExt, future::join4, join};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName,
OwnedUserId, RoomId, RoomVersionId, ServerName, UserId,
api::{
client::{
error::ErrorKind,
knock::knock_room,
membership::{
ban_user, forget_room, get_member_events, invite_user, join_room_by_id,
join_room_by_id_or_alias,
ThirdPartySigned, ban_user, forget_room,
get_member_events::{self, v3::MembershipEventFilter},
invite_user, join_room_by_id, join_room_by_id_or_alias,
joined_members::{self, v3::RoomMember},
joined_rooms, kick_user, leave_room, unban_user, ThirdPartySigned,
joined_rooms, kick_user, leave_room, unban_user,
},
},
federation::{self, membership::create_invite},
},
canonical_json::to_canonical_value,
events::{
StateEventType,
room::{
join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
message::RoomMessageEventContent,
},
StateEventType,
},
state_res, CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId,
OwnedServerName, OwnedUserId, RoomId, RoomVersionId, ServerName, UserId,
};
use service::{
appservice::RegistrationInfo,
pdu::gen_event_id,
rooms::{
state::RoomMutexGuard,
state_compressor::{CompressedState, HashSetCompressStateEvent},
},
Services,
};
use crate::{client::full_user_deactivate, Ruma};
use crate::{Ruma, client::full_user_deactivate};
/// Checks if the room is banned in any way possible and the sender user is not
/// an admin.
@@ -75,10 +79,9 @@ async fn banned_room_check(
if let Some(room_id) = room_id {
if services.rooms.metadata.is_banned(room_id).await
|| services
.server
.config
.forbidden_remote_server_names
.contains(&room_id.server_name().unwrap().to_owned())
.is_match(room_id.server_name().unwrap().host())
{
warn!(
"User {user_id} who is not an admin attempted to send an invite for or \
@@ -116,10 +119,9 @@ async fn banned_room_check(
}
} else if let Some(server_name) = server_name {
if services
.server
.config
.forbidden_remote_server_names
.contains(&server_name.to_owned())
.is_match(server_name.host())
{
warn!(
"User {user_id} who is not an admin tried joining a room which has the server \
@@ -474,9 +476,9 @@ pub(crate) async fn leave_room_route(
State(services): State<crate::State>,
body: Ruma<leave_room::v3::Request>,
) -> Result<leave_room::v3::Response> {
leave_room(&services, body.sender_user(), &body.room_id, body.reason.clone()).await?;
Ok(leave_room::v3::Response::new())
leave_room(&services, body.sender_user(), &body.room_id, body.reason.clone())
.await
.map(|()| leave_room::v3::Response::new())
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/invite`
@@ -490,7 +492,7 @@ pub(crate) async fn invite_user_route(
) -> Result<invite_user::v3::Response> {
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await && services.globals.block_non_admin_invites() {
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
info!(
"User {sender_user} is not an admin and attempted to send an invite to room {}",
&body.room_id
@@ -507,43 +509,52 @@ pub(crate) async fn invite_user_route(
)
.await?;
if let invite_user::v3::InvitationRecipient::UserId { user_id } = &body.recipient {
let sender_ignored_recipient = services.users.user_is_ignored(sender_user, user_id);
let recipient_ignored_by_sender = services.users.user_is_ignored(user_id, sender_user);
match &body.recipient {
| invite_user::v3::InvitationRecipient::UserId { user_id } => {
let sender_ignored_recipient = services.users.user_is_ignored(sender_user, user_id);
let recipient_ignored_by_sender =
services.users.user_is_ignored(user_id, sender_user);
let (sender_ignored_recipient, recipient_ignored_by_sender) =
join!(sender_ignored_recipient, recipient_ignored_by_sender);
let (sender_ignored_recipient, recipient_ignored_by_sender) =
join!(sender_ignored_recipient, recipient_ignored_by_sender);
if sender_ignored_recipient {
return Err!(Request(Forbidden(
"You cannot invite users you have ignored to rooms."
)));
}
if let Ok(target_user_membership) = services
.rooms
.state_accessor
.get_member(&body.room_id, user_id)
.await
{
if target_user_membership.membership == MembershipState::Ban {
return Err!(Request(Forbidden("User is banned from this room.")));
if sender_ignored_recipient {
return Ok(invite_user::v3::Response {});
}
}
if recipient_ignored_by_sender {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
return Ok(invite_user::v3::Response {});
}
if let Ok(target_user_membership) = services
.rooms
.state_accessor
.get_member(&body.room_id, user_id)
.await
{
if target_user_membership.membership == MembershipState::Ban {
return Err!(Request(Forbidden("User is banned from this room.")));
}
}
invite_helper(&services, sender_user, user_id, &body.room_id, body.reason.clone(), false)
if recipient_ignored_by_sender {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
return Ok(invite_user::v3::Response {});
}
invite_helper(
&services,
sender_user,
user_id,
&body.room_id,
body.reason.clone(),
false,
)
.boxed()
.await?;
Ok(invite_user::v3::Response {})
} else {
Err!(Request(NotFound("User not found.")))
Ok(invite_user::v3::Response {})
},
| _ => {
Err!(Request(NotFound("User not found.")))
},
}
}
@@ -706,21 +717,37 @@ pub(crate) async fn forget_room_route(
State(services): State<crate::State>,
body: Ruma<forget_room::v3::Request>,
) -> Result<forget_room::v3::Response> {
let sender_user = body.sender_user();
let user_id = body.sender_user();
let room_id = &body.room_id;
if services
.rooms
.state_cache
.is_joined(sender_user, &body.room_id)
.await
{
let joined = services.rooms.state_cache.is_joined(user_id, room_id);
let knocked = services.rooms.state_cache.is_knocked(user_id, room_id);
let left = services.rooms.state_cache.is_left(user_id, room_id);
let invited = services.rooms.state_cache.is_invited(user_id, room_id);
let (joined, knocked, left, invited) = join4(joined, knocked, left, invited).await;
if joined || knocked || invited {
return Err!(Request(Unknown("You must leave the room before forgetting it")));
}
services
let membership = services
.rooms
.state_cache
.forget(&body.room_id, sender_user);
.state_accessor
.get_member(room_id, user_id)
.await;
if membership.is_not_found() {
return Err!(Request(Unknown("No membership event was found, room was never joined")));
}
if left
|| membership.is_ok_and(|member| {
member.membership == MembershipState::Leave
|| member.membership == MembershipState::Ban
}) {
services.rooms.state_cache.forget(room_id, user_id);
}
Ok(forget_room::v3::Response::new())
}
@@ -743,6 +770,54 @@ pub(crate) async fn joined_rooms_route(
})
}
fn membership_filter(
pdu: PduEvent,
for_membership: Option<&MembershipEventFilter>,
not_membership: Option<&MembershipEventFilter>,
) -> Option<PduEvent> {
let membership_state_filter = match for_membership {
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
| Some(MembershipEventFilter::Leave) => MembershipState::Leave,
| Some(_) | None => MembershipState::Join,
};
let not_membership_state_filter = match not_membership {
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
| Some(MembershipEventFilter::Join) => MembershipState::Join,
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
| Some(_) | None => MembershipState::Leave,
};
let evt_membership = pdu.get_content::<RoomMemberEventContent>().ok()?.membership;
if for_membership.is_some() && not_membership.is_some() {
if membership_state_filter != evt_membership
|| not_membership_state_filter == evt_membership
{
None
} else {
Some(pdu)
}
} else if for_membership.is_some() && not_membership.is_none() {
if membership_state_filter != evt_membership {
None
} else {
Some(pdu)
}
} else if not_membership.is_some() && for_membership.is_none() {
if not_membership_state_filter == evt_membership {
None
} else {
Some(pdu)
}
} else {
Some(pdu)
}
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/members`
///
/// Lists all joined users in a room (TODO: at a specific point in time, with a
@@ -754,6 +829,8 @@ pub(crate) async fn get_member_events_route(
body: Ruma<get_member_events::v3::Request>,
) -> Result<get_member_events::v3::Response> {
let sender_user = body.sender_user();
let membership = body.membership.as_ref();
let not_membership = body.not_membership.as_ref();
if !services
.rooms
@@ -772,6 +849,7 @@ pub(crate) async fn get_member_events_route(
.ready_filter_map(Result::ok)
.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
.map(at!(1))
.ready_filter_map(|pdu| membership_filter(pdu, membership, not_membership))
.map(PduEvent::into_member_event)
.collect()
.await,
@@ -982,7 +1060,7 @@ async fn join_room_by_id_helper_remote(
| _ => {
join_event_stub.remove("event_id");
},
};
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
@@ -1011,10 +1089,17 @@ async fn join_room_by_id_helper_remote(
.await,
};
let send_join_response = services
let send_join_response = match services
.sending
.send_synapse_request(&remote_server, send_join_request)
.await?;
.await
{
| Ok(response) => response,
| Err(e) => {
error!("send_join failed: {e}");
return Err(e);
},
};
info!("send_join finished");
@@ -1151,8 +1236,8 @@ async fn join_room_by_id_helper_remote(
debug!("Running send_join auth check");
let fetch_state = &state;
let state_fetch = |k: &'static StateEventType, s: String| async move {
let shortstatekey = services.rooms.short.get_shortstatekey(k, &s).await.ok()?;
let state_fetch = |k: StateEventType, s: StateKey| async move {
let shortstatekey = services.rooms.short.get_shortstatekey(&k, &s).await.ok()?;
let event_id = fetch_state.get(&shortstatekey)?;
services.rooms.timeline.get_pdu(event_id).await.ok()
@@ -1162,7 +1247,7 @@ async fn join_room_by_id_helper_remote(
&state_res::RoomVersion::new(&room_version_id)?,
&parsed_join_pdu,
None, // TODO: third party invite
|k, s| state_fetch(k, s.to_owned()),
|k, s| state_fetch(k.clone(), s.into()),
)
.await
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
@@ -1402,7 +1487,7 @@ async fn join_room_by_id_helper_local(
| _ => {
join_event_stub.remove("event_id");
},
};
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
@@ -1544,7 +1629,7 @@ pub(crate) async fn invite_helper(
reason: Option<String>,
is_direct: bool,
) -> Result {
if !services.users.is_admin(sender_user).await && services.globals.block_non_admin_invites() {
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
info!(
"User {sender_user} is not an admin and attempted to send an invite to room \
{room_id}"
@@ -1679,8 +1764,8 @@ pub(crate) async fn invite_helper(
Ok(())
}
// Make a user leave all their joined rooms, forgets all rooms, and ignores
// errors
// Make a user leave all their joined rooms, rescinds knocks, forgets all rooms,
// and ignores errors
pub async fn leave_all_rooms(services: &Services, user_id: &UserId) {
let rooms_joined = services
.rooms
@@ -1694,7 +1779,17 @@ pub async fn leave_all_rooms(services: &Services, user_id: &UserId) {
.rooms_invited(user_id)
.map(|(r, _)| r);
let all_rooms: Vec<_> = rooms_joined.chain(rooms_invited).collect().await;
let rooms_knocked = services
.rooms
.state_cache
.rooms_knocked(user_id)
.map(|(r, _)| r);
let all_rooms: Vec<_> = rooms_joined
.chain(rooms_invited)
.chain(rooms_knocked)
.collect()
.await;
for room_id in all_rooms {
// ignore errors
@@ -1711,7 +1806,40 @@ pub async fn leave_room(
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
) -> Result<()> {
) -> Result {
let default_member_content = RoomMemberEventContent {
membership: MembershipState::Leave,
reason: reason.clone(),
join_authorized_via_users_server: None,
is_direct: None,
avatar_url: None,
displayname: None,
third_party_invite: None,
blurhash: None,
};
if services.rooms.metadata.is_banned(room_id).await
|| services.rooms.metadata.is_disabled(room_id).await
{
// the room is banned/disabled, the room must be rejected locally since we
// cant/dont want to federate with this server
services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
None,
None,
true,
)
.await?;
return Ok(());
}
// Ask a remote server if we don't have this room and are not knocking on it
if !services
.rooms
@@ -1744,7 +1872,7 @@ pub async fn leave_room(
.update_membership(
room_id,
user_id,
RoomMemberEventContent::new(MembershipState::Leave),
default_member_content,
user_id,
last_state,
None,
@@ -1764,26 +1892,23 @@ pub async fn leave_room(
)
.await
else {
// Fix for broken rooms
warn!(
debug_warn!(
"Trying to leave a room you are not a member of, marking room as left locally."
);
services
return services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
RoomMemberEventContent::new(MembershipState::Leave),
default_member_content,
user_id,
None,
None,
true,
)
.await?;
return Ok(());
.await;
};
services
@@ -1813,7 +1938,7 @@ async fn remote_leave_room(
room_id: &RoomId,
) -> Result<()> {
let mut make_leave_response_and_server =
Err!(BadServerResponse("No server available to assist in leaving."));
Err!(BadServerResponse("No remote server available to assist in leaving {room_id}."));
let mut servers: HashSet<OwnedServerName> = services
.rooms
@@ -1823,38 +1948,46 @@ async fn remote_leave_room(
.collect()
.await;
if let Ok(invite_state) = services
match services
.rooms
.state_cache
.invite_state(user_id, room_id)
.await
{
servers.extend(
invite_state
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
} else if let Ok(knock_state) = services
.rooms
.state_cache
.knock_state(user_id, room_id)
.await
{
servers.extend(
knock_state
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.filter_map(|sender| {
if !services.globals.user_is_local(sender) {
Some(sender.server_name().to_owned())
} else {
None
}
}),
);
| Ok(invite_state) => {
servers.extend(
invite_state
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
},
| _ => {
match services
.rooms
.state_cache
.knock_state(user_id, room_id)
.await
{
| Ok(knock_state) => {
servers.extend(
knock_state
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.filter_map(|sender| {
if !services.globals.user_is_local(sender) {
Some(sender.server_name().to_owned())
} else {
None
}
}),
);
},
| _ => {},
}
},
}
if let Some(room_id_server_name) = room_id.server_name() {
@@ -1885,20 +2018,25 @@ async fn remote_leave_room(
let (make_leave_response, remote_server) = make_leave_response_and_server?;
let Some(room_version_id) = make_leave_response.room_version else {
return Err!(BadServerResponse("Remote room version is not supported by conduwuit"));
return Err!(BadServerResponse(warn!(
"No room version was returned by {remote_server} for {room_id}, room version is \
likely not supported by conduwuit"
)));
};
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
return Err!(BadServerResponse(warn!(
"Remote room version {room_version_id} for {room_id} is not supported by conduwuit",
)));
}
let mut leave_event_stub = serde_json::from_str::<CanonicalJsonObject>(
make_leave_response.event.get(),
)
.map_err(|e| {
err!(BadServerResponse("Invalid make_leave event json received from server: {e:?}"))
err!(BadServerResponse(warn!(
"Invalid make_leave event json received from {remote_server} for {room_id}: {e:?}"
)))
})?;
// TODO: Is origin needed?
@@ -1921,7 +2059,7 @@ async fn remote_leave_room(
| _ => {
leave_event_stub.remove("event_id");
},
};
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present

View File

@@ -1,36 +1,39 @@
use axum::extract::State;
use conduwuit::{
at, is_equal_to,
Err, Result, at,
matrix::{
Event,
pdu::{PduCount, PduEvent},
},
utils::{
IterStream, ReadyExt,
result::{FlatOk, LogErr},
stream::{BroadbandExt, TryIgnore, WidebandExt},
IterStream, ReadyExt,
},
Event, PduCount, PduEvent, Result,
};
use futures::{future::OptionFuture, pin_mut, FutureExt, StreamExt, TryFutureExt};
use ruma::{
api::{
client::{filter::RoomEventFilter, message::get_message_events},
Direction,
},
events::{AnyStateEvent, StateEventType, TimelineEventType, TimelineEventType::*},
serde::Raw,
RoomId, UserId,
};
use service::{
use conduwuit_service::{
Services,
rooms::{
lazy_loading,
lazy_loading::{Options, Witness},
timeline::PdusIterItem,
},
Services,
};
use futures::{FutureExt, StreamExt, TryFutureExt, future::OptionFuture, pin_mut};
use ruma::{
RoomId, UserId,
api::{
Direction,
client::{filter::RoomEventFilter, message::get_message_events},
},
events::{AnyStateEvent, StateEventType, TimelineEventType, TimelineEventType::*},
serde::Raw,
};
use crate::Ruma;
/// list of safe and common non-state events to ignore if the user is ignored
const IGNORED_MESSAGE_TYPES: &[TimelineEventType; 17] = &[
const IGNORED_MESSAGE_TYPES: &[TimelineEventType] = &[
Audio,
CallInvite,
Emote,
@@ -69,6 +72,10 @@ pub(crate) async fn get_message_events_route(
let room_id = &body.room_id;
let filter = &body.filter;
if !services.rooms.metadata.exists(room_id).await {
return Err!(Request(Forbidden("Room does not exist to this server")));
}
let from: PduCount = body
.from
.as_deref()
@@ -154,7 +161,7 @@ pub(crate) async fn get_message_events_route(
let chunk = events
.into_iter()
.map(at!(1))
.map(|pdu| pdu.to_room_event())
.map(PduEvent::into_room_event)
.collect();
Ok(get_message_events::v3::Response {
@@ -225,34 +232,49 @@ async fn get_member_event(
.ok()
}
#[inline]
pub(crate) async fn ignored_filter(
services: &Services,
item: PdusIterItem,
user_id: &UserId,
) -> Option<PdusIterItem> {
let (_, pdu) = &item;
let (_, ref pdu) = item;
is_ignored_pdu(services, pdu, user_id)
.await
.eq(&false)
.then_some(item)
}
#[inline]
pub(crate) async fn is_ignored_pdu(
services: &Services,
pdu: &PduEvent,
user_id: &UserId,
) -> bool {
// exclude Synapse's dummy events from bloating up response bodies. clients
// don't need to see this.
if pdu.kind.to_cow_str() == "org.matrix.dummy_event" {
return None;
return true;
}
if IGNORED_MESSAGE_TYPES.binary_search(&pdu.kind).is_ok()
&& (services.users.user_is_ignored(&pdu.sender, user_id).await
|| services
.server
.config
.forbidden_remote_server_names
.iter()
.any(is_equal_to!(pdu.sender().server_name())))
let ignored_type = IGNORED_MESSAGE_TYPES.binary_search(&pdu.kind).is_ok();
let ignored_server = services
.config
.forbidden_remote_server_names
.is_match(pdu.sender().server_name().host());
if ignored_type
&& (ignored_server || services.users.user_is_ignored(&pdu.sender, user_id).await)
{
return None;
return true;
}
Some(item)
false
}
#[inline]
pub(crate) async fn visibility_filter(
services: &Services,
item: PdusIterItem,
@@ -268,7 +290,16 @@ pub(crate) async fn visibility_filter(
.then_some(item)
}
#[inline]
pub(crate) fn event_filter(item: PdusIterItem, filter: &RoomEventFilter) -> Option<PdusIterItem> {
let (_, pdu) = &item;
pdu.matches(filter).then_some(item)
}
#[cfg_attr(debug_assertions, conduwuit::ctor)]
fn _is_sorted() {
debug_assert!(
IGNORED_MESSAGE_TYPES.is_sorted(),
"IGNORED_MESSAGE_TYPES must be sorted by the developer"
);
}

View File

@@ -1,14 +1,14 @@
use std::time::Duration;
use axum::extract::State;
use conduwuit::utils;
use conduwuit::{Error, Result, utils};
use ruma::{
api::client::{account, error::ErrorKind},
authentication::TokenType,
};
use super::TOKEN_LENGTH;
use crate::{Error, Result, Ruma};
use crate::Ruma;
/// # `POST /_matrix/client/v3/user/{userId}/openid/request_token`
///

View File

@@ -1,12 +1,10 @@
use std::time::Duration;
use axum::extract::State;
use ruma::api::client::{
error::ErrorKind,
presence::{get_presence, set_presence},
};
use conduwuit::{Err, Result};
use ruma::api::client::presence::{get_presence, set_presence};
use crate::{Error, Result, Ruma};
use crate::Ruma;
/// # `PUT /_matrix/client/r0/presence/{userId}/status`
///
@@ -15,24 +13,17 @@ pub(crate) async fn set_presence_route(
State(services): State<crate::State>,
body: Ruma<set_presence::v3::Request>,
) -> Result<set_presence::v3::Response> {
if !services.globals.allow_local_presence() {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Presence is disabled on this server",
));
if !services.config.allow_local_presence {
return Err!(Request(Forbidden("Presence is disabled on this server")));
}
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if sender_user != &body.user_id && body.appservice_info.is_none() {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Not allowed to set presence of other users",
));
if body.sender_user() != body.user_id && body.appservice_info.is_none() {
return Err!(Request(InvalidParam("Not allowed to set presence of other users")));
}
services
.presence
.set_presence(sender_user, &body.presence, None, None, body.status_msg.clone())
.set_presence(body.sender_user(), &body.presence, None, None, body.status_msg.clone())
.await?;
Ok(set_presence::v3::Response {})
@@ -47,21 +38,15 @@ pub(crate) async fn get_presence_route(
State(services): State<crate::State>,
body: Ruma<get_presence::v3::Request>,
) -> Result<get_presence::v3::Response> {
if !services.globals.allow_local_presence() {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Presence is disabled on this server",
));
if !services.config.allow_local_presence {
return Err!(Request(Forbidden("Presence is disabled on this server",)));
}
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut presence_event = None;
let has_shared_rooms = services
.rooms
.state_cache
.user_sees_user(sender_user, &body.user_id)
.user_sees_user(body.sender_user(), &body.user_id)
.await;
if has_shared_rooms {
@@ -70,37 +55,35 @@ pub(crate) async fn get_presence_route(
}
}
if let Some(presence) = presence_event {
let status_msg = if presence
.content
.status_msg
.as_ref()
.is_some_and(String::is_empty)
{
None
} else {
presence.content.status_msg
};
let last_active_ago = match presence.content.currently_active {
| Some(true) => None,
| _ => presence
match presence_event {
| Some(presence) => {
let status_msg = if presence
.content
.last_active_ago
.map(|millis| Duration::from_millis(millis.into())),
};
.status_msg
.as_ref()
.is_some_and(String::is_empty)
{
None
} else {
presence.content.status_msg
};
Ok(get_presence::v3::Response {
// TODO: Should ruma just use the presenceeventcontent type here?
status_msg,
currently_active: presence.content.currently_active,
last_active_ago,
presence: presence.content.presence,
})
} else {
Err(Error::BadRequest(
ErrorKind::NotFound,
"Presence state for this user was not found",
))
let last_active_ago = match presence.content.currently_active {
| Some(true) => None,
| _ => presence
.content
.last_active_ago
.map(|millis| Duration::from_millis(millis.into())),
};
Ok(get_presence::v3::Response {
// TODO: Should ruma just use the presenceeventcontent type here?
status_msg,
currently_active: presence.content.currently_active,
last_active_ago,
presence: presence.content.presence,
})
},
| _ => Err!(Request(NotFound("Presence state for this user was not found"))),
}
}

View File

@@ -2,12 +2,15 @@
use axum::extract::State;
use conduwuit::{
pdu::PduBuilder,
utils::{stream::TryIgnore, IterStream},
warn, Err, Error, Result,
Err, Error, Result,
matrix::pdu::PduBuilder,
utils::{IterStream, stream::TryIgnore},
warn,
};
use futures::{future::join3, StreamExt, TryStreamExt};
use conduwuit_service::Services;
use futures::{StreamExt, TryStreamExt, future::join3};
use ruma::{
OwnedMxcUri, OwnedRoomId, UserId,
api::{
client::{
error::ErrorKind,
@@ -19,9 +22,7 @@
},
events::room::member::{MembershipState, RoomMemberEventContent},
presence::PresenceState,
OwnedMxcUri, OwnedRoomId, UserId,
};
use service::Services;
use crate::Ruma;
@@ -51,7 +52,7 @@ pub(crate) async fn set_displayname_route(
update_displayname(&services, &body.user_id, body.displayname.clone(), &all_joined_rooms)
.await;
if services.globals.allow_local_presence() {
if services.config.allow_local_presence {
// Presence update
services
.presence
@@ -146,7 +147,7 @@ pub(crate) async fn set_avatar_url_route(
)
.await;
if services.globals.allow_local_presence() {
if services.config.allow_local_presence {
// Presence update
services
.presence

View File

@@ -1,6 +1,8 @@
use axum::extract::State;
use conduwuit::{err, Err};
use conduwuit::{Err, Error, Result, err};
use conduwuit_service::Services;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue,
api::client::{
error::ErrorKind,
push::{
@@ -10,18 +12,16 @@
},
},
events::{
push_rules::{PushRulesEvent, PushRulesEventContent},
GlobalAccountDataEventType,
push_rules::{PushRulesEvent, PushRulesEventContent},
},
push::{
InsertPushRuleError, PredefinedContentRuleId, PredefinedOverrideRuleId,
RemovePushRuleError, Ruleset,
},
CanonicalJsonObject, CanonicalJsonValue,
};
use service::Services;
use crate::{Error, Result, Ruma};
use crate::Ruma;
/// # `GET /_matrix/client/r0/pushrules/`
///
@@ -503,7 +503,7 @@ pub(crate) async fn set_pushers_route(
services
.pusher
.set_pusher(sender_user, &body.action)
.set_pusher(sender_user, body.sender_device(), &body.action)
.await?;
Ok(set_pusher::v3::Response::new())

View File

@@ -1,17 +1,17 @@
use std::collections::BTreeMap;
use axum::extract::State;
use conduwuit::{err, Err, PduCount};
use conduwuit::{Err, PduCount, Result, err};
use ruma::{
MilliSecondsSinceUnixEpoch,
api::client::{read_marker::set_read_marker, receipt::create_receipt},
events::{
receipt::{ReceiptThread, ReceiptType},
RoomAccountDataEventType,
receipt::{ReceiptThread, ReceiptType},
},
MilliSecondsSinceUnixEpoch,
};
use crate::{Result, Ruma};
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/read_markers`
///
@@ -50,7 +50,7 @@ pub(crate) async fn set_read_marker_route(
}
// ping presence
if services.globals.allow_local_presence() {
if services.config.allow_local_presence {
services
.presence
.ping_presence(sender_user, &ruma::presence::PresenceState::Online)
@@ -126,7 +126,7 @@ pub(crate) async fn create_receipt_route(
}
// ping presence
if services.globals.allow_local_presence() {
if services.config.allow_local_presence {
services
.presence
.ping_presence(sender_user, &ruma::presence::PresenceState::Online)
@@ -197,11 +197,12 @@ pub(crate) async fn create_receipt_route(
.read_receipt
.private_read_set(&body.room_id, sender_user, count);
},
| _ =>
| _ => {
return Err!(Request(InvalidParam(warn!(
"Received unknown read receipt type: {}",
&body.receipt_type
)))),
))));
},
}
Ok(create_receipt::v3::Response {})

View File

@@ -1,9 +1,10 @@
use axum::extract::State;
use conduwuit::{Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::redact::redact_event, events::room::redaction::RoomRedactionEventContent,
};
use crate::{service::pdu::PduBuilder, Result, Ruma};
use crate::Ruma;
/// # `PUT /_matrix/client/r0/rooms/{roomId}/redact/{eventId}/{txnId}`
///

View File

@@ -1,22 +1,22 @@
use axum::extract::State;
use conduwuit::{
at,
utils::{result::FlatOk, stream::WidebandExt, IterStream, ReadyExt},
PduCount, Result,
Result, at,
matrix::pdu::PduCount,
utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt},
};
use conduwuit_service::{Services, rooms::timeline::PdusIterItem};
use futures::StreamExt;
use ruma::{
EventId, RoomId, UInt, UserId,
api::{
Direction,
client::relations::{
get_relating_events, get_relating_events_with_rel_type,
get_relating_events_with_rel_type_and_event_type,
},
Direction,
},
events::{relation::RelationType, TimelineEventType},
EventId, RoomId, UInt, UserId,
events::{TimelineEventType, relation::RelationType},
};
use service::{rooms::timeline::PdusIterItem, Services};
use crate::Ruma;

View File

@@ -2,23 +2,21 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{info, utils::ReadyExt, Err};
use conduwuit::{Err, Error, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
use conduwuit_service::Services;
use rand::Rng;
use ruma::{
EventId, RoomId, UserId,
api::client::{
error::ErrorKind,
room::{report_content, report_room},
},
events::room::message,
int, EventId, RoomId, UserId,
int,
};
use tokio::time::sleep;
use crate::{
debug_info,
service::{pdu::PduEvent, Services},
Error, Result, Ruma,
};
use crate::Ruma;
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
///
@@ -43,7 +41,7 @@ pub(crate) async fn report_room_route(
ErrorKind::InvalidParam,
"Reason too long, should be 750 characters or fewer",
));
};
}
delay_response().await;
@@ -164,14 +162,14 @@ async fn is_event_report_valid(
ErrorKind::InvalidParam,
"Invalid score, must be within 0 to -100",
));
};
}
if reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Reason too long, should be 750 characters or fewer",
));
};
}
if !services
.rooms

View File

@@ -2,15 +2,20 @@
use axum::extract::State;
use conduwuit::{
debug_info, debug_warn, err, error, info, pdu::PduBuilder, warn, Err, Error, Result,
Err, Error, Result, debug_info, debug_warn, err, error, info,
matrix::{StateKey, pdu::PduBuilder},
warn,
};
use conduwuit_service::{Services, appservice::RegistrationInfo};
use futures::FutureExt;
use ruma::{
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
api::client::{
error::ErrorKind,
room::{self, create_room},
},
events::{
TimelineEventType,
room::{
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
@@ -22,16 +27,13 @@
power_levels::RoomPowerLevelsEventContent,
topic::RoomTopicEventContent,
},
TimelineEventType,
},
int,
serde::{JsonObject, Raw},
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
};
use serde_json::{json, value::to_raw_value};
use service::{appservice::RegistrationInfo, Services};
use crate::{client::invite_helper, Ruma};
use crate::{Ruma, client::invite_helper};
/// # `POST /_matrix/client/v3/createRoom`
///
@@ -68,10 +70,9 @@ pub(crate) async fn create_room_route(
));
}
let room_id: OwnedRoomId = if let Some(custom_room_id) = &body.room_id {
custom_room_id_check(&services, custom_room_id)?
} else {
RoomId::new(&services.server.name)
let room_id: OwnedRoomId = match &body.room_id {
| Some(custom_room_id) => custom_room_id_check(&services, custom_room_id)?,
| _ => RoomId::new(&services.server.name),
};
// check if room ID doesn't already exist instead of erroring on auth check
@@ -114,10 +115,10 @@ pub(crate) async fn create_room_route(
.await;
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
let alias: Option<OwnedRoomAliasId> = if let Some(alias) = body.room_alias_name.as_ref() {
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?)
} else {
None
let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() {
| Some(alias) =>
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?),
| _ => None,
};
let room_version = match body.room_version.clone() {
@@ -198,7 +199,7 @@ pub(crate) async fn create_room_route(
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)
.expect("create event content serialization"),
state_key: Some(String::new()),
state_key: Some(StateKey::new()),
..Default::default()
},
sender_user,
@@ -240,9 +241,7 @@ pub(crate) async fn create_room_route(
if preset == RoomPreset::TrustedPrivateChat {
for invite in &body.invite {
if services.users.user_is_ignored(sender_user, invite).await {
return Err!(Request(Forbidden(
"You cannot invite users you have ignored to rooms."
)));
continue;
} else if services.users.user_is_ignored(invite, sender_user).await {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
@@ -267,7 +266,7 @@ pub(crate) async fn create_room_route(
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_content)
.expect("serialized power_levels event content"),
state_key: Some(String::new()),
state_key: Some(StateKey::new()),
..Default::default()
},
sender_user,
@@ -371,11 +370,11 @@ pub(crate) async fn create_room_route(
}
// Implicit state key defaults to ""
pdu_builder.state_key.get_or_insert_with(String::new);
pdu_builder.state_key.get_or_insert_with(StateKey::new);
// Silently skip encryption events if they are not allowed
if pdu_builder.event_type == TimelineEventType::RoomEncryption
&& !services.globals.allow_encryption()
&& !services.config.allow_encryption
{
continue;
}
@@ -421,9 +420,7 @@ pub(crate) async fn create_room_route(
drop(state_lock);
for user_id in &body.invite {
if services.users.user_is_ignored(sender_user, user_id).await {
return Err!(Request(Forbidden(
"You cannot invite users you have ignored to rooms."
)));
continue;
} else if services.users.user_is_ignored(user_id, sender_user).await {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked

View File

@@ -1,52 +1,44 @@
use axum::extract::State;
use conduwuit::{err, Err, Event, Result};
use futures::{try_join, FutureExt, TryFutureExt};
use conduwuit::{Err, Event, Result, err};
use futures::{FutureExt, TryFutureExt, future::try_join};
use ruma::api::client::room::get_room_event;
use crate::{client::ignored_filter, Ruma};
use crate::{Ruma, client::is_ignored_pdu};
/// # `GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}`
///
/// Gets a single event.
pub(crate) async fn get_room_event_route(
State(services): State<crate::State>,
State(ref services): State<crate::State>,
ref body: Ruma<get_room_event::v3::Request>,
) -> Result<get_room_event::v3::Response> {
let event_id = &body.event_id;
let room_id = &body.room_id;
let event = services
.rooms
.timeline
.get_pdu(&body.event_id)
.map_err(|_| err!(Request(NotFound("Event {} not found.", &body.event_id))));
let token = services
.rooms
.timeline
.get_pdu_count(&body.event_id)
.map_err(|_| err!(Request(NotFound("Event not found."))));
.get_pdu(event_id)
.map_err(|_| err!(Request(NotFound("Event {} not found.", event_id))));
let visible = services
.rooms
.state_accessor
.user_can_see_event(body.sender_user(), &body.room_id, &body.event_id)
.user_can_see_event(body.sender_user(), room_id, event_id)
.map(Ok);
let (token, mut event, visible) = try_join!(token, event, visible)?;
let (mut event, visible) = try_join(event, visible).await?;
if !visible
|| ignored_filter(&services, (token, event.clone()), body.sender_user())
.await
.is_none()
{
if !visible || is_ignored_pdu(services, &event, body.sender_user()).await {
return Err!(Request(Forbidden("You don't have permission to view this event.")));
}
if event.event_id() != &body.event_id || event.room_id() != body.room_id {
return Err!(Request(NotFound("Event not found")));
}
debug_assert!(
event.event_id() == event_id && event.room_id() == room_id,
"Fetched PDU must match requested"
);
event.add_age().ok();
let event = event.to_room_event();
Ok(get_room_event::v3::Response { event })
Ok(get_room_event::v3::Response { event: event.into_room_event() })
}

View File

@@ -1,8 +1,7 @@
use axum::extract::State;
use conduwuit::{
at,
utils::{stream::TryTools, BoolExt},
Err, PduEvent, Result,
Err, PduEvent, Result, at,
utils::{BoolExt, stream::TryTools},
};
use futures::TryStreamExt;
use ruma::api::client::room::initial_sync::v3::{PaginationChunk, Request, Response};
@@ -56,7 +55,7 @@ pub(crate) async fn room_initial_sync_route(
chunk: events
.into_iter()
.map(at!(1))
.map(|pdu| pdu.to_room_event())
.map(PduEvent::into_room_event)
.collect(),
};

View File

@@ -2,9 +2,14 @@
mod create;
mod event;
mod initial_sync;
mod summary;
mod upgrade;
pub(crate) use self::{
aliases::get_room_aliases_route, create::create_room_route, event::get_room_event_route,
initial_sync::room_initial_sync_route, upgrade::upgrade_room_route,
aliases::get_room_aliases_route,
create::create_room_route,
event::get_room_event_route,
initial_sync::room_initial_sync_route,
summary::{get_room_summary, get_room_summary_legacy},
upgrade::upgrade_room_route,
};

View File

@@ -0,0 +1,330 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, debug_warn, trace,
utils::{IterStream, future::TryExtExt},
};
use futures::{
FutureExt, StreamExt,
future::{OptionFuture, join3},
stream::FuturesUnordered,
};
use ruma::{
OwnedServerName, RoomId, UserId,
api::{
client::room::get_summary,
federation::space::{SpaceHierarchyParentSummary, get_hierarchy},
},
events::room::member::MembershipState,
space::SpaceRoomJoinRule::{self, *},
};
use service::Services;
use crate::{Ruma, RumaResponse};
/// # `GET /_matrix/client/unstable/im.nheko.summary/rooms/{roomIdOrAlias}/summary`
///
/// Returns a short description of the state of a room.
///
/// This is the "wrong" endpoint that some implementations/clients may use
/// according to the MSC. Request and response bodies are the same as
/// `get_room_summary`.
///
/// An implementation of [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)
pub(crate) async fn get_room_summary_legacy(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_summary::msc3266::Request>,
) -> Result<RumaResponse<get_summary::msc3266::Response>> {
get_room_summary(State(services), InsecureClientIp(client), body)
.boxed()
.await
.map(RumaResponse)
}
/// # `GET /_matrix/client/unstable/im.nheko.summary/summary/{roomIdOrAlias}`
///
/// Returns a short description of the state of a room.
///
/// An implementation of [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)
#[tracing::instrument(skip_all, fields(%client), name = "room_summary")]
pub(crate) async fn get_room_summary(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_summary::msc3266::Request>,
) -> Result<get_summary::msc3266::Response> {
let (room_id, servers) = services
.rooms
.alias
.resolve_with_servers(&body.room_id_or_alias, Some(body.via.clone()))
.await?;
if services.rooms.metadata.is_banned(&room_id).await {
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
}
room_summary_response(&services, &room_id, &servers, body.sender_user.as_deref())
.boxed()
.await
}
async fn room_summary_response(
services: &Services,
room_id: &RoomId,
servers: &[OwnedServerName],
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
if services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await
{
return local_room_summary_response(services, room_id, sender_user)
.boxed()
.await;
}
let room =
remote_room_summary_hierarchy_response(services, room_id, servers, sender_user).await?;
Ok(get_summary::msc3266::Response {
room_id: room_id.to_owned(),
canonical_alias: room.canonical_alias,
avatar_url: room.avatar_url,
guest_can_join: room.guest_can_join,
name: room.name,
num_joined_members: room.num_joined_members,
topic: room.topic,
world_readable: room.world_readable,
join_rule: room.join_rule,
room_type: room.room_type,
room_version: room.room_version,
encryption: room.encryption,
allowed_room_ids: room.allowed_room_ids,
membership: sender_user.is_some().then_some(MembershipState::Leave),
})
}
async fn local_room_summary_response(
services: &Services,
room_id: &RoomId,
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
trace!(?sender_user, "Sending local room summary response for {room_id:?}");
let join_rule = services.rooms.state_accessor.get_join_rules(room_id);
let world_readable = services.rooms.state_accessor.is_world_readable(room_id);
let guest_can_join = services.rooms.state_accessor.guest_can_join(room_id);
let (join_rule, world_readable, guest_can_join) =
join3(join_rule, world_readable, guest_can_join).await;
trace!("{join_rule:?}, {world_readable:?}, {guest_can_join:?}");
user_can_see_summary(
services,
room_id,
&join_rule.clone().into(),
guest_can_join,
world_readable,
join_rule.allowed_rooms(),
sender_user,
)
.await?;
let canonical_alias = services
.rooms
.state_accessor
.get_canonical_alias(room_id)
.ok();
let name = services.rooms.state_accessor.get_name(room_id).ok();
let topic = services.rooms.state_accessor.get_room_topic(room_id).ok();
let room_type = services.rooms.state_accessor.get_room_type(room_id).ok();
let avatar_url = services
.rooms
.state_accessor
.get_avatar(room_id)
.map(|res| res.into_option().unwrap_or_default().url);
let room_version = services.rooms.state.get_room_version(room_id).ok();
let encryption = services
.rooms
.state_accessor
.get_room_encryption(room_id)
.ok();
let num_joined_members = services
.rooms
.state_cache
.room_joined_count(room_id)
.unwrap_or(0);
let membership: OptionFuture<_> = sender_user
.map(|sender_user| {
services
.rooms
.state_accessor
.get_member(room_id, sender_user)
.map_ok_or(MembershipState::Leave, |content| content.membership)
})
.into();
let (
canonical_alias,
name,
num_joined_members,
topic,
avatar_url,
room_type,
room_version,
encryption,
membership,
) = futures::join!(
canonical_alias,
name,
num_joined_members,
topic,
avatar_url,
room_type,
room_version,
encryption,
membership,
);
Ok(get_summary::msc3266::Response {
room_id: room_id.to_owned(),
canonical_alias,
avatar_url,
guest_can_join,
name,
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
topic,
world_readable,
room_type,
room_version,
encryption,
membership,
allowed_room_ids: join_rule.allowed_rooms().map(Into::into).collect(),
join_rule: join_rule.into(),
})
}
/// used by MSC3266 to fetch a room's info if we do not know about it
async fn remote_room_summary_hierarchy_response(
services: &Services,
room_id: &RoomId,
servers: &[OwnedServerName],
sender_user: Option<&UserId>,
) -> Result<SpaceHierarchyParentSummary> {
trace!(?sender_user, ?servers, "Sending remote room summary response for {room_id:?}");
if !services.config.allow_federation {
return Err!(Request(Forbidden("Federation is disabled.")));
}
if services.rooms.metadata.is_disabled(room_id).await {
return Err!(Request(Forbidden(
"Federaton of room {room_id} is currently disabled on this server."
)));
}
let request = get_hierarchy::v1::Request::new(room_id.to_owned());
let mut requests: FuturesUnordered<_> = servers
.iter()
.map(|server| {
services
.sending
.send_federation_request(server, request.clone())
})
.collect();
while let Some(Ok(response)) = requests.next().await {
trace!("{response:?}");
let room = response.room.clone();
if room.room_id != room_id {
debug_warn!(
"Room ID {} returned does not belong to the requested room ID {}",
room.room_id,
room_id
);
continue;
}
return user_can_see_summary(
services,
room_id,
&room.join_rule,
room.guest_can_join,
room.world_readable,
room.allowed_room_ids.iter().map(AsRef::as_ref),
sender_user,
)
.await
.map(|()| room);
}
Err!(Request(NotFound(
"Room is unknown to this server and was unable to fetch over federation with the \
provided servers available"
)))
}
async fn user_can_see_summary<'a, I>(
services: &Services,
room_id: &RoomId,
join_rule: &SpaceRoomJoinRule,
guest_can_join: bool,
world_readable: bool,
allowed_room_ids: I,
sender_user: Option<&UserId>,
) -> Result
where
I: Iterator<Item = &'a RoomId> + Send,
{
let is_public_room = matches!(join_rule, Public | Knock | KnockRestricted);
match sender_user {
| Some(sender_user) => {
let user_can_see_state_events = services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, room_id);
let is_guest = services.users.is_deactivated(sender_user).unwrap_or(false);
let user_in_allowed_restricted_room = allowed_room_ids
.stream()
.any(|room| services.rooms.state_cache.is_joined(sender_user, room));
let (user_can_see_state_events, is_guest, user_in_allowed_restricted_room) =
join3(user_can_see_state_events, is_guest, user_in_allowed_restricted_room)
.boxed()
.await;
if user_can_see_state_events
|| (is_guest && guest_can_join)
|| is_public_room
|| user_in_allowed_restricted_room
{
return Ok(());
}
Err!(Request(Forbidden(
"Room is not world readable, not publicly accessible/joinable, restricted room \
conditions not met, and guest access is forbidden. Not allowed to see details \
of this room."
)))
},
| None => {
if is_public_room || world_readable {
return Ok(());
}
Err!(Request(Forbidden(
"Room is not world readable or publicly accessible/joinable, authentication is \
required"
)))
},
}
}

View File

@@ -1,19 +1,23 @@
use std::cmp::max;
use axum::extract::State;
use conduwuit::{err, info, pdu::PduBuilder, Error, Result};
use conduwuit::{
Error, Result, err, info,
matrix::{StateKey, pdu::PduBuilder},
};
use futures::StreamExt;
use ruma::{
CanonicalJsonObject, RoomId, RoomVersionId,
api::client::{error::ErrorKind, room::upgrade_room},
events::{
StateEventType, TimelineEventType,
room::{
member::{MembershipState, RoomMemberEventContent},
power_levels::RoomPowerLevelsEventContent,
tombstone::RoomTombstoneEventContent,
},
StateEventType, TimelineEventType,
},
int, CanonicalJsonObject, RoomId, RoomVersionId,
int,
};
use serde_json::{json, value::to_raw_value};
@@ -77,7 +81,7 @@ pub(crate) async fn upgrade_room_route(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomTombstoneEventContent {
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: replacement_room.clone(),
}),
@@ -102,7 +106,7 @@ pub(crate) async fn upgrade_room_route(
// Use the m.room.tombstone event as the predecessor
let predecessor = Some(ruma::events::room::create::PreviousRoom::new(
body.room_id.clone(),
(*tombstone_event_id).to_owned(),
Some(tombstone_event_id),
));
// Send a m.room.create event containing a predecessor field and the applicable
@@ -159,7 +163,7 @@ pub(crate) async fn upgrade_room_route(
content: to_raw_value(&create_event_content)
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
state_key: Some(StateKey::new()),
redacts: None,
timestamp: None,
},
@@ -188,7 +192,7 @@ pub(crate) async fn upgrade_room_route(
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.to_string()),
state_key: Some(sender_user.as_str().into()),
redacts: None,
timestamp: None,
},
@@ -217,7 +221,7 @@ pub(crate) async fn upgrade_room_route(
PduBuilder {
event_type: event_type.to_string().into(),
content: event_content,
state_key: Some(String::new()),
state_key: Some(StateKey::new()),
..Default::default()
},
sender_user,
@@ -272,7 +276,7 @@ pub(crate) async fn upgrade_room_route(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomPowerLevelsEventContent {
PduBuilder::state(StateKey::new(), &RoomPowerLevelsEventContent {
events_default: new_level,
invite: new_level,
..power_levels_event_content

View File

@@ -2,23 +2,23 @@
use axum::extract::State;
use conduwuit::{
at, is_true,
Err, Result, at, is_true,
matrix::pdu::PduEvent,
result::FlatOk,
utils::{stream::ReadyExt, IterStream},
Err, PduEvent, Result,
utils::{IterStream, stream::ReadyExt},
};
use futures::{future::OptionFuture, FutureExt, StreamExt, TryFutureExt, TryStreamExt};
use conduwuit_service::{Services, rooms::search::RoomQuery};
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::OptionFuture};
use ruma::{
OwnedRoomId, RoomId, UInt, UserId,
api::client::search::search_events::{
self,
v3::{Criteria, EventContextResult, ResultCategories, ResultRoomEvents, SearchResult},
},
events::AnyStateEvent,
serde::Raw,
OwnedRoomId, RoomId, UInt, UserId,
};
use search_events::v3::{Request, Response};
use service::{rooms::search::RoomQuery, Services};
use crate::Ruma;
@@ -144,7 +144,7 @@ async fn category_room_events(
.map(at!(2))
.flatten()
.stream()
.map(|pdu| pdu.to_room_event())
.map(PduEvent::into_room_event)
.map(|result| SearchResult {
rank: None,
result: Some(result),

View File

@@ -1,11 +1,11 @@
use std::collections::BTreeMap;
use axum::extract::State;
use conduwuit::{err, Err};
use conduwuit::{Err, Result, err, matrix::pdu::PduBuilder, utils};
use ruma::{api::client::message::send_message_event, events::MessageLikeEventType};
use serde_json::from_str;
use crate::{service::pdu::PduBuilder, utils, Result, Ruma};
use crate::Ruma;
/// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`
///
@@ -25,8 +25,7 @@ pub(crate) async fn send_message_event_route(
let appservice_info = body.appservice_info.as_ref();
// Forbid m.room.encrypted if encryption is disabled
if MessageLikeEventType::RoomEncrypted == body.event_type
&& !services.globals.allow_encryption()
if MessageLikeEventType::RoomEncrypted == body.event_type && !services.config.allow_encryption
{
return Err!(Request(Forbidden("Encryption has been disabled")));
}

View File

@@ -2,11 +2,15 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{debug, err, info, utils::ReadyExt, warn, Err};
use conduwuit::{
Err, Error, Result, debug, err, info, utils,
utils::{ReadyExt, hash},
};
use conduwuit_service::uiaa::SESSION_ID_LENGTH;
use futures::StreamExt;
use ruma::{
UserId,
api::client::{
error::ErrorKind,
session::{
get_login_token,
get_login_types::{
@@ -21,12 +25,10 @@
},
uiaa,
},
OwnedUserId, UserId,
};
use service::uiaa::SESSION_ID_LENGTH;
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::{utils, utils::hash, Error, Result, Ruma};
use crate::Ruma;
/// # `GET /_matrix/client/v3/login`
///
@@ -67,6 +69,8 @@ pub(crate) async fn login_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<login::v3::Request>,
) -> Result<login::v3::Response> {
let emergency_mode_enabled = services.config.emergency_password.is_some();
// Validate login method
// TODO: Other login methods
let user_id = match &body.login_info {
@@ -78,36 +82,67 @@ pub(crate) async fn login_route(
..
}) => {
debug!("Got password login type");
let user_id = if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) =
identifier
{
UserId::parse_with_server_name(
user_id.to_lowercase(),
services.globals.server_name(),
)
} else if let Some(user) = user {
OwnedUserId::parse(user)
} else {
warn!("Bad login type: {:?}", &body.login_info);
return Err!(Request(Forbidden("Bad login type.")));
}
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
let user_id =
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
UserId::parse_with_server_name(user_id, &services.config.server_name)
} else if let Some(user) = user {
UserId::parse_with_server_name(user, &services.config.server_name)
} else {
return Err!(Request(Unknown(
debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)")
)));
}
.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
let lowercased_user_id = UserId::parse_with_server_name(
user_id.localpart().to_lowercase(),
&services.config.server_name,
)?;
if !services.globals.user_is_local(&user_id)
|| !services.globals.user_is_local(&lowercased_user_id)
{
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
}
// first try the username as-is
let hash = services
.users
.password_hash(&user_id)
.await
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
.inspect_err(|e| debug!("{e}"));
if hash.is_empty() {
return Err!(Request(UserDeactivated("The user has been deactivated")));
match hash {
| Ok(hash) => {
if hash.is_empty() {
return Err!(Request(UserDeactivated("The user has been deactivated")));
}
hash::verify_password(password, &hash)
.inspect_err(|e| debug!("{e}"))
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
user_id
},
| Err(_e) => {
let hash_lowercased_user_id = services
.users
.password_hash(&lowercased_user_id)
.await
.inspect_err(|e| debug!("{e}"))
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
if hash_lowercased_user_id.is_empty() {
return Err!(Request(UserDeactivated("The user has been deactivated")));
}
hash::verify_password(password, &hash_lowercased_user_id)
.inspect_err(|e| debug!("{e}"))
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
lowercased_user_id
},
}
if hash::verify_password(password, &hash).is_err() {
return Err!(Request(Forbidden("Wrong username or password.")));
}
user_id
},
| login::v3::LoginInfo::Token(login::v3::Token { token }) => {
debug!("Got token login type");
@@ -122,46 +157,38 @@ pub(crate) async fn login_route(
user,
}) => {
debug!("Got appservice login type");
let Some(ref info) = body.appservice_info else {
return Err!(Request(MissingToken("Missing appservice token.")));
};
let user_id =
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
UserId::parse_with_server_name(
user_id.to_lowercase(),
services.globals.server_name(),
)
UserId::parse_with_server_name(user_id, &services.config.server_name)
} else if let Some(user) = user {
OwnedUserId::parse(user)
UserId::parse_with_server_name(user, &services.config.server_name)
} else {
warn!("Bad login type: {:?}", &body.login_info);
return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
return Err!(Request(Unknown(
debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)")
)));
}
.map_err(|e| {
warn!("Failed to parse username from appservice logging in: {e}");
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
})?;
.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
if let Some(ref info) = body.appservice_info {
if !info.is_user_match(&user_id) {
return Err(Error::BadRequest(
ErrorKind::Exclusive,
"User is not in namespace.",
));
}
} else {
return Err(Error::BadRequest(
ErrorKind::MissingToken,
"Missing appservice token.",
));
if !services.globals.user_is_local(&user_id) {
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
}
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
}
user_id
},
| _ => {
warn!("Unsupported or unknown login type: {:?}", &body.login_info);
debug!("JSON body: {:?}", &body.json_body);
return Err(Error::BadRequest(
ErrorKind::Unknown,
"Unsupported or unknown login type.",
));
debug!("/login json_body: {:?}", &body.json_body);
return Err!(Request(Unknown(
debug_warn!(?body.login_info, "Invalid or unsupported login type")
)));
},
};
@@ -214,9 +241,6 @@ pub(crate) async fn login_route(
info!("{user_id} logged in");
// home_server is deprecated but apparently must still be sent despite it being
// deprecated over 6 years ago. initially i thought this macro was unnecessary,
// but ruma uses this same macro for the same reason so...
#[allow(deprecated)]
Ok(login::v3::Response {
user_id,
@@ -224,7 +248,7 @@ pub(crate) async fn login_route(
device_id,
well_known: client_discovery_info,
expires_in: None,
home_server: Some(services.globals.server_name().to_owned()),
home_server: Some(services.config.server_name.clone()),
refresh_token: None,
})
}
@@ -259,26 +283,32 @@ pub(crate) async fn login_token_route(
auth_error: None,
};
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
match &body.auth {
| Some(auth) => {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
} else if let Some(json) = body.json_body.as_ref() {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
// Success!
},
| _ => match body.json_body.as_ref() {
| Some(json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
} else {
return Err!(Request(NotJson("No JSON body was sent when required.")));
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err!(Request(NotJson("No JSON body was sent when required.")));
},
},
}
let login_token = utils::random_string(TOKEN_LENGTH);

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