Compare commits

...

337 Commits

Author SHA1 Message Date
strawberry fe1ce521aa add ignored user checks on /context and /event, misc cleanup
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-12-04 18:33:12 -05:00
strawberry ad0c5ceda4 add origin to tracing instrument logs on /send
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-12-04 17:13:39 -05:00
Jason Volk 68afdb22c7 force Cargo.lock version to 3
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 21:55:50 +00:00
Jason Volk 1d02851028 implement several broadband loops
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 21:50:20 +00:00
Jason Volk 59d5e3ebf1 additional stream extensions for any/all
additional stream extension TryBroadbandExt

Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 21:49:19 +00:00
Jason Volk c2d97aaa5e increase default db pool worker count for large systems
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 21:49:19 +00:00
Jason Volk 513236b3ce bump ruma for async state-res optimizations
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 00:51:57 +00:00
strawberry 9db0325b42 bump rust to 1.83.0
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-12-04 00:51:57 +00:00
morguldir e0494c1538 add /bin/conduit to OCI image contents
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-12-04 00:51:57 +00:00
Jason Volk 784ccd6bad return stream from multi_get_eventid_from_short
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 00:51:57 +00:00
Jason Volk 48703173bc split get_batch from get.rs; add aqry_batch
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 00:51:57 +00:00
Jason Volk c01b049910 move cidr_range_denylist from globals to client service
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 00:51:57 +00:00
Jason Volk 9d9f403ad5 prevent adding presence timer for server's own user
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 00:51:57 +00:00
Jason Volk 3109c0daba perform async shutdown for database pool after services stop
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 00:51:57 +00:00
Jason Volk ef9b1c6303 simplify sender shutdown; prevent launching any retries
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 00:51:57 +00:00
Jason Volk b7df0a14c6 parallelize events_before and events_after in api/client/context
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-04 00:51:57 +00:00
Jason Volk b5006a4c41 offload initial iterator seeks to threadpool
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-03 13:25:33 +00:00
Jason Volk 320b0680bd pipeline various loops
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-03 13:25:33 +00:00
Jason Volk ed8c21ac9a modernize async srv lookup
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-03 13:25:33 +00:00
Jason Volk 9a9c071e82 use tokio for threadpool mgmt
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-03 07:39:02 +00:00
Jason Volk 89a158ab0b add delay before starting updates check
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-03 07:39:02 +00:00
Jason Volk 7d6710c033 add broadband stream extensions
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-03 07:39:02 +00:00
Jason Volk 61d9ac66fa add ref_at util macro
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-03 07:39:02 +00:00
Jason Volk 3b30bd3580 add try_filter_map to TryReadyExt
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-03 07:39:02 +00:00
Jason Volk 3fbd74310f impl transposed form of MapExpect
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-01 10:51:04 +00:00
Jason Volk 9263439af8 fix is_matching macro argument designator
Signed-off-by: Jason Volk <jason@zemos.net>
2024-12-01 10:51:04 +00:00
Jason Volk 4a3cc9fffa de-arc state_full_ids
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-30 08:38:12 +00:00
Jason Volk b5266ad9f5 parallelize sender edu selection
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-29 08:47:03 +00:00
Jason Volk 6175e72f1c simplify get_pdu() interface; eliminate unconditional Arc
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-29 08:47:02 +00:00
Jason Volk 58be22e695 fix new lints; clippy::unnecessary-map-or
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-29 06:58:45 +00:00
Jason Volk 2a9bb1ce11 add configurables for frontend pool options
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-28 07:20:43 +00:00
Jason Volk 3ad6aa59f9 use smallvec for db query buffering
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-28 06:03:33 +00:00
strawberry 76c75cc05a bump tracing fork
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-27 20:58:04 -05:00
Jason Volk c7ae951676 add frontend threadpool to database
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-27 10:53:44 +00:00
Jason Volk 94d7b21cf0 use stricter timeout for fetching state
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-27 06:30:20 +00:00
Jason Volk 2aeee4f509 parallel query for outlier/non-outlier pdu data
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-27 06:30:20 +00:00
Jason Volk dd8c646b63 optimize state compressor I/O w/ batch operation
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-27 06:30:20 +00:00
Jason Volk 527494a34b fix oversized tracing span arguments; lints
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-27 06:30:20 +00:00
Jason Volk e83fa12451 tweak dev profile
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-27 02:57:13 +00:00
Jason Volk 4f97ff98d6 enter the tokio runtime for the scope of main init
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-27 02:57:13 +00:00
Jason Volk f69c596f56 generalize return value wrapping to not require Arc
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-26 03:45:21 +00:00
Jason Volk 238523f177 cleanup: reuse api rather than querying db
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-26 03:45:21 +00:00
strawberry c5c74febb5 bump rust-rocksdb to 4bce1bb97d8be6f0d47245c99d465ca9cef33aad
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-25 16:32:09 -05:00
morguldir 63d1fcf213 add queued transactions rocksdb cf cache
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-25 16:08:30 -05:00
strawberry b20bd65d38 fix matrix-appservice-irc workaround
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-25 15:55:31 -05:00
Jason Volk 62d560e2fb improve tracing instruments on database::map
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-25 07:00:55 +00:00
Jason Volk 6c66391988 fix unnecessary serialization of sender query keys
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-25 06:50:15 +00:00
strawberry 6ccfc9ed98 slightly refactor appservice registration command
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-24 23:14:19 -05:00
strawberry e9fee04eef fix needlessly strict appservice user existence check
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-24 23:14:19 -05:00
strawberry 8611cc0ee9 fix ignored_filter check, exclude dummy events over sync
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-24 23:14:19 -05:00
Jason Volk 2592f83b69 add migration fix for duplicate readreceipt entries
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-25 02:54:29 +00:00
Jason Volk c903a71807 refactor and optimize receipt service data
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-25 02:54:29 +00:00
Jason Volk 343ec59a8b use arrayvec for integer deserialization buffer
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-25 02:54:29 +00:00
Jason Volk 6f1d50dda3 panic on otherwise ignored errors in debug mode
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-25 02:54:29 +00:00
strawberry 29c715a45f ci: remove some old/unnecessary paths-ignore
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-24 19:30:54 -05:00
strawberry 2675033aac send plain txt admin room error responses
fixes bracketed arguments not showing up on missing args

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-24 19:19:08 -05:00
strawberry b87362cbf1 ci: add test for validating generated example config is current
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-24 19:17:07 -05:00
Jason Volk 1c751168c6 check-in missed example config changes
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-24 23:50:05 +00:00
Jason Volk a582d0559a bump url and cargo lock
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-24 22:16:16 +00:00
Jason Volk 4e74a1811b ci: set cancel-in-progress to true
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-24 22:16:16 +00:00
Jason Volk 97ad9afc86 default to main event for batch tokens
fix prev_batch token for legacy sync timeline

Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-24 21:47:25 +00:00
Jason Volk c519a40cb8 use multiget for shortid conversions
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-24 21:30:32 +00:00
Jason Volk 3789d60b6a refactor to iterator inputs for auth_chain/short batch functions
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-24 21:30:32 +00:00
Jason Volk 5da42fb859 refactor account_data.changes_since to stream
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-24 21:30:31 +00:00
Jason Volk fd4c447a2d move attribute argument extractor to utils
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-24 07:11:26 +00:00
Jason Volk f30b08f015 fix optional config section related
split api/client well_known

simplify well_known config access

Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-24 07:11:26 +00:00
Jason Volk 5f1cab6850 passthru worker thread count from env
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-24 05:18:36 +00:00
strawberry 175e1c6453 correct admin cmd getting version and bin name
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-23 22:36:22 -05:00
strawberry af772b0240 various misc documentation improvements
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-23 22:35:54 -05:00
strawberry 3fe98f35f2 remove queued push keys on pusher deletion, use more refs
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-23 13:45:27 -05:00
strawberry 9d23a2b6f5 add missing length checks on pushkey/appid, improve error msgs for pusher
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-23 12:53:26 -05:00
strawberry f15370027e improve DNS error messages
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-23 12:05:52 -05:00
Jason Volk b94eeb9580 fix deletions on readreceipt update
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-22 09:38:34 +00:00
Jason Volk 3968d03868 move and improve common-rooms related
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-22 09:38:34 +00:00
Jason Volk aea82183b2 add set intersection util for two sorted streams
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-22 09:38:34 +00:00
Jason Volk bae0667066 limit sync response events to within the since/next_batch window
fixes #606

Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-22 09:02:01 +00:00
strawberry 5256cad396 ignore bare_urls lint for well_known client config option
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-21 23:49:46 -05:00
strawberry 9100af9974 add eventid_pdu database cf cache
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-21 23:45:16 -05:00
strawberry b6d53e97a6 bump ruwuma and a few http deps
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-21 23:44:50 -05:00
strawberry 336de49e6a tiny optimisation in append_pdu push notif
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-21 23:44:50 -05:00
emily ee3c58f78f docs: add workaround to use unix sockets with the nixos module 2024-11-20 11:14:05 -05:00
nisbet-hubbard 876c6e933c A minimal caveat 2024-11-20 09:41:11 -05:00
Jason Volk 2f2cebe84d implement local room preview
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-19 09:12:50 +00:00
Jason Volk e257512aa7 relax state visibility for invited modes
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-19 08:37:25 +00:00
Jason Volk 411c60009d enrich state iteration interface
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-19 08:37:25 +00:00
Jason Volk 7680d1bd5e replace yields point with consume_budget
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-19 08:37:25 +00:00
Jason Volk 8fedc358e0 typename additional shortids
cleanup/split state_compressor load

Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-19 08:37:25 +00:00
Jason Volk 90106c4c33 streamline batch insertions
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-19 08:37:25 +00:00
Jason Volk a05dc03100 use debug_warn for backfill event evals
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-19 08:37:25 +00:00
Jason Volk 26bcc7e312 fix default stateinfo cache size
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-17 00:15:13 +00:00
strawberry 85a6d8fc6b ci: fix github pages publish check
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-16 01:57:31 -05:00
Tamara Schmitz 2b2793fac6 docs: add note about the nixos service defaulting to sqlite
Co-authored-by: June 🍓🦴 <june@girlboss.ceo>
2024-11-16 00:18:58 -05:00
strawberry 8f14048528 ci: free up a bit of runner space safely (again)
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 23:48:55 -05:00
strawberry 7f96b2f92a nix: remove libllvm, libgcc, and llvm from OCI images as well
aarch64 OCI images love llvm??

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 23:18:12 -05:00
strawberry b92b4e043c drop hyper-util back down to 0.1.8 due to DNS issues
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 22:16:11 -05:00
strawberry 6319384072 implement GET /_matrix/client/v3/pushrules/global/
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 21:41:38 -05:00
strawberry ead9d66797 send the actual unsupported room version in join errors
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 21:28:08 -05:00
strawberry cd2c473bfe add missing fix_referencedevents_missing_sep key on fresh db creations
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 21:00:26 -05:00
Jason Volk 887ae84f1e optimize sha256 interface gather/vector inputs
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-16 00:33:40 +00:00
Jason Volk 14e3b242df add database get_batch stream wrapper
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-16 00:33:40 +00:00
Jason Volk 9f7a4a012b improve tracing/logging for state_compressor
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-16 00:33:40 +00:00
Jason Volk 5f625216aa slight optimizations for statediff
calculate with_capacity for set/get_statediff() etc

Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-16 00:33:40 +00:00
Jason Volk 20836cc3db flush=false for database-backup in read-only/secondary modes; improve error
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-16 00:33:40 +00:00
Jason Volk 59834a4b05 add is_read_only()/is_secondary() to Engine
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-16 00:33:40 +00:00
strawberry 4b652f5236 ok cargo doc
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 17:50:39 -05:00
strawberry be5a04f47c ci: install liburing-dev
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 17:09:36 -05:00
strawberry 9c95a74d56 fix getting canonical alias server for backfill
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 16:48:16 -05:00
strawberry 6b1b464abc add missing knock_restricted room type to /publicRooms
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 16:48:16 -05:00
strawberry f897b4daee ci: remove all free runner space steps due to flakiness
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 16:48:16 -05:00
strawberry 666989f74c delete trivy as lately its been terribly unreliable
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 16:48:16 -05:00
strawberry 9783bc78ba remove sentry_telemetry from default features
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 16:48:16 -05:00
strawberry c23786d37f dont try to backfill empty, private rooms
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 16:48:16 -05:00
strawberry a9c280bd4c document NAT hairpinning/loopback if needed
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 16:48:16 -05:00
strawberry c1f553cf4f bump rocksdb to v9.7.4, and ruwuma
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 16:48:11 -05:00
strawberry b4d809c681 add more checks for gh pages deployment workflow
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 09:49:54 -05:00
strawberry 3f69f2ee73 replace deprecated sha-1 crate, try to reduce some unnecessary crates/features
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 09:44:29 -05:00
strawberry dac1a01216 update generated example config
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 09:43:58 -05:00
strawberry 44a7ac0703 add debug_assert is_sorted for inline content types
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 09:41:17 -05:00
strawberry 011d44b749 add missing declared support for MSC3952
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 09:41:17 -05:00
strawberry 72fb8371f9 link to migrating from conduit on the README
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 09:41:17 -05:00
strawberry 4f0bdb5194 general misc bug fixes and slight improvements
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 09:41:17 -05:00
strawberry fd2a002480 dont build sentry or perf_measurements features for complement
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 09:41:17 -05:00
strawberry 4296d7174f add receive_ephemeral check for appservice EDU sending (if it even works)
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 09:41:17 -05:00
strawberry 4fe47903c2 misc docs changes/improvements from example config
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 09:41:17 -05:00
strawberry 08365bf5f4 update config documentation, commit generated example config
also removes the no-op/useless "database_backend" config option

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-15 09:41:17 -05:00
Jason Volk 4ec5d1e28e replace additional use tracing::
add log:: to disallowed-macros

Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-14 04:50:07 +00:00
Jason Volk e228dec4f2 add byte counting for compressed state caches
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-13 23:14:05 +00:00
Jason Volk 6ffdc1b2a6 bump serde, image, loole, termimad etc
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-13 22:59:28 +00:00
Jason Volk 004be3bf00 prepare utf-8 check bypass for database deserializer
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-13 22:59:28 +00:00
Jason Volk 77fab2c323 use ruma visibility enum in directory interface
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-13 02:38:03 +00:00
Jason Volk 68582dd868 add parallel query for current membership state
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-13 02:38:03 +00:00
Jason Volk feefa43e65 add pretty/si-unit byte size parsing/printing utils
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-13 02:38:03 +00:00
strawberry c59f474aff fixes for gh workflow
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-13 02:38:03 +00:00
Jason Volk 86694f2d1d move non-generic code out of generic; reduce codegen
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-13 02:38:03 +00:00
Jason Volk 999d731a65 move err macro visitor out-of-line; reduce codegen
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-11 22:45:44 +00:00
Jason Volk 3962333043 partially revert e507c31306
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-11 21:35:30 +00:00
Jason Volk 61174dd0d3 check if lazyset already contains user prior to querying
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-11 21:30:48 +00:00
Jason Volk e2afaa9f03 add config item for with_span_events
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-11 20:50:05 +00:00
Jason Volk 9790a6edc9 add unwrap_or_err to result
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-11 20:50:05 +00:00
Jason Volk 08a4e931a0 supplement a from_str for FmtSpan
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-11 20:50:05 +00:00
OverPhoenix 24a5ecb6b4 fix incorrect user id for non-admin invites checking 2024-11-10 22:24:35 +00:00
Jason Volk 1efc52c440 increase logging during server keys acquire
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-10 11:17:42 +00:00
Jason Volk f290d1a9c8 prevent retry for missing keys later in join process
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-10 08:47:15 +00:00
strawberry 7e087bb93c Fixes for CI
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-10 04:33:30 +00:00
Jason Volk 5e74391c6c fix config generator macro matchers
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-10 04:33:30 +00:00
Jason Volk cc86feded3 bump ruma
fixes for key type changes

Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-10 04:33:30 +00:00
Jason Volk 14fce38403 cork around send_join response processing
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-10 04:33:30 +00:00
Jason Volk 10be301646 split large notary requests into batches
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-10 04:33:29 +00:00
Jason Volk 1ce3db727f split event_handler service
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-08 09:21:42 +00:00
Jason Volk 6eba36d788 split make_body template
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-08 09:21:42 +00:00
Jason Volk f59e8af734 slight cleanup/simplifications to backfil
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-08 09:21:30 +00:00
Jason Volk 1f2e939fd5 optional arguments for timeline pdus iterations
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-08 08:22:54 +00:00
Jason Volk 13ef6dcbcf add standalone getters for shortid service
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-08 06:09:04 +00:00
Jason Volk 27966221f1 add ready_try_fold to utils
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-08 06:09:04 +00:00
Jason Volk 79c6b51860 renames for core pdu
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-08 06:09:04 +00:00
Jason Volk e507c31306 make pdu batch tokens zeroith-indexed
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-08 06:06:18 +00:00
Jason Volk f36757027e split api/client/room
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-06 21:46:20 +00:00
Jason Volk 7450c654ae add get_pdu_owned sans Arc; improve client/room/event handler
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-06 21:46:20 +00:00
Jason Volk 3ed2c17f98 move sync watcher from globals service to sync service
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-06 21:46:20 +00:00
Jason Volk 26c890d5ac skip redundant receipts on syncs
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-06 21:46:20 +00:00
Jason Volk 137e3008ea merge rooms threads data and service
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-06 21:46:20 +00:00
Jason Volk 9da523c004 refactor for stronger RawPduId type
implement standard traits for PduCount

enable serde for arrayvec

typedef various shortid's

pducount simplifications

split parts of pdu_metadata service to core/pdu and api/relations

remove some yields; improve var names/syntax

tweak types for limit timeline limit arguments

Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-06 21:46:20 +00:00
Kirill Hmelnitski 2e4d9cb37c fix thread pagination
refactor logic

increase fetch limit for first relates

apply other format

Co-authored-by: Jason Volk <jason@zemos.net>
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-04 19:25:31 +00:00
Jason Volk 78aeb620bc add broad timeout on acquire_origins keys operation
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-04 19:25:31 +00:00
Jason Volk 4a94a4c945 rename pdu/id to pdu/event_id
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-04 19:25:31 +00:00
Jason Volk 768e81741c use FnMut for ready_try_for_each extension
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 14:51:07 +00:00
Jason Volk 8d251003a2 reduce Error-related codegen; add PoisonError
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 14:51:07 +00:00
Jason Volk 52f09fdb51 add database migration for missing referencedevents separator
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 14:50:28 +00:00
Jason Volk f191b4bad4 add map_expect for stream
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 14:50:28 +00:00
Jason Volk 8742437036 wrap unimplemented ser/de branches with internal macro
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 08:03:25 +00:00
Jason Volk ba1c134689 move migrations out of globals service
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 08:03:25 +00:00
Jason Volk 1f1e2d547c optimize override ips; utilize all ips from cache
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 08:03:25 +00:00
Jason Volk f746be82c1 typename some loose u64 ShortId's
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 08:03:25 +00:00
Jason Volk 0bc6fdd589 Refactor ShortStateInfo et al to properly named structures
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 08:03:25 +00:00
Jason Volk 6b0eb7608d add Filter extension to Result
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 08:03:25 +00:00
Jason Volk e49aee61c1 consolidate and parallelize api/server access check prologues
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 08:03:25 +00:00
Jason Volk 7fcc6d11a4 de-wrap state_accessor.server_can_see_event
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 08:03:25 +00:00
Jason Volk 0eb67cfea0 additional bool extensions for Result/Option conversion
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 08:03:25 +00:00
Jason Volk 9775694423 inline database stream interface functions lt 64B
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 07:32:09 +00:00
Jason Volk a7cb1c5951 slightly optimize request signing/verifying
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 07:32:09 +00:00
Jason Volk ed76797b55 add raw_ overloads for prefix/from counting
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 07:32:09 +00:00
Jason Volk ad117641b8 add tuple-apply macro with length argument for now
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 07:32:09 +00:00
Jason Volk 1fbfc983e9 optimize FedDest::Named port
Signed-off-by: Jason Volk <jason@zemos.net>
2024-11-03 07:32:09 +00:00
strawberry 0387871063 add workaround for matrix-appservice-irc using historical localparts
see https://github.com/matrix-org/matrix-appservice-irc/issues/1780

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-02 21:20:36 -04:00
strawberry 6f37a251fb allow taking room aliases for auto_join_rooms config option
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-02 20:55:40 -04:00
strawberry 9466aeb088 remove some unnecessary debug prints on notices
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-02 18:52:25 -04:00
strawberry ee6af6c90e drop report delay response range to 2-5 secs
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-02 18:46:20 -04:00
strawberry 6cbaef2d12 always set RUST_BACKTRACE=full in OCI images
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-02 13:17:31 -04:00
strawberry 240c78e810 strong-type URL for URL previews to Url type
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-11-02 13:17:22 -04:00
strawberry 8ed9d49b73 skip new flakey complement test
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-31 14:41:35 -04:00
Jason Volk 354dc9e703 add map accessor to Database; move cork interface
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-28 20:52:52 -04:00
strawberry 567a4cb441 implement admin command to force join all local users to room
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-28 20:52:52 -04:00
strawberry c71db93e22 implement admin command to force join list of local users
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-28 20:52:52 -04:00
strawberry 0a281241ef bump few dependencies, bump ruwuma
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-28 20:52:52 -04:00
strawberry 85890ed425 remove some unnecessary HTML from admin commands
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-28 20:52:52 -04:00
strawberry 065396f8f5 better document allow_inbound_profile_lookup_federation_requests
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-28 20:52:52 -04:00
strawberry d92f2c121f document nginx needing request_uri
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-28 20:52:52 -04:00
Jason Volk 52e356d780 generate ActualDest https string on the fly
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-28 20:52:52 -04:00
Jason Volk 7a09ac81e0 split send from messages; refactor client/messages; add filters to client/context
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-28 20:52:52 -04:00
Jason Volk 6c9ecb031a re-export ruma Event trait through core pdu
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 21:38:49 +00:00
Jason Volk e7e606300f slightly simplify reqwest/hickory hooks
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 19:17:41 +00:00
Jason Volk 9787dfe77c fix clippy::ref_option
fix needless borrow

fix clippy::nonminimal_bool
2024-10-27 02:11:07 +00:00
Jason Volk 5e6dbaa27f apply room event filter to messages endpoint (#596)
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 02:11:07 +00:00
Jason Volk d281b8d3ae implement filters for search (#596)
closes #596

Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 02:11:07 +00:00
Jason Volk 21a67513f2 refactor search system
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 02:11:07 +00:00
Jason Volk f245389c02 add typedef for pdu_ids
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:11:50 +00:00
Jason Volk 1e7207c230 start an ArrayVec extension trait
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:11:50 +00:00
Jason Volk 0426f92ac0 unify database record separator constants
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:11:50 +00:00
Jason Volk 6808671751 merge search service w/ data
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:11:50 +00:00
Jason Volk b7369074d4 add RoomEventFilter matcher for PduEvent
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:11:50 +00:00
Jason Volk cf59f738b9 move macros incorrectly moved out of utils to top level
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:11:50 +00:00
Jason Volk 8742266ff0 split up core/pdu
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:11:50 +00:00
Jason Volk ee92a33a4d add some accessors to Ar for common patterns
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:11:50 +00:00
Jason Volk 60cc07134f log error for auth_chain corruption immediately
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:09:05 +00:00
Jason Volk e175b7d28d slightly cleanup prev_event eval loop
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:09:05 +00:00
Jason Volk 0e616f1d12 add event macro log wrapper suite
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:09:05 +00:00
Jason Volk 9438dc89e6 merge and resplit/cleanup appservice service
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:09:05 +00:00
Jason Volk efb28c1a99 add a Map::contains suite to db
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:09:05 +00:00
Jason Volk 49343281d4 additional bool extensions
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-27 00:09:05 +00:00
strawberry b921983a79 send room alias on pusher notification
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-26 18:50:29 -04:00
strawberry 60d84195c5 implement MSC4210, bump ruwuma
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-26 18:50:29 -04:00
strawberry d6991611f0 add require_auth_for_profile_requests config option, check endpoint metadata instead of request string
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-26 18:50:29 -04:00
strawberry 0efe24a028 remove spaces from CSP header to save a few bytes
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-26 18:50:29 -04:00
strawberry 2ce91f33af log method on tracing req spans, fix path sometimes being truncated
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-26 18:50:29 -04:00
strawberry 652b04b9b6 update conduwuit freebsd docs
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-26 18:50:29 -04:00
strawberry f29879288d document conduwuit k8s helm chart
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-26 18:50:29 -04:00
strawberry 89cc865868 bump conduwuit to 0.5.0
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-26 18:50:29 -04:00
Jason Volk aa768b5dec distill active and old keys for federation key/server response
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk c769fcc347 move core result into core utils
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 5cb0a5f676 add config generator controls via attribute metadatas
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 367d153380 add default-directives to config document comments
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 3396542168 complete the example-config generator macro
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk b08c1241a8 add some interruption points in recursive event handling to prevent shutdown hangs
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk dd6621a720 reduce unnecessary clone in pdu handler
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk b8260e0104 optimize for pdu_exists; remove a yield thing
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk ca57dc7928 optimize config denylists
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk d35376a90c aggregate receipts into single edu; dedup presence; refactor selection limits etc
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk a74461fc9a split keys_changed for stronger-type overloads
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 0e0438e1f9 further optimize presence_since iteration
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk c06f560913 add some additional database::de test cases
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 167807e0a6 de-wrapper max_fetch_prev_event; increase default config
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 0e55fa2de2 add ready_try_for_each to TryReadyExt extension utils
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk b505f0d0d7 add (back) query_trusted_key_servers_first w/ additional configuration detail
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk ac75ebee8a event_handler/timeline service cleanups
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 93130fbb85 add is_ok to futures TryExtExt utils
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 1fdcab0319 additional sync cleanup
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 828cb96ba9 split client/sync
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 55b8908894 merge rooms state_compressor service and data
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 84191656fb slightly cleanup appservice_in_room
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 0b085ea84f merge remaining rooms state_cache data and service
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 4576313a7c merge rooms user service and data
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk ed5b5d7877 merge rooms state service and data
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk d0ee4b6d25 add resolve_with_servers() to alias service; simplify api
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk b4ec1e9d3c add federation client for select high-timeout requests
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk c0939c3e9a Refactor server_keys service/interface and related callsites
Signed-off-by: Jason Volk <jason@zemos.net>
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-26 18:50:29 -04:00
Jason Volk d82ea331cf add random shuffle util
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 1a09eb0f02 use string::EMPTY; minor formatting and misc cleanups
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 89b5c4ee1c add timepoint_from_now to complement timepoint_ago in utils
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 2ed0c267eb Refactor for structured insertions
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 8258d16a94 re-scheme naming of stream iterator overloads
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 19880ce12b add IgnoreAll directive to deserializer
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk d3d11356ee add serialized insert interface
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 2f24d7117a further develop serializer for insertions
add JSON delegator to db serializer

consolidate writes through memfun; simplifications

Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk fc4d109f35 add document comments to config items
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk f67cfcd535 cleanup Config::load()
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 2a59a56eaa initial example-config generator
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk c40d20cb95 add macro util to determine if cargo build or check/clippy.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 43b0bb6a5e add non-allocating fixed-size random string generator
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk a5e85727b5 add tuple access functor-macro
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 16f82b02a0 add util to restore state on scope exit
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk c9c405facf relax Sized bound for debug::type_name
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk 8ea2dccc9a sort rustfmt
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:29 -04:00
Jason Volk e482c0646f Add constructions and Default for PduBuilder
simplify various RoomMemberEventContent constructions

Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk f503ed918c misc cleanup
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk 57e0a5f65d additional database stream deserializations for serde_json::from_ elim
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk d526db681f refactor various patterns for serde_json::from_ elim
bump ruma

Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk 55c85f6851 refactor to pdu.get_content() for serde_json::from_ elim
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk f7af6966b7 refactor to room_state_get_content() for serde_json::from_ elim
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk 68315ac112 Add state_get_content(shortid) for serde_json::from elim
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk da34b43302 abstract account-data deserializations for serde_json::from_elim
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk 48a767d52c abstract common patterns as core pdu memberfns
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk 2b2055fe8a parallelize calculate_invite_state
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk 685eadb171 add is_not_found as Error member function; tweak interface; add doc comments
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk dd9f53080a add unwrap_or to TryFutureExtExt
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk 4485f36e34 add mactors for true/false
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk a2e5c3d5d3 add FlatOk trait to Result/Option suite
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk 08a2fecc0e catch panics at base functions to integrate with other fatal errors.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk 89a3c80700 split admin-room branch from build_and_append_pdu (fixes large stack warning)
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk 56dd0f5139 use loop condition to account for loole channel close
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
Jason Volk 814b9e28b6 fix unnecessary re-serializations
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-26 18:50:28 -04:00
strawberry 8eec78e9e0 mark the server user bot as online/offline on shutdown/startup
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:45 -04:00
morguldir 9eace1fbbb fix sliding sync room type filter regression
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:45 -04:00
Jason Volk ba683cf534 fix aliasid_alias key deserialization
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:38:45 -04:00
Jason Volk bd9a9cc5f8 fix trait-solver issue requiring recursion_limit increase
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:38:45 -04:00
Jason Volk 2d049dacc3 fix get_all_media_keys deserialization
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:38:45 -04:00
Jason Volk c6b7c24e99 consume all bytes for top-level Ignore; add comments/tweaks
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:38:45 -04:00
strawberry fa7c1200b5 miniscule spaces code optimisations
still terrible though

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:45 -04:00
strawberry bd56d83045 fix room directory regression
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:45 -04:00
strawberry ab9a65db5d add MSC4151 room reporting support
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:45 -04:00
strawberry 54a107c3c4 drop unnecessary error to debug_warn
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:45 -04:00
strawberry 98363852b1 fix: dont add remote users for push targets, use hashset instead of vec
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:45 -04:00
strawberry 4eb7ad79d1 update last_seen_ip and last_seen_ts on updating device metadata
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:45 -04:00
strawberry 115ea03edf remove unnecessary full type annos
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:18 -04:00
strawberry a9e3e8f77a dont send non-state events from ignored users over /context/{eventId}
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:18 -04:00
strawberry 6a81bf23de dont send events from ignored users over /messages
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:18 -04:00
strawberry 7a59add8f1 add support for reading a registration token from a file
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:18 -04:00
strawberry ee1580e480 fix list_rooms admin command filters
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:18 -04:00
strawberry b64a235165 use ok_or_else for a rare error
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:18 -04:00
strawberry 4413793f7e dont allow sending/receiving room invites with ignored users
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:18 -04:00
strawberry 2083c38c76 dont send non-state events from ignored users over sync
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:18 -04:00
strawberry 890ee84f71 dont send read receipts and typing indicators from ignored users
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:18 -04:00
strawberry fafe320899 send EDUs to appservices if in events
to_device is not supported yet

Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:38:14 -04:00
strawberry 8311952629 bump ruma, cargo.lock, and deps
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:37:15 -04:00
Jason Volk 36677bb982 optimize auth_chain short_id to event_id translation step
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:36:30 -04:00
Jason Volk ab06701ed0 refactor multi-get to handle result type
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:36:30 -04:00
Jason Volk 26dcab272d various cleanup tweaks/fixes
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:36:30 -04:00
Jason Volk 96fcf7f94d add rocksdb secondary; fix read_only mode.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:36:30 -04:00
Jason Volk 6b80361c31 additional stream tools
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:36:30 -04:00
Jason Volk a8d5cf9651 Add rocksdb logging integration with tracing.
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:36:30 -04:00
Jason Volk c569881b08 merge rooms/short Data w/ Service; optimize queries
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:36:28 -04:00
Jason Volk 0e8ae1e13e add ArrayVec-backed serialized query overload; doc comments
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:32:33 -04:00
Jason Volk 5192927a53 split remaining map suites
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:32:33 -04:00
strawberry 4496cf2d5b add missing await to first admin room creation
Signed-off-by: strawberry <strawberry@puppygock.gay>
2024-10-25 00:32:33 -04:00
Jason Volk 3f7ec4221d minor auth_chain optimizations/cleanup
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:32:33 -04:00
Jason Volk 4776fe66c4 handle serde_json for deserialized()
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:32:33 -04:00
Jason Volk 946ca364e0 Database Refactor
combine service/users data w/ mod unit

split sliding sync related out of service/users

instrument database entry points

remove increment crap from database interface

de-wrap all database get() calls

de-wrap all database insert() calls

de-wrap all database remove() calls

refactor database interface for async streaming

add query key serializer for database

implement Debug for result handle

add query deserializer for database

add deserialization trait for option handle

start a stream utils suite

de-wrap/asyncify/type-query count_one_time_keys()

de-wrap/asyncify users count

add admin query users command suite

de-wrap/asyncify users exists

de-wrap/partially asyncify user filter related

asyncify/de-wrap users device/keys related

asyncify/de-wrap user auth/misc related

asyncify/de-wrap users blurhash

asyncify/de-wrap account_data get; merge Data into Service

partial asyncify/de-wrap uiaa; merge Data into Service

partially asyncify/de-wrap transaction_ids get; merge Data into Service

partially asyncify/de-wrap key_backups; merge Data into Service

asyncify/de-wrap pusher service getters; merge Data into Service

asyncify/de-wrap rooms alias getters/some iterators

asyncify/de-wrap rooms directory getters/iterator

partially asyncify/de-wrap rooms lazy-loading

partially asyncify/de-wrap rooms metadata

asyncify/dewrap rooms outlier

asyncify/dewrap rooms pdu_metadata

dewrap/partially asyncify rooms read receipt

de-wrap rooms search service

de-wrap/partially asyncify rooms user service

partial de-wrap rooms state_compressor

de-wrap rooms state_cache

de-wrap room state et al

de-wrap rooms timeline service

additional users device/keys related

de-wrap/asyncify sender

asyncify services

refactor database to TryFuture/TryStream

refactor services for TryFuture/TryStream

asyncify api handlers

additional asyncification for admin module

abstract stream related; support reverse streams

additional stream conversions

asyncify state-res related

Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:32:30 -04:00
Jason Volk 6001014078 add UnwrapInfallible to Result
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:15:01 -04:00
Jason Volk a5de27442a re-export crates used by error macros
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:15:01 -04:00
Jason Volk f7ce4db0b0 add is_not_found functor to error; tweak status code matcher
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:15:01 -04:00
Jason Volk a5822ebc27 add missing err! case
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:15:01 -04:00
Jason Volk 63053640f1 add util functors for is_zero/is_equal; move clamp to math utils
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:15:01 -04:00
Jason Volk bd75ff65c9 move common_elements util into unit
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:15:01 -04:00
Jason Volk aa265f7ca4 add err log trait to Result
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:15:01 -04:00
Jason Volk 3d4b0f10a5 add expected! macro to checked math expression suite
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:15:01 -04:00
Jason Volk 2709995f84 add MapExpect to Result
add DebugInspect to Result

move Result typedef into unit

Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:15:01 -04:00
Jason Volk 99ad404ea9 add str traits for split, between, unquote; consolidate tests
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:15:01 -04:00
Jason Volk 2db017af37 simplify service trait bounds and lifetimes
Signed-off-by: Jason Volk <jason@zemos.net>
2024-10-25 00:15:01 -04:00
376 changed files with 28623 additions and 20470 deletions
+25 -27
View File
@@ -3,15 +3,10 @@ name: CI and Artifacts
on:
pull_request:
push:
# documentation workflow deals with this or is not relevant for this workflow
paths-ignore:
- '*.md'
- 'conduwuit-example.toml'
- 'book.toml'
- '.gitlab-ci.yml'
- '.gitignore'
- 'renovate.json'
- 'docs/**'
- 'debian/**'
- 'docker/**'
branches:
@@ -23,7 +18,7 @@ on:
concurrency:
group: ${{ github.head_ref || github.ref_name }}
cancel-in-progress: false
cancel-in-progress: true
env:
# sccache only on main repo
@@ -65,23 +60,20 @@ permissions:
jobs:
tests:
name: Test
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
- name: Free up more runner space
- name: Install liburing
run: |
set +o pipefail
# large docker images
sudo docker image prune --all --force || true
# large packages
sudo apt-get purge -y '^llvm-.*' 'php.*' '^mongodb-.*' '^mysql-.*' azure-cli google-cloud-cli google-chrome-stable firefox powershell microsoft-edge-stable || true
sudo apt-get autoremove -y
sudo apt-get clean
# large folders
sudo rm -rf /var/lib/apt/lists/* /usr/local/games /usr/local/sqlpackage /usr/local/.ghcup /usr/local/share/powershell /usr/local/share/edge_driver /usr/local/share/gecko_driver /usr/local/share/chromium /usr/local/share/chromedriver-linux64 /usr/local/share/vcpkg /usr/local/lib/python* /usr/local/lib/node_modules /usr/local/julia* /opt/mssql-tools /etc/skel /usr/share/vim /usr/share/postgresql /usr/share/man /usr/share/apache-maven-* /usr/share/R /usr/share/alsa /usr/share/miniconda /usr/share/grub /usr/share/gradle-* /usr/share/locale /usr/share/texinfo /usr/share/kotlinc /usr/share/swift /usr/share/doc /usr/share/az_9.3.0 /usr/share/sbt /usr/share/ri /usr/share/icons /usr/share/java /usr/share/fonts /usr/lib/google-cloud-sdk /usr/lib/jvm /usr/lib/mono /usr/lib/R /usr/lib/postgresql /usr/lib/heroku /usr/lib/gcc
set -o pipefail
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 -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
@@ -231,7 +223,7 @@ jobs:
build:
name: Build
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs: tests
strategy:
matrix:
@@ -239,13 +231,10 @@ jobs:
- target: aarch64-linux-musl
- target: x86_64-linux-musl
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
- name: Sync repository
uses: actions/checkout@v4
- uses: nixbuild/nix-quick-install-action@v28
- uses: nixbuild/nix-quick-install-action@master
- name: Restore and cache Nix store
uses: nix-community/cache-nix-action@v5.1.0
@@ -450,6 +439,7 @@ jobs:
steps:
- name: Sync repository
uses: actions/checkout@v4
- name: Tag comparison check
if: ${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-rc') }}
run: |
@@ -460,14 +450,17 @@ jobs:
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
if: (github.event.pull_request.draft != true) && (vars.DOCKER_USERNAME != '') && (vars.GITLAB_USERNAME != '') && (vars.SCCACHE_ENDPOINT != '') && (github.event.pull_request.user.login != 'renovate[bot]')
uses: mozilla-actions/sccache-action@main
# use rust-cache
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
# Nix can't do portable macOS builds yet
- name: Build macOS x86_64 binary
if: ${{ matrix.os == 'macos-13' }}
@@ -475,22 +468,26 @@ jobs:
CONDUWUIT_VERSION_EXTRA="$(git rev-parse --short HEAD)" cargo build --release
cp -v -f target/release/conduit 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 --version
- name: Build macOS arm64 binary
if: ${{ matrix.os == 'macos-latest' }}
run: |
CONDUWUIT_VERSION_EXTRA="$(git rev-parse --short HEAD)" cargo build --release
cp -v -f target/release/conduit 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 --version
- name: Upload macOS x86_64 binary
if: ${{ matrix.os == 'macos-13' }}
uses: actions/upload-artifact@v4
@@ -498,6 +495,7 @@ jobs:
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
@@ -508,7 +506,7 @@ jobs:
docker:
name: Docker publish
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
needs: build
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main' || (github.event.pull_request.draft != true)) && (vars.DOCKER_USERNAME != '') && (vars.GITLAB_USERNAME != '') && github.event.pull_request.user.login != 'renovate[bot]'
env:
+12 -6
View File
@@ -39,7 +39,7 @@ concurrency:
jobs:
docs:
name: Documentation and GitHub Pages
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
pages: write
@@ -50,14 +50,20 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
- 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
- name: Setup GitHub Pages
if: github.event_name != 'pull_request'
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
@@ -139,12 +145,12 @@ jobs:
compression-level: 0
- name: Upload generated documentation (book) as GitHub Pages artifact
if: github.event_name != 'pull_request'
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: github.event_name != 'pull_request'
if: (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') && (github.event_name != 'pull_request')
id: deployment
uses: actions/deploy-pages@v4
-42
View File
@@ -1,42 +0,0 @@
name: Trivy code and vulnerability scanning
on:
pull_request:
push:
branches:
- main
tags:
- '*'
schedule:
- cron: '00 12 * * *'
permissions:
contents: read
jobs:
trivy-scan:
name: Trivy Scan
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Trivy code and vulnerability scanner on repo
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: repo
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH,MEDIUM,LOW
- name: Run Trivy code and vulnerability scanner on filesystem
uses: aquasecurity/trivy-action@0.28.0
with:
scan-type: fs
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH,MEDIUM,LOW
+2 -1
View File
@@ -131,7 +131,8 @@ allowed to be licenced under the Apache-2.0 licence and all of your conduct is
in line with the Contributor's Covenant, and conduwuit's Code of Conduct.
Contribution by users who violate either of these code of conducts will not have
their contributions accepted.
their contributions accepted. This includes users who have been banned from
conduwuit Matrix rooms for Code of Conduct violations.
[issues]: https://github.com/girlbossceo/conduwuit/issues
[conduwuit-matrix]: https://matrix.to/#/#conduwuit:puppygock.gay
Generated
+601 -259
View File
File diff suppressed because it is too large Load Diff
+81 -52
View File
@@ -19,20 +19,30 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://github.com/girlbossceo/conduwuit"
rust-version = "1.82.0"
version = "0.4.7"
rust-version = "1.83.0"
version = "0.5.0"
[workspace.metadata.crane]
name = "conduit"
[workspace.dependencies.arrayvec]
version = "0.7.4"
features = ["serde"]
[workspace.dependencies.smallvec]
version = "1.13.2"
features = [
"const_generics",
"const_new",
"serde",
"write",
]
[workspace.dependencies.const-str]
version = "0.5.7"
[workspace.dependencies.ctor]
version = "0.2.8"
version = "0.2.9"
[workspace.dependencies.cargo_toml]
version = "0.20"
@@ -45,20 +55,20 @@ default-features = false
features = ["parse"]
[workspace.dependencies.sanitize-filename]
version = "0.5.0"
version = "0.6.0"
[workspace.dependencies.jsonwebtoken]
version = "9.3.0"
default-features = false
[workspace.dependencies.base64]
version = "0.22.1"
default-features = false
# used for TURN server authentication
[workspace.dependencies.hmac]
version = "0.12.1"
[workspace.dependencies.sha-1]
version = "0.10.1"
default-features = false
# used for checking if an IP is in specific subnets / CIDR ranges easier
[workspace.dependencies.ipaddress]
@@ -69,19 +79,19 @@ version = "0.8.5"
# Used for the http request / response body type for Ruma endpoints used with reqwest
[workspace.dependencies.bytes]
version = "1.7.2"
version = "1.8.0"
[workspace.dependencies.http-body-util]
version = "0.1.1"
version = "0.1.2"
[workspace.dependencies.http]
version = "1.1.0"
[workspace.dependencies.regex]
version = "1.10.6"
version = "1.11.1"
[workspace.dependencies.axum]
version = "0.7.5"
version = "0.7.9"
default-features = false
features = [
"form",
@@ -94,7 +104,7 @@ features = [
]
[workspace.dependencies.axum-extra]
version = "0.9.3"
version = "0.9.6"
default-features = false
features = ["typed-header", "tracing"]
@@ -115,7 +125,7 @@ default-features = false
features = ["util"]
[workspace.dependencies.tower-http]
version = "0.6.0"
version = "0.6.2"
default-features = false
features = [
"add-extension",
@@ -128,10 +138,12 @@ features = [
]
[workspace.dependencies.rustls]
version = "0.23.13"
version = "0.23.16"
default-features = false
features = ["aws_lc_rs"]
[workspace.dependencies.reqwest]
version = "0.12.8"
version = "0.12.9"
default-features = false
features = [
"rustls-tls-native-roots",
@@ -141,12 +153,12 @@ features = [
]
[workspace.dependencies.serde]
version = "1.0.209"
version = "1.0.215"
default-features = false
features = ["rc"]
[workspace.dependencies.serde_json]
version = "1.0.124"
version = "1.0.133"
default-features = false
features = ["raw_value"]
@@ -170,7 +182,7 @@ default-features = false
# Used to generate thumbnails for images
[workspace.dependencies.image]
version = "0.25.1"
version = "0.25.5"
default-features = false
features = [
"jpeg",
@@ -181,16 +193,18 @@ features = [
# logging
[workspace.dependencies.log]
version = "0.4.21"
version = "0.4.22"
default-features = false
[workspace.dependencies.tracing]
version = "0.1.40"
version = "0.1.41"
default-features = false
[workspace.dependencies.tracing-subscriber]
version = "0.3.18"
features = ["env-filter"]
default-features = false
features = ["env-filter", "std", "tracing", "tracing-log", "ansi", "fmt"]
[workspace.dependencies.tracing-core]
version = "0.1.32"
version = "0.1.33"
default-features = false
# for URL previews
[workspace.dependencies.webpage]
@@ -199,23 +213,25 @@ default-features = false
# used for conduit's CLI and admin room command parsing
[workspace.dependencies.clap]
version = "4.5.20"
version = "4.5.21"
default-features = false
features = [
"std",
"derive",
"help",
"usage",
"env",
"error-context",
"help",
"std",
"string",
"usage",
]
[workspace.dependencies.futures-util]
[workspace.dependencies.futures]
version = "0.3.30"
default-features = false
features = ["std", "async-await"]
[workspace.dependencies.tokio]
version = "1.40.0"
version = "1.41.1"
default-features = false
features = [
"fs",
@@ -236,7 +252,7 @@ version = "0.8.5"
# Validating urls in config, was already a transitive dependency
[workspace.dependencies.url]
version = "2.5.0"
version = "2.5.4"
default-features = false
features = ["serde"]
@@ -247,7 +263,7 @@ features = ["alloc", "std"]
default-features = false
[workspace.dependencies.hyper]
version = "1.5.0"
version = "1.5.1"
default-features = false
features = [
"server",
@@ -256,26 +272,24 @@ features = [
]
[workspace.dependencies.hyper-util]
# 0.1.9 causes DNS issues
# hyper-util >=0.1.9 seems to have DNS issues
version = "=0.1.8"
default-features = false
features = [
"client",
"server-auto",
"server-graceful",
"service",
"tokio",
]
# to support multiple variations of setting a config option
[workspace.dependencies.either]
version = "1.11.0"
version = "1.13.0"
default-features = false
features = ["serde"]
# Used for reading the configuration from conduwuit.toml & environment variables
[workspace.dependencies.figment]
version = "0.10.18"
version = "0.10.19"
default-features = false
features = ["env", "toml"]
@@ -285,11 +299,13 @@ default-features = false
# Used for conduit::Error type
[workspace.dependencies.thiserror]
version = "1.0.63"
version = "2.0.3"
default-features = false
# Used when hashing the state
[workspace.dependencies.ring]
version = "0.17.8"
default-features = false
# Used to make working with iterators easier, was already a transitive depdendency
[workspace.dependencies.itertools]
@@ -300,12 +316,16 @@ version = "0.13.0"
[workspace.dependencies.cyborgtime]
version = "2.1.1"
# used to replace the channels of the tokio runtime
# used for MPSC channels
[workspace.dependencies.loole]
version = "0.3.1"
version = "0.4.0"
# used for MPMC channels
[workspace.dependencies.async-channel]
version = "2.3.1"
[workspace.dependencies.async-trait]
version = "0.1.81"
version = "0.1.83"
[workspace.dependencies.lru-cache]
version = "0.1.2"
@@ -314,7 +334,7 @@ version = "0.1.2"
[workspace.dependencies.ruma]
git = "https://github.com/girlbossceo/ruwuma"
#branch = "conduwuit-changes"
rev = "9900d0676564883cfade556d6e8da2a2c9061efd"
rev = "1a550585bf025cce48ef8b734339245092bc986e"
features = [
"compat",
"rand",
@@ -327,6 +347,7 @@ features = [
"server-util",
"unstable-exhaustive-types",
"ring-compat",
"compat-upload-signatures",
"identifiers-validation",
"unstable-unspecified",
"unstable-msc2409",
@@ -345,6 +366,7 @@ features = [
"unstable-msc4121",
"unstable-msc4125",
"unstable-msc4186",
"unstable-msc4210", # remove legacy mentions
"unstable-extensible-events",
]
@@ -360,9 +382,13 @@ features = [
"bzip2",
]
# optional SHA256 media keys feature
[workspace.dependencies.sha2]
version = "0.10.8"
default-features = false
[workspace.dependencies.sha1]
version = "0.10.6"
default-features = false
# optional opentelemetry, performance measurements, flamegraphs, etc for performance measurements and monitoring
[workspace.dependencies.opentelemetry]
@@ -430,7 +456,8 @@ default-features = false
features = ["resource"]
[workspace.dependencies.sd-notify]
version = "0.4.1"
version = "0.4.3"
default-features = false
[workspace.dependencies.hardened_malloc-rs]
version = "0.1.2"
@@ -446,23 +473,25 @@ version = "0.4.3"
default-features = false
[workspace.dependencies.termimad]
version = "0.30.1"
version = "0.31.1"
default-features = false
[workspace.dependencies.checked_ops]
version = "0.1"
[workspace.dependencies.syn]
version = "2.0.76"
version = "2.0.87"
default-features = false
features = ["full", "extra-traits"]
[workspace.dependencies.quote]
version = "1.0.36"
version = "1.0.37"
[workspace.dependencies.proc-macro2]
version = "1.0.89"
[workspace.dependencies.bytesize]
version = "1.3.0"
#
# Patches
@@ -473,22 +502,22 @@ version = "1.0.89"
# https://github.com/girlbossceo/tracing/commit/b348dca742af641c47bc390261f60711c2af573c
[patch.crates-io.tracing-subscriber]
git = "https://github.com/girlbossceo/tracing"
rev = "4d78a14a5e03f539b8c6b475aefa08bb14e4de91"
rev = "ccc4fbd8238c2d5ba354e61ec17ac610af11401d"
[patch.crates-io.tracing]
git = "https://github.com/girlbossceo/tracing"
rev = "4d78a14a5e03f539b8c6b475aefa08bb14e4de91"
rev = "ccc4fbd8238c2d5ba354e61ec17ac610af11401d"
[patch.crates-io.tracing-core]
git = "https://github.com/girlbossceo/tracing"
rev = "4d78a14a5e03f539b8c6b475aefa08bb14e4de91"
rev = "ccc4fbd8238c2d5ba354e61ec17ac610af11401d"
[patch.crates-io.tracing-log]
git = "https://github.com/girlbossceo/tracing"
rev = "4d78a14a5e03f539b8c6b475aefa08bb14e4de91"
rev = "ccc4fbd8238c2d5ba354e61ec17ac610af11401d"
# 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
[patch.crates-io.rustyline-async]
git = "https://github.com/girlbossceo/rustyline-async"
rev = "9654cc84e19241f6e19021eb8e677892656f5071"
rev = "deaeb0694e2083f53d363b648da06e10fc13900c"
#
# Our crates
@@ -617,7 +646,6 @@ opt-level = 0
panic = "unwind"
debug-assertions = true
incremental = true
codegen-units = 64
#rustflags = [
# '--cfg', 'conduit_mods',
# '-Ztime-passes',
@@ -659,7 +687,6 @@ incremental = false
[profile.dev.package.conduit]
inherits = "dev"
incremental = false
#rustflags = [
# '--cfg', 'conduit_mods',
# '-Ztime-passes',
@@ -771,6 +798,7 @@ unused-qualifications = "warn"
#unused-results = "warn" # TODO
## some sadness
elided_named_lifetimes = "allow" # TODO!
let_underscore_drop = "allow"
missing_docs = "allow"
# cfgs cannot be limited to expected cfgs or their de facto non-transitive/opt-in use-case e.g.
@@ -828,6 +856,7 @@ missing_panics_doc = { level = "allow", priority = 1 }
module_name_repetitions = { 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_field_names = { level = "allow", priority = 1 }
unnecessary_wraps = { level = "allow", priority = 1 }
unused_async = { level = "allow", priority = 1 }
+5 -4
View File
@@ -1,7 +1,6 @@
# conduwuit
`main`: [![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)
[![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)
<!-- ANCHOR: catchphrase -->
@@ -10,7 +9,7 @@ Artifacts](https://github.com/girlbossceo/conduwuit/actions/workflows/ci.yml/bad
<!-- ANCHOR_END: catchphrase -->
Visit the [conduwuit documentation](https://conduwuit.puppyirl.gay/) for more
information.
information and how to deploy/setup conduwuit.
<!-- ANCHOR: body -->
@@ -63,7 +62,9 @@ and we have no plans in stopping or slowing down any time soon!
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.
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.
<!-- ANCHOR_END: body -->
+1 -1
View File
@@ -18,7 +18,7 @@ RESULTS_FILE="$3"
OCI_IMAGE="complement-conduwuit:main"
# Complement tests that are skipped due to flakiness/reliability issues
SKIPPED_COMPLEMENT_TESTS='-skip=TestClientSpacesSummary.*|TestJoinFederatedRoomFromApplicationServiceBridgeUser.*|TestJumpToDateEndpoint.*'
SKIPPED_COMPLEMENT_TESTS='-skip=TestClientSpacesSummary.*|TestJoinFederatedRoomFromApplicationServiceBridgeUser.*|TestJumpToDateEndpoint.*|TestUnbanViaInvite.*'
# $COMPLEMENT_SRC needs to be a directory to Complement source code
if [ -f "$COMPLEMENT_SRC" ]; then
+9 -1
View File
@@ -2,6 +2,14 @@ array-size-threshold = 4096
cognitive-complexity-threshold = 94 # TODO reduce me ALARA
excessive-nesting-threshold = 11 # TODO reduce me to 4 or 5
future-size-threshold = 7745 # TODO reduce me ALARA
stack-size-threshold = 144000 # reduce me ALARA
stack-size-threshold = 196608 # reduce me ALARA
too-many-lines-threshold = 700 # TODO reduce me to <= 100
type-complexity-threshold = 250 # reduce me to ~200
disallowed-macros = [
{ path = "log::error", reason = "use conduit_core::error" },
{ path = "log::warn", reason = "use conduit_core::warn" },
{ path = "log::info", reason = "use conduit_core::info" },
{ path = "log::debug", reason = "use conduit_core::debug" },
{ path = "log::trace", reason = "use conduit_core::trace" },
]
+1211 -743
View File
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -1,17 +1,22 @@
# conduwuit for Debian
Information about downloading and deploying the Debian package. This may also be referenced for other `apt`-based distros such as Ubuntu.
Information about downloading and deploying the Debian package. This may also be
referenced for other `apt`-based distros such as Ubuntu.
### Installation
It is recommended to see the [generic deployment guide](../deploying/generic.md) for further information if needed as usage of the Debian package is generally related.
It is recommended to see the [generic deployment guide](../deploying/generic.md)
for further information if needed as usage of the Debian package is generally
related.
### Configuration
When installed, the example config is placed at `/etc/conduwuit/conduwuit.toml` as the default config. At the minimum, you will need to change your `server_name` here.
When installed, the example config is placed at `/etc/conduwuit/conduwuit.toml`
as the default config. The config mentions things required to be changed before
starting.
You can tweak more detailed settings by uncommenting and setting the config options
in `/etc/conduwuit/conduwuit.toml`.
You can tweak more detailed settings by uncommenting and setting the config
options in `/etc/conduwuit/conduwuit.toml`.
### Running
+1 -1
View File
@@ -27,7 +27,7 @@ malloc-usable-size = ["rust-rocksdb/malloc-usable-size"]
[dependencies.rust-rocksdb]
git = "https://github.com/girlbossceo/rust-rocksdb-zaidoon1"
rev = "c1e5523eae095a893deaf9056128c7dbc2d5fd73"
rev = "4bce1bb97d8be6f0d47245c99d465ca9cef33aad"
#branch = "master"
default-features = false
+1
View File
@@ -8,6 +8,7 @@
- [Generic](deploying/generic.md)
- [NixOS](deploying/nixos.md)
- [Docker](deploying/docker.md)
- [Kubernetes](deploying/kubernetes.md)
- [Arch Linux](deploying/arch-linux.md)
- [Debian](deploying/debian.md)
- [FreeBSD](deploying/freebsd.md)
@@ -14,9 +14,8 @@ services:
environment:
CONDUWUIT_SERVER_NAME: your.server.name.example # EDIT THIS
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit
CONDUWUIT_DATABASE_BACKEND: rocksdb
CONDUWUIT_PORT: 6167 # should match the loadbalancer traefik label
CONDUWUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
CONDUWUIT_ALLOW_REGISTRATION: 'true'
CONDUWUIT_ALLOW_FEDERATION: 'true'
CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
+1 -2
View File
@@ -30,9 +30,8 @@ services:
environment:
CONDUWUIT_SERVER_NAME: example.com # EDIT THIS
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit
CONDUWUIT_DATABASE_BACKEND: rocksdb
CONDUWUIT_PORT: 6167
CONDUWUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
CONDUWUIT_ALLOW_REGISTRATION: 'true'
CONDUWUIT_ALLOW_FEDERATION: 'true'
CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
@@ -15,7 +15,8 @@ services:
CONDUWUIT_SERVER_NAME: your.server.name.example # EDIT THIS
CONDUWUIT_TRUSTED_SERVERS: '["matrix.org"]'
CONDUWUIT_ALLOW_REGISTRATION: 'false' # After setting a secure registration token, you can enable this
CONDUWUIT_REGISTRATION_TOKEN: # This is a token you can use to register on the server
CONDUWUIT_REGISTRATION_TOKEN: "" # This is a token you can use to register on the server
#CONDUWUIT_REGISTRATION_TOKEN_FILE: "" # Alternatively you can configure a path to a token file to read
CONDUWUIT_ADDRESS: 0.0.0.0
CONDUWUIT_PORT: 6167 # you need to match this with the traefik load balancer label if you're want to change it
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit
@@ -23,7 +24,6 @@ services:
### Uncomment and change values as desired, note that conduwuit has plenty of config options, so you should check out the example example config too
# Available levels are: error, warn, info, debug, trace - more info at: https://docs.rs/env_logger/*/env_logger/#enabling-logging
# CONDUWUIT_LOG: info # default is: "warn,state_res=warn"
# CONDUWUIT_ALLOW_JAEGER: 'false'
# CONDUWUIT_ALLOW_ENCRYPTION: 'true'
# CONDUWUIT_ALLOW_FEDERATION: 'true'
# CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
@@ -31,7 +31,7 @@ services:
# CONDUWUIT_ALLOW_OUTGOING_PRESENCE: true
# CONDUWUIT_ALLOW_LOCAL_PRESENCE: true
# CONDUWUIT_WORKERS: 10
# CONDUWUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
# CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
# CONDUWUIT_NEW_USER_DISPLAYNAME_SUFFIX = "🏳<200d>⚧"
# We need some way to serve the client and server .well-known json. The simplest way is via the CONDUWUIT_WELL_KNOWN
+1 -2
View File
@@ -14,9 +14,8 @@ services:
environment:
CONDUWUIT_SERVER_NAME: your.server.name # EDIT THIS
CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit
CONDUWUIT_DATABASE_BACKEND: rocksdb
CONDUWUIT_PORT: 6167
CONDUWUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB
CONDUWUIT_ALLOW_REGISTRATION: 'true'
CONDUWUIT_ALLOW_FEDERATION: 'true'
CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
+26 -12
View File
@@ -11,9 +11,9 @@ OCI images for conduwuit are available in the registries listed below.
| Registry | Image | Size | Notes |
| --------------- | --------------------------------------------------------------- | ----------------------------- | ---------------------- |
| GitHub Registry | [ghcr.io/girlbossceo/conduwuit:latest][gh] | ![Image Size][shield-latest] | Stable tagged image. |
| GitLab Registry | [registry.gitlab.com/conduwuit/conduwuit:latest][gl] | ![Image Size][shield-latest] | Stable tagged image. |
| Docker Hub | [docker.io/girlbossceo/conduwuit:latest][dh] | ![Image Size][shield-latest] | Stable tagged image. |
| GitHub Registry | [ghcr.io/girlbossceo/conduwuit:latest][gh] | ![Image Size][shield-latest] | Stable latest tagged image. |
| GitLab Registry | [registry.gitlab.com/conduwuit/conduwuit:latest][gl] | ![Image Size][shield-latest] | Stable latest tagged image. |
| Docker Hub | [docker.io/girlbossceo/conduwuit:latest][dh] | ![Image Size][shield-latest] | Stable latest tagged image. |
| GitHub Registry | [ghcr.io/girlbossceo/conduwuit:main][gh] | ![Image Size][shield-main] | Stable main branch. |
| GitLab Registry | [registry.gitlab.com/conduwuit/conduwuit:main][gl] | ![Image Size][shield-main] | Stable main branch. |
| Docker Hub | [docker.io/girlbossceo/conduwuit:main][dh] | ![Image Size][shield-main] | Stable main branch. |
@@ -40,7 +40,6 @@ When you have the image you can simply run it with
docker run -d -p 8448:6167 \
-v db:/var/lib/conduwuit/ \
-e CONDUWUIT_SERVER_NAME="your.server.name" \
-e CONDUWUIT_DATABASE_BACKEND="rocksdb" \
-e CONDUWUIT_ALLOW_REGISTRATION=false \
--name conduit $LINK
```
@@ -93,16 +92,28 @@ Additional info about deploying conduwuit can be found [here](generic.md).
### Build
To build the conduwuit image with docker-compose, you first need to open and
modify the `docker-compose.yml` file. There you need to comment the `image:`
option and uncomment the `build:` option. Then call docker compose with:
Official conduwuit images are built using Nix's
[`buildLayeredImage`][nix-buildlayeredimage]. This ensures all OCI images are
repeatable and reproducible by anyone, keeps the images lightweight, and can be
built offline.
```bash
docker compose up
```
This also ensures portability of our images because `buildLayeredImage` builds
OCI images, not Docker images, and works with other container software.
This will also start the container right afterwards, so if want it to run in
detached mode, you also should use the `-d` flag.
The OCI images are OS-less with only a very minimal environment of the `tini`
init system, CA certificates, and the conduwuit binary. This does mean there is
not a shell, but in theory you can get a shell by adding the necessary layers
to the layered image. However it's very unlikely you will need a shell for any
real troubleshooting.
The flake file for the OCI image definition is at [`nix/pkgs/oci-image/default.nix`][oci-image-def].
To build an OCI image using Nix, the following outputs can be built:
- `nix build -L .#oci-image` (default features, x86_64 glibc)
- `nix build -L .#oci-image-x86_64-linux-musl` (default features, x86_64 musl)
- `nix build -L .#oci-image-aarch64-linux-musl` (default features, aarch64 musl)
- `nix build -L .#oci-image-x86_64-linux-musl-all-features` (all features, x86_64 musl)
- `nix build -L .#oci-image-aarch64-linux-musl-all-features` (all features, aarch64 musl)
### Run
@@ -137,3 +148,6 @@ those two files.
## Voice communication
See the [TURN](../turn.md) page.
[nix-buildlayeredimage]: https://ryantm.github.io/nixpkgs/builders/images/dockertools/#ssec-pkgs-dockerTools-buildLayeredImage
[oci-image-def]: https://github.com/girlbossceo/conduwuit/blob/main/nix/pkgs/oci-image/default.nix
+2 -8
View File
@@ -1,11 +1,5 @@
# conduwuit for FreeBSD
conduwuit at the moment does not provide FreeBSD builds. Building conduwuit on
FreeBSD requires a specific environment variable to use the system prebuilt
RocksDB library instead of rust-rocksdb / rust-librocksdb-sys which does *not*
work and will cause a build error or coredump.
conduwuit at the moment does not provide FreeBSD builds or have FreeBSD packaging, however conduwuit does build and work on FreeBSD using the system-provided RocksDB.
Use the following environment variable: `ROCKSDB_LIB_DIR=/usr/local/lib`
Such example commandline with it can be: `ROCKSDB_LIB_DIR=/usr/local/lib cargo
build --release`
Contributions for getting conduwuit packaged are welcome.
+38 -10
View File
@@ -42,6 +42,9 @@ replace the binary / container image / etc.
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.
@@ -51,13 +54,13 @@ While conduwuit can run as any user it is better to use dedicated users for
different services. This also allows you to make sure that the file permissions
are correctly set up.
In Debian or Fedora/RHEL, you can use this command to create a conduwuit user:
In Debian, you can use this command to create a conduwuit user:
```bash
sudo adduser --system conduwuit --group --disabled-login --no-create-home
```
For distros without `adduser`:
For distros without `adduser` (or where it's a symlink to `useradd`):
```bash
sudo useradd -r --shell /usr/bin/nologin --no-create-home conduwuit
@@ -65,13 +68,25 @@ sudo useradd -r --shell /usr/bin/nologin --no-create-home conduwuit
## Forwarding ports in the firewall or the router
conduwuit uses the ports 443 and 8448 both of which need to be open in the
firewall.
Matrix's default federation port is port 8448, and clients must be using port 443.
If you would like to use only port 443, or a different port, you will need to setup
delegation. conduwuit has config options for doing delegation, or you can configure
your reverse proxy to manually serve the necessary JSON files to do delegation
(see the `[global.well_known]` config section).
If conduwuit runs behind a router or in a container and has a different public
IP address than the host system these public ports need to be forwarded directly
or indirectly to the port mentioned in the config.
Note for NAT users; if you have trouble connecting to your server from the inside
of your network, you need to research your router and see if it supports "NAT
hairpinning" or "NAT loopback".
If your router does not support this feature, you need to research doing local
DNS overrides and force your Matrix DNS records to use your local IP internally.
This can be done at the host level using `/etc/hosts`. If you need this to be
on the network level, consider something like NextDNS or Pi-Hole.
## Setting up a systemd service
The systemd unit for conduwuit can be found
@@ -119,12 +134,16 @@ is the recommended reverse proxy for new users and is very trivial to use
(handles TLS, reverse proxy headers, etc transparently with proper defaults).
Lighttpd is not supported as it seems to mess with the `X-Matrix` Authorization
header, making federation non-functional. If using Apache, you need to use
`nocanon` in your `ProxyPass` directive to prevent this (note that Apache
isn't very good as a general reverse proxy).
header, making federation non-functional. If a workaround is found, feel free to share to get it added to the documentation here.
Nginx users may need to set `proxy_buffering off;` if there are issues with
uploading media like images.
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).
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;`
Nginx users need to increase `client_max_body_size` (default is 1M) to match
`max_request_size` defined in conduwuit.toml.
You will need to reverse proxy everything under following routes:
- `/_matrix/` - core Matrix C-S and S-S APIs
@@ -133,11 +152,20 @@ You will need to reverse proxy everything under following routes:
You can optionally reverse proxy the following individual routes:
- `/.well-known/matrix/client` and `/.well-known/matrix/server` if using
conduwuit to perform delegation
conduwuit to perform delegation (see the `[global.well_known]` config section)
- `/.well-known/matrix/support` if using conduwuit to send the homeserver admin
contact and support page (formerly known as MSC1929)
- `/` if you would like to see `hewwo from conduwuit woof!` at the root
See the following spec pages for more details on these files:
- [`/.well-known/matrix/server`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixserver)
- [`/.well-known/matrix/client`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient)
- [`/.well-known/matrix/support`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixsupport)
Examples of delegation:
- <https://puppygock.gay/.well-known/matrix/server>
- <https://puppygock.gay/.well-known/matrix/client>
### Caddy
Create `/etc/caddy/conf.d/conduwuit_caddyfile` and enter this (substitute for
+8
View File
@@ -0,0 +1,8 @@
# conduwuit for Kubernetes
conduwuit doesn't support horizontal scalability or distributed loading
natively, however a community maintained Helm Chart is available here to run
conduwuit on Kubernetes: <https://gitlab.cronce.io/charts/conduwuit>
Should changes need to be made, please reach out to the maintainer in our
Matrix room as this is not maintained/controlled by the conduwuit maintainers.
+30 -5
View File
@@ -39,6 +39,15 @@ The `flake.nix` and `default.nix` do not currently provide a NixOS module (contr
welcome!), so [`services.matrix-conduit`][module] from Nixpkgs can be used to configure
conduwuit.
### Conduit NixOS Config Module and SQLite
Beware! The [`services.matrix-conduit`][module] module defaults to SQLite as a database backend.
Conduwuit dropped SQLite support in favor of exclusively supporting the much faster RocksDB.
Make sure that you are using the RocksDB backend before migrating!
There is a [tool to migrate a Conduit SQLite database to
RocksDB](https://github.com/ShadowJonathan/conduit_toolbox/).
If you want to run the latest code, you should get conduwuit from the `flake.nix`
or `default.nix` and set [`services.matrix-conduit.package`][package]
appropriately to use conduwuit instead of Conduit.
@@ -46,15 +55,31 @@ appropriately to use conduwuit instead of Conduit.
### UNIX sockets
Due to the lack of a conduwuit NixOS module, when using the `services.matrix-conduit` module
it is not possible to use UNIX sockets. This is because the UNIX socket option does not exist
in Conduit, and their module forces listening on `[::1]:6167` by default if unspecified.
a workaround like the one below is necessary to use UNIX sockets. This is because the UNIX
socket option does not exist in Conduit, and the module forcibly sets the `address` and
`port` config options.
```nix
options.services.matrix-conduit.settings = lib.mkOption {
apply = old: old // (
if (old.global ? "unix_socket_path")
then { global = builtins.removeAttrs old.global [ "address" "port" ]; }
else { }
);
};
```
Additionally, the [`matrix-conduit` systemd unit][systemd-unit] in the module does not allow
the `AF_UNIX` socket address family in their systemd unit's `RestrictAddressFamilies=` which
disallows the namespace from accessing or creating UNIX sockets.
disallows the namespace from accessing or creating UNIX sockets and has to be enabled like so:
There is no known workaround these. A conduwuit NixOS configuration module must be developed and
published by the community.
```nix
systemd.services.conduit.serviceConfig.RestrictAddressFamilies = [ "AF_UNIX" ];
```
Even though those workarounds are feasible a conduwuit NixOS configuration module, developed and
published by the community, would be appreciated.
### jemalloc and hardened profile
+14 -10
View File
@@ -75,21 +75,21 @@ development (unresponsive or slow upstream), conduwuit-specific usecases, or
lack of time to upstream some things.
- [ruma/ruma][1]: <https://github.com/girlbossceo/ruwuma> - various performance
improvements, more features, faster-paced development, client/server interop
improvements, more features, faster-paced development, better client/server interop
hacks upstream won't accept, etc
- [facebook/rocksdb][2]: <https://github.com/girlbossceo/rocksdb> - liburing
build fixes, GCC build fix, and logging callback C API for Rust tracing
integration
build fixes and GCC debug build fix
- [tikv/jemallocator][3]: <https://github.com/girlbossceo/jemallocator> - musl
builds seem to be broken on upstream
builds seem to be broken on upstream, fixes some broken/suspicious code in
places, additional safety measures, and support redzones for Valgrind
- [zyansheep/rustyline-async][4]:
<https://github.com/girlbossceo/rustyline-async> - tab completion callback and
`CTRL+\` signal quit event for CLI
`CTRL+\` signal quit event for conduwuit console CLI
- [rust-rocksdb/rust-rocksdb][5]:
<https://github.com/girlbossceo/rust-rocksdb-zaidoon1> - [`@zaidoon1`'s][8] fork
has quicker updates, more up to date dependencies. Our changes fix musl build
issues, Rust part of the logging callback C API, removes unnecessary `gtest`
include, and uses our RocksDB and jemallocator
<https://github.com/girlbossceo/rust-rocksdb-zaidoon1> - [`@zaidoon1`][8]'s fork
has quicker updates, more up to date dependencies, etc. Our fork fixes musl build
issues, removes unnecessary `gtest` include, and uses our RocksDB and jemallocator
forks.
- [tokio-rs/tracing][6]: <https://github.com/girlbossceo/tracing> - Implements
`Clone` for `EnvFilter` to support dynamically changing tracing envfilter's
alongside other logging/metrics things
@@ -103,12 +103,16 @@ tokio_unstable` flag to enable experimental tokio APIs. A build might look like
this:
```bash
RUSTFLAGS="--cfg tokio_unstable" cargo build \
RUSTFLAGS="--cfg tokio_unstable" cargo +nightly build \
--release \
--no-default-features \
--features=systemd,element_hacks,gzip_compression,brotli_compression,zstd_compression,tokio_console
```
You will also need to enable the `tokio_console` config option in conduwuit when
starting it. This was due to tokio-console causing gradual memory leak/usage
if left enabled.
[1]: https://github.com/ruma/ruma/
[2]: https://github.com/facebook/rocksdb/
[3]: https://github.com/tikv/jemallocator/
+2 -2
View File
@@ -5,8 +5,8 @@
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, 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
+1 -2
View File
@@ -241,8 +241,7 @@ 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/),
[Trivy](https://github.com/aquasecurity/trivy-action), and keeps ALL
- 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
+37 -14
View File
@@ -47,10 +47,11 @@ and communicate with your host's DNS servers (host's `/etc/resolv.conf`)
Some filesystems may not like RocksDB using [Direct
IO](https://github.com/facebook/rocksdb/wiki/Direct-IO). Direct IO is for
non-buffered I/O which improves conduwuit performance, but at least FUSE is a
filesystem potentially known to not like this. See the [example
config](configuration/examples.md) for disabling it if needed. Issues from
Direct IO on unsupported filesystems are usually shown as startup errors.
non-buffered I/O which improves conduwuit performance and reduces system CPU
usage, but at least FUSE and possibly ZFS are filesystems potentially known
to not like this. See the [example config](configuration/examples.md) for
disabling it if needed. Issues from Direct IO on unsupported filesystems are
usually shown as startup errors.
#### Database corruption
@@ -105,24 +106,46 @@ Various debug commands can be found in `!admin debug`.
#### Debug/Trace log level
conduwuit builds without debug or trace log levels by default for at least
performance reasons. This may change in the future and/or binaries providing
such configurations may be provided. If you need to access debug/trace log
levels, you will need to build without the `release_max_log_level` feature.
conduwuit builds without debug or trace log levels at compile time by default
for substantial performance gains in CPU usage and improved compile times. If
you need to access debug/trace log levels, you will need to build without the
`release_max_log_level` feature or use our provided static debug binaries.
#### Changing log level dynamically
conduwuit supports changing the tracing log environment filter on-the-fly using
the admin command `!admin debug change-log-level`. This accepts a string
**without quotes** the same format as the `log` config option.
the admin command `!admin debug change-log-level <log env filter>`. This accepts
a string **without quotes** the same format as the `log` config option.
Example: `!admin debug change-log-level debug`
This can also accept complex filters such as:
`!admin debug change-log-level info,conduit_service[{dest="example.com"}]=trace,ruma_state_res=trace`
`!admin debug change-log-level info,conduit_service[{dest="example.com"}]=trace,conduit_service[send{dest="example.org"}]=trace`
And to reset the log level to the one that was set at startup / last config
load, simply pass the `--reset` flag.
`!admin debug change-log-level --reset`
#### Pinging servers
conduwuit can ping other servers using `!admin debug ping`. This takes a server
name and goes through the server discovery process and queries
conduwuit can ping other servers using `!admin debug ping <server>`. This takes
a server name and goes through the server discovery process and queries
`/_matrix/federation/v1/version`. Errors are outputted.
While it does measure the latency of the request, it is not indicative of
server performance on either side as that endpoint is completely unauthenticated
and simply fetches a string on a static JSON endpoint. It is very low cost both
bandwidth and computationally.
#### Allocator memory stats
When using jemalloc with jemallocator's `stats` feature, you can see conduwuit's
jemalloc memory stats by using `!admin debug memory-stats`
When using jemalloc with jemallocator's `stats` feature (`--enable-stats`), you
can see conduwuit's high-level allocator stats by using
`!admin server memory-usage` at the bottom.
If you are a developer, you can also view the raw jemalloc statistics with
`!admin debug memory-stats`. Please note that this output is extremely large
which may only be visible in the conduwuit console CLI due to PDU size limits,
and is not easy for non-developers to understand.
+10
View File
@@ -188,6 +188,16 @@ cargo test \
--color=always
"""
# Checks if the generated example config differs from the checked in repo's
# example config.
[[task]]
name = "example-config"
group = "tests"
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
Generated
+4 -4
View File
@@ -922,16 +922,16 @@
"rocksdb": {
"flake": false,
"locked": {
"lastModified": 1729712930,
"narHash": "sha256-jlp4kPkRTpoJaUdobEoHd8rCGAQNBy4ZHZ6y5zL/ibw=",
"lastModified": 1731690620,
"narHash": "sha256-Xd4TJYqPERMJLXaGa6r6Ny1Wlw8Uy5Cyf/8q7nS58QM=",
"owner": "girlbossceo",
"repo": "rocksdb",
"rev": "871eda6953c3f399aae39808dcfccdd014885beb",
"rev": "292446aa2bc41699204d817a1e4b091679a886eb",
"type": "github"
},
"original": {
"owner": "girlbossceo",
"ref": "v9.7.3",
"ref": "v9.7.4",
"repo": "rocksdb",
"type": "github"
}
+2 -2
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.7.3"; flake = false; };
rocksdb = { url = "github:girlbossceo/rocksdb?ref=v9.7.4"; flake = false; };
liburing = { url = "github:axboe/liburing?ref=master"; flake = false; };
};
@@ -27,7 +27,7 @@
file = ./rust-toolchain.toml;
# See also `rust-toolchain.toml`
sha256 = "sha256-yMuSb5eQPO/bHv+Bcf/US8LVMbf/G/0MSfiPwBhiPpk=";
sha256 = "sha256-s1RPtyvDGJaX/BisLT+ifVfuhDT1nZkZ1NcK8sbwELM=";
};
mkScope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
+2
View File
@@ -25,6 +25,7 @@ let
"tokio_console"
# sentry telemetry isn't useful for complement, disabled by default anyways
"sentry_telemetry"
"perf_measurements"
# the containers don't use or need systemd signal support
"systemd"
# this is non-functional on nix for some reason
@@ -96,6 +97,7 @@ dockerTools.buildImage {
Env = [
"SSL_CERT_FILE=/complement/ca/ca.crt"
"CONDUWUIT_CONFIG=${./config.toml}"
"RUST_BACKTRACE=full"
];
ExposedPorts = {
+1 -1
View File
@@ -176,7 +176,7 @@ commonAttrs = {
#
# <https://github.com/input-output-hk/haskell.nix/issues/829>
postInstall = with pkgsBuildHost; ''
find "$out" -type f -exec remove-references-to -t ${stdenv.cc} -t ${gcc} -t ${rustc.unwrapped} -t ${rustc} -t ${libidn2} -t ${libunistring} '{}' +
find "$out" -type f -exec remove-references-to -t ${stdenv.cc} -t ${gcc} -t ${libgcc} -t ${llvm} -t ${libllvm} -t ${rustc.unwrapped} -t ${rustc} -t ${libidn2} -t ${libunistring} '{}' +
'';
};
in
+4
View File
@@ -14,6 +14,7 @@ dockerTools.buildLayeredImage {
created = "@${toString inputs.self.lastModified}";
contents = [
dockerTools.caCertificates
main
];
config = {
Entrypoint = if !stdenv.hostPlatform.isDarwin
@@ -24,5 +25,8 @@ dockerTools.buildLayeredImage {
Cmd = [
"${lib.getExe main}"
];
Env = [
"RUST_BACKTRACE=full"
];
};
}
+1 -1
View File
@@ -9,7 +9,7 @@
# If you're having trouble making the relevant changes, bug a maintainer.
[toolchain]
channel = "1.82.0"
channel = "1.83.0"
profile = "minimal"
components = [
# For rust-analyzer
+14 -15
View File
@@ -1,28 +1,27 @@
edition = "2021"
array_width = 80
chain_width = 60
comment_width = 80
condense_wildcard_suffixes = true
edition = "2021"
fn_call_width = 80
fn_params_layout = "Compressed"
fn_single_line = true
format_code_in_doc_comments = true
format_macro_bodies = true
format_macro_matchers = true
format_strings = true
hex_literal_case = "Upper"
max_width = 120
tab_spaces = 4
array_width = 80
comment_width = 80
wrap_comments = true
fn_params_layout = "Compressed"
fn_call_width = 80
fn_single_line = true
group_imports = "StdExternalCrate"
hard_tabs = true
match_block_trailing_comma = true
hex_literal_case = "Upper"
imports_granularity = "Crate"
match_block_trailing_comma = true
max_width = 120
newline_style = "Unix"
normalize_comments = false
reorder_impl_items = true
reorder_imports = true
group_imports = "StdExternalCrate"
newline_style = "Unix"
tab_spaces = 4
use_field_init_shorthand = true
use_small_heuristics = "Off"
use_try_shorthand = true
chain_width = 60
wrap_comments = true
+2 -1
View File
@@ -29,10 +29,11 @@ release_max_log_level = [
clap.workspace = true
conduit-api.workspace = true
conduit-core.workspace = true
conduit-database.workspace = true
conduit-macros.workspace = true
conduit-service.workspace = true
const-str.workspace = true
futures-util.workspace = true
futures.workspace = true
log.workspace = true
ruma.workspace = true
serde_json.workspace = true
+1 -1
View File
@@ -9,7 +9,7 @@ use crate::{
};
#[derive(Debug, Parser)]
#[command(name = "admin", version = env!("CARGO_PKG_VERSION"))]
#[command(name = "conduwuit", version = conduit::version())]
pub(super) enum AdminCommand {
#[command(subcommand)]
/// - Commands for managing appservices
+12 -6
View File
@@ -11,19 +11,25 @@ pub(super) async fn register(&self) -> Result<RoomMessageEventContent> {
));
}
let appservice_config = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
let parsed_config = serde_yaml::from_str::<Registration>(&appservice_config);
let appservice_config_body = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
let parsed_config = serde_yaml::from_str::<Registration>(&appservice_config_body);
match parsed_config {
Ok(yaml) => match self.services.appservice.register_appservice(yaml).await {
Ok(id) => Ok(RoomMessageEventContent::text_plain(format!(
"Appservice registered with ID: {id}."
Ok(registration) => match self
.services
.appservice
.register_appservice(&registration, &appservice_config_body)
.await
{
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!(
"Appservice registered with ID: {}",
registration.id
))),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Failed to register appservice: {e}"
))),
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Could not parse appservice config: {e}"
"Could not parse appservice config as YAML: {e}"
))),
}
}
+4 -5
View File
@@ -1,5 +1,6 @@
use conduit::Result;
use conduit_macros::implement;
use futures::StreamExt;
use ruma::events::room::message::RoomMessageEventContent;
use crate::Command;
@@ -10,14 +11,12 @@ use crate::Command;
#[implement(Command, params = "<'_>")]
pub(super) async fn check_all_users(&self) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let results = self.services.users.db.iter();
let users = self.services.users.iter().collect::<Vec<_>>().await;
let query_time = timer.elapsed();
let users = results.collect::<Vec<_>>();
let total = users.len();
let err_count = users.iter().filter(|user| user.is_err()).count();
let ok_count = users.iter().filter(|user| user.is_ok()).count();
let err_count = users.iter().filter(|_user| false).count();
let ok_count = users.iter().filter(|_user| true).count();
let message = format!(
"Database query completed in {query_time:?}:\n\n```\nTotal entries: {total:?}\nFailure/Invalid user count: \
+167 -139
View File
@@ -1,18 +1,19 @@
use std::{
collections::{BTreeMap, HashMap},
collections::HashMap,
fmt::Write,
iter::once,
sync::Arc,
time::{Instant, SystemTime},
};
use api::client::validate_and_add_event_id;
use conduit::{debug, debug_error, err, info, trace, utils, warn, Error, PduEvent, Result};
use conduit::{debug_error, err, info, trace, utils, utils::string::EMPTY, warn, Error, PduEvent, Result};
use futures::{FutureExt, StreamExt};
use ruma::{
api::{client::error::ErrorKind, federation::event::get_room_state},
events::room::message::RoomMessageEventContent,
CanonicalJsonObject, EventId, OwnedRoomOrAliasId, RoomId, RoomVersionId, ServerName,
};
use tokio::sync::RwLock;
use service::rooms::state_compressor::HashSetCompressStateEvent;
use tracing_subscriber::EnvFilter;
use crate::admin_command;
@@ -26,37 +27,39 @@ pub(super) async fn echo(&self, message: Vec<String>) -> Result<RoomMessageEvent
#[admin_command]
pub(super) async fn get_auth_chain(&self, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
let event_id = Arc::<EventId>::from(event_id);
if let Some(event) = self.services.rooms.timeline.get_pdu_json(&event_id)? {
let room_id_str = event
.get("room_id")
.and_then(|val| val.as_str())
.ok_or_else(|| Error::bad_database("Invalid event in database"))?;
let Ok(event) = self.services.rooms.timeline.get_pdu_json(&event_id).await else {
return Ok(RoomMessageEventContent::notice_plain("Event not found."));
};
let room_id = <&RoomId>::try_from(room_id_str)
.map_err(|_| Error::bad_database("Invalid room id field in event in database"))?;
let room_id_str = event
.get("room_id")
.and_then(|val| val.as_str())
.ok_or_else(|| Error::bad_database("Invalid event in database"))?;
let start = Instant::now();
let count = self
.services
.rooms
.auth_chain
.event_ids_iter(room_id, vec![event_id])
.await?
.count();
let room_id = <&RoomId>::try_from(room_id_str)
.map_err(|_| Error::bad_database("Invalid room id field in event in database"))?;
let elapsed = start.elapsed();
Ok(RoomMessageEventContent::text_plain(format!(
"Loaded auth chain with length {count} in {elapsed:?}"
)))
} else {
Ok(RoomMessageEventContent::text_plain("Event not found."))
}
let start = Instant::now();
let count = self
.services
.rooms
.auth_chain
.event_ids_iter(room_id, once(event_id.as_ref()))
.await?
.count()
.await;
let elapsed = start.elapsed();
Ok(RoomMessageEventContent::text_plain(format!(
"Loaded auth chain with length {count} in {elapsed:?}"
)))
}
#[admin_command]
pub(super) async fn parse_pdu(&self) -> Result<RoomMessageEventContent> {
if self.body.len() < 2 || !self.body[0].trim().starts_with("```") || self.body.last().unwrap_or(&"").trim() != "```"
if self.body.len() < 2
|| !self.body[0].trim().starts_with("```")
|| self.body.last().unwrap_or(&EMPTY).trim() != "```"
{
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
@@ -91,25 +94,28 @@ pub(super) async fn get_pdu(&self, event_id: Box<EventId>) -> Result<RoomMessage
.services
.rooms
.timeline
.get_non_outlier_pdu_json(&event_id)?;
if pdu_json.is_none() {
.get_non_outlier_pdu_json(&event_id)
.await;
if pdu_json.is_err() {
outlier = true;
pdu_json = self.services.rooms.timeline.get_pdu_json(&event_id)?;
pdu_json = self.services.rooms.timeline.get_pdu_json(&event_id).await;
}
match pdu_json {
Some(json) => {
Ok(json) => {
let json_text = serde_json::to_string_pretty(&json).expect("canonical json is valid json");
Ok(RoomMessageEventContent::notice_markdown(format!(
"{}\n```json\n{}\n```",
if outlier {
"Outlier PDU found in our database"
"Outlier (Rejected / Soft Failed) PDU found in our database"
} else {
"PDU found in our database"
},
json_text
)))
},
None => Ok(RoomMessageEventContent::text_plain("PDU not found locally.")),
Err(_) => Ok(RoomMessageEventContent::text_plain("PDU not found locally.")),
}
}
@@ -130,7 +136,9 @@ pub(super) async fn get_remote_pdu_list(
));
}
if self.body.len() < 2 || !self.body[0].trim().starts_with("```") || self.body.last().unwrap_or(&"").trim() != "```"
if self.body.len() < 2
|| !self.body[0].trim().starts_with("```")
|| self.body.last().unwrap_or(&EMPTY).trim() != "```"
{
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
@@ -157,7 +165,8 @@ pub(super) async fn get_remote_pdu_list(
.send_message(RoomMessageEventContent::text_plain(format!(
"Failed to get remote PDU, ignoring error: {e}"
)))
.await;
.await
.ok();
warn!("Failed to get remote PDU, ignoring error: {e}");
} else {
success_count = success_count.saturating_add(1);
@@ -196,6 +205,7 @@ pub(super) async fn get_remote_pdu(
&server,
ruma::api::federation::event::get_event::v1::Request {
event_id: event_id.clone().into(),
include_unredacted_content: None,
},
)
.await
@@ -210,12 +220,14 @@ pub(super) async fn get_remote_pdu(
})?;
trace!("Attempting to parse PDU: {:?}", &response.pdu);
let parsed_pdu = {
let _parsed_pdu = {
let parsed_result = self
.services
.rooms
.event_handler
.parse_incoming_pdu(&response.pdu);
.parse_incoming_pdu(&response.pdu)
.await;
let (event_id, value, room_id) = match parsed_result {
Ok(t) => t,
Err(e) => {
@@ -230,22 +242,12 @@ pub(super) async fn get_remote_pdu(
vec![(event_id, value, room_id)]
};
let pub_key_map = RwLock::new(BTreeMap::new());
debug!("Attempting to fetch homeserver signing keys for {server}");
self.services
.server_keys
.fetch_required_signing_keys(parsed_pdu.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map)
.await
.unwrap_or_else(|e| {
warn!("Could not fetch all signatures for PDUs from {server}: {e:?}");
});
info!("Attempting to handle event ID {event_id} as backfilled PDU");
self.services
.rooms
.timeline
.backfill_pdu(&server, response.pdu, &pub_key_map)
.backfill_pdu(&server, response.pdu)
.boxed()
.await?;
let json_text = serde_json::to_string_pretty(&json).expect("canonical json is valid json");
@@ -264,15 +266,15 @@ pub(super) async fn get_remote_pdu(
#[admin_command]
pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result<RoomMessageEventContent> {
let room_id = self.services.rooms.alias.resolve(&room).await?;
let room_state = self
let room_state: Vec<_> = self
.services
.rooms
.state_accessor
.room_state_full(&room_id)
.await?
.values()
.map(|pdu| pdu.to_state_event())
.collect::<Vec<_>>();
.map(PduEvent::to_state_event)
.collect();
if room_state.is_empty() {
return Ok(RoomMessageEventContent::text_plain(
@@ -333,9 +335,12 @@ pub(super) async fn ping(&self, server: Box<ServerName>) -> Result<RoomMessageEv
#[admin_command]
pub(super) async fn force_device_list_updates(&self) -> Result<RoomMessageEventContent> {
// Force E2EE device list updates for all users
for user_id in self.services.users.iter().filter_map(Result::ok) {
self.services.users.mark_device_key_update(&user_id)?;
}
self.services
.users
.stream()
.for_each(|user_id| self.services.users.mark_device_key_update(user_id))
.await;
Ok(RoomMessageEventContent::text_plain(
"Marked all devices for all users as having new keys to update",
))
@@ -419,12 +424,10 @@ pub(super) async fn sign_json(&self) -> Result<RoomMessageEventContent> {
let string = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
match serde_json::from_str(&string) {
Ok(mut value) => {
ruma::signatures::sign_json(
self.services.globals.server_name().as_str(),
self.services.globals.keypair(),
&mut value,
)
.expect("our request json is what ruma expects");
self.services
.server_keys
.sign_json(&mut value)
.expect("our request json is what ruma expects");
let json_text = serde_json::to_string_pretty(&value).expect("canonical json is valid json");
Ok(RoomMessageEventContent::text_plain(json_text))
},
@@ -442,27 +445,31 @@ pub(super) async fn verify_json(&self) -> Result<RoomMessageEventContent> {
}
let string = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
match serde_json::from_str(&string) {
Ok(value) => {
let pub_key_map = RwLock::new(BTreeMap::new());
self.services
.server_keys
.fetch_required_signing_keys([&value], &pub_key_map)
.await?;
let pub_key_map = pub_key_map.read().await;
match ruma::signatures::verify_json(&pub_key_map, &value) {
Ok(()) => Ok(RoomMessageEventContent::text_plain("Signature correct")),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Signature verification failed: {e}"
))),
}
match serde_json::from_str::<CanonicalJsonObject>(&string) {
Ok(value) => match self.services.server_keys.verify_json(&value, None).await {
Ok(()) => Ok(RoomMessageEventContent::text_plain("Signature correct")),
Err(e) => Ok(RoomMessageEventContent::text_plain(format!(
"Signature verification failed: {e}"
))),
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Invalid json: {e}"))),
}
}
#[admin_command]
pub(super) async fn verify_pdu(&self, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
let mut event = self.services.rooms.timeline.get_pdu_json(&event_id).await?;
event.remove("event_id");
let msg = match self.services.server_keys.verify_event(&event, None).await {
Ok(ruma::signatures::Verified::Signatures) => "signatures OK, but content hash failed (redaction).",
Ok(ruma::signatures::Verified::All) => "signatures and hashes OK.",
Err(e) => return Err(e),
};
Ok(RoomMessageEventContent::notice_plain(msg))
}
#[admin_command]
#[tracing::instrument(skip(self))]
pub(super) async fn first_pdu_in_room(&self, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
@@ -470,7 +477,8 @@ pub(super) async fn first_pdu_in_room(&self, room_id: Box<RoomId>) -> Result<Roo
.services
.rooms
.state_cache
.server_in_room(&self.services.globals.config.server_name, &room_id)?
.server_in_room(&self.services.globals.config.server_name, &room_id)
.await
{
return Ok(RoomMessageEventContent::text_plain(
"We are not participating in the room / we don't know about the room ID.",
@@ -481,8 +489,9 @@ pub(super) async fn first_pdu_in_room(&self, room_id: Box<RoomId>) -> Result<Roo
.services
.rooms
.timeline
.first_pdu_in_room(&room_id)?
.ok_or_else(|| Error::bad_database("Failed to find the first PDU in database"))?;
.first_pdu_in_room(&room_id)
.await
.map_err(|_| Error::bad_database("Failed to find the first PDU in database"))?;
Ok(RoomMessageEventContent::text_plain(format!("{first_pdu:?}")))
}
@@ -494,7 +503,8 @@ pub(super) async fn latest_pdu_in_room(&self, room_id: Box<RoomId>) -> Result<Ro
.services
.rooms
.state_cache
.server_in_room(&self.services.globals.config.server_name, &room_id)?
.server_in_room(&self.services.globals.config.server_name, &room_id)
.await
{
return Ok(RoomMessageEventContent::text_plain(
"We are not participating in the room / we don't know about the room ID.",
@@ -505,8 +515,9 @@ pub(super) async fn latest_pdu_in_room(&self, room_id: Box<RoomId>) -> Result<Ro
.services
.rooms
.timeline
.latest_pdu_in_room(&room_id)?
.ok_or_else(|| Error::bad_database("Failed to find the latest PDU in database"))?;
.latest_pdu_in_room(&room_id)
.await
.map_err(|_| Error::bad_database("Failed to find the latest PDU in database"))?;
Ok(RoomMessageEventContent::text_plain(format!("{latest_pdu:?}")))
}
@@ -520,7 +531,8 @@ pub(super) async fn force_set_room_state_from_server(
.services
.rooms
.state_cache
.server_in_room(&self.services.globals.config.server_name, &room_id)?
.server_in_room(&self.services.globals.config.server_name, &room_id)
.await
{
return Ok(RoomMessageEventContent::text_plain(
"We are not participating in the room / we don't know about the room ID.",
@@ -531,13 +543,13 @@ pub(super) async fn force_set_room_state_from_server(
.services
.rooms
.timeline
.latest_pdu_in_room(&room_id)?
.ok_or_else(|| Error::bad_database("Failed to find the latest PDU in database"))?;
.latest_pdu_in_room(&room_id)
.await
.map_err(|_| Error::bad_database("Failed to find the latest PDU in database"))?;
let room_version = self.services.rooms.state.get_room_version(&room_id)?;
let room_version = self.services.rooms.state.get_room_version(&room_id).await?;
let mut state: HashMap<u64, Arc<EventId>> = HashMap::new();
let pub_key_map = RwLock::new(BTreeMap::new());
let remote_state_response = self
.services
@@ -551,30 +563,28 @@ pub(super) async fn force_set_room_state_from_server(
)
.await?;
let mut events = Vec::with_capacity(remote_state_response.pdus.len());
for pdu in remote_state_response.pdus.clone() {
events.push(match self.services.rooms.event_handler.parse_incoming_pdu(&pdu) {
match self
.services
.rooms
.event_handler
.parse_incoming_pdu(&pdu)
.await
{
Ok(t) => t,
Err(e) => {
warn!("Could not parse PDU, ignoring: {e}");
continue;
},
});
};
}
info!("Fetching required signing keys for all the state events we got");
self.services
.server_keys
.fetch_required_signing_keys(events.iter().map(|(_event_id, event, _room_id)| event), &pub_key_map)
.await?;
info!("Going through room_state response PDUs");
for result in remote_state_response
.pdus
.iter()
.map(|pdu| validate_and_add_event_id(self.services, pdu, &room_version, &pub_key_map))
{
for result in remote_state_response.pdus.iter().map(|pdu| {
self.services
.server_keys
.validate_and_add_event_id(pdu, &room_version)
}) {
let Ok((event_id, value)) = result.await else {
continue;
};
@@ -587,23 +597,26 @@ pub(super) async fn force_set_room_state_from_server(
self.services
.rooms
.outlier
.add_pdu_outlier(&event_id, &value)?;
.add_pdu_outlier(&event_id, &value);
if let Some(state_key) = &pdu.state_key {
let shortstatekey = self
.services
.rooms
.short
.get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key)?;
.get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key)
.await;
state.insert(shortstatekey, pdu.event_id.clone());
}
}
info!("Going through auth_chain response");
for result in remote_state_response
.auth_chain
.iter()
.map(|pdu| validate_and_add_event_id(self.services, pdu, &room_version, &pub_key_map))
{
for result in remote_state_response.auth_chain.iter().map(|pdu| {
self.services
.server_keys
.validate_and_add_event_id(pdu, &room_version)
}) {
let Ok((event_id, value)) = result.await else {
continue;
};
@@ -611,7 +624,7 @@ pub(super) async fn force_set_room_state_from_server(
self.services
.rooms
.outlier
.add_pdu_outlier(&event_id, &value)?;
.add_pdu_outlier(&event_id, &value);
}
let new_room_state = self
@@ -622,17 +635,22 @@ pub(super) async fn force_set_room_state_from_server(
.await?;
info!("Forcing new room state");
let (short_state_hash, new, removed) = self
let HashSetCompressStateEvent {
shortstatehash: short_state_hash,
added,
removed,
} = self
.services
.rooms
.state_compressor
.save_state(room_id.clone().as_ref(), new_room_state)?;
.save_state(room_id.clone().as_ref(), new_room_state)
.await?;
let state_lock = self.services.rooms.state.mutex.lock(&room_id).await;
self.services
.rooms
.state
.force_state(room_id.clone().as_ref(), short_state_hash, new, removed, &state_lock)
.force_state(room_id.clone().as_ref(), short_state_hash, added, removed, &state_lock)
.await?;
info!(
@@ -642,7 +660,8 @@ pub(super) async fn force_set_room_state_from_server(
self.services
.rooms
.state_cache
.update_joined_count(&room_id)?;
.update_joined_count(&room_id)
.await;
drop(state_lock);
@@ -653,10 +672,33 @@ pub(super) async fn force_set_room_state_from_server(
#[admin_command]
pub(super) async fn get_signing_keys(
&self, server_name: Option<Box<ServerName>>, _cached: bool,
&self, server_name: Option<Box<ServerName>>, notary: Option<Box<ServerName>>, query: bool,
) -> Result<RoomMessageEventContent> {
let server_name = server_name.unwrap_or_else(|| self.services.server.config.server_name.clone().into());
let signing_keys = self.services.globals.signing_keys_for(&server_name)?;
if let Some(notary) = notary {
let signing_keys = self
.services
.server_keys
.notary_request(&notary, &server_name)
.await?;
return Ok(RoomMessageEventContent::notice_markdown(format!(
"```rs\n{signing_keys:#?}\n```"
)));
}
let signing_keys = if query {
self.services
.server_keys
.server_request(&server_name)
.await?
} else {
self.services
.server_keys
.signing_keys_for(&server_name)
.await?
};
Ok(RoomMessageEventContent::notice_markdown(format!(
"```rs\n{signing_keys:#?}\n```"
@@ -664,34 +706,20 @@ pub(super) async fn get_signing_keys(
}
#[admin_command]
#[allow(dead_code)]
pub(super) async fn get_verify_keys(
&self, server_name: Option<Box<ServerName>>, cached: bool,
) -> Result<RoomMessageEventContent> {
pub(super) async fn get_verify_keys(&self, server_name: Option<Box<ServerName>>) -> Result<RoomMessageEventContent> {
let server_name = server_name.unwrap_or_else(|| self.services.server.config.server_name.clone().into());
let mut out = String::new();
if cached {
writeln!(out, "| Key ID | VerifyKey |")?;
writeln!(out, "| --- | --- |")?;
for (key_id, verify_key) in self.services.globals.verify_keys_for(&server_name)? {
writeln!(out, "| {key_id} | {verify_key:?} |")?;
}
return Ok(RoomMessageEventContent::notice_markdown(out));
}
let signature_ids: Vec<String> = Vec::new();
let keys = self
.services
.server_keys
.fetch_signing_keys_for_server(&server_name, signature_ids)
.await?;
.verify_keys_for(&server_name)
.await;
let mut out = String::new();
writeln!(out, "| Key ID | Public Key |")?;
writeln!(out, "| --- | --- |")?;
for (key_id, key) in keys {
writeln!(out, "| {key_id} | {key} |")?;
writeln!(out, "| {key_id} | {key:?} |")?;
}
Ok(RoomMessageEventContent::notice_markdown(out))
@@ -814,10 +842,10 @@ pub(super) async fn database_stats(
&self, property: Option<String>, map: Option<String>,
) -> Result<RoomMessageEventContent> {
let property = property.unwrap_or_else(|| "rocksdb.stats".to_owned());
let map_name = map.as_ref().map_or(utils::string::EMPTY, String::as_str);
let map_name = map.as_ref().map_or(EMPTY, String::as_str);
let mut out = String::new();
for (name, map) in self.services.db.iter_maps() {
for (name, map) in self.services.db.iter() {
if !map_name.is_empty() && *map_name != *name {
continue;
}
+16 -1
View File
@@ -80,8 +80,16 @@ pub(super) enum DebugCommand {
GetSigningKeys {
server_name: Option<Box<ServerName>>,
#[arg(long)]
notary: Option<Box<ServerName>>,
#[arg(short, long)]
cached: bool,
query: bool,
},
/// - Get and display signing keys from local cache or remote server.
GetVerifyKeys {
server_name: Option<Box<ServerName>>,
},
/// - Sends a federation request to the remote server's
@@ -119,6 +127,13 @@ pub(super) enum DebugCommand {
/// the command.
VerifyJson,
/// - Verify PDU
///
/// This re-verifies a PDU existing in the database found by ID.
VerifyPdu {
event_id: Box<EventId>,
},
/// - Prints the very first PDU in the specified room (typically
/// m.room.create)
FirstPduInRoom {
+12 -29
View File
@@ -1,19 +1,20 @@
use std::fmt::Write;
use conduit::Result;
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomId, ServerName, UserId};
use crate::{admin_command, escape_html, get_room_info};
use crate::{admin_command, get_room_info};
#[admin_command]
pub(super) async fn disable_room(&self, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
self.services.rooms.metadata.disable_room(&room_id, true)?;
self.services.rooms.metadata.disable_room(&room_id, true);
Ok(RoomMessageEventContent::text_plain("Room disabled."))
}
#[admin_command]
pub(super) async fn enable_room(&self, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
self.services.rooms.metadata.disable_room(&room_id, false)?;
self.services.rooms.metadata.disable_room(&room_id, false);
Ok(RoomMessageEventContent::text_plain("Room enabled."))
}
@@ -85,7 +86,7 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: Box<UserId>) -> Result<
));
}
if !self.services.users.exists(&user_id)? {
if !self.services.users.exists(&user_id).await {
return Ok(RoomMessageEventContent::text_plain(
"Remote user does not exist in our database.",
));
@@ -96,9 +97,9 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: Box<UserId>) -> Result<
.rooms
.state_cache
.rooms_joined(&user_id)
.filter_map(Result::ok)
.map(|room_id| get_room_info(self.services, &room_id))
.collect();
.then(|room_id| get_room_info(self.services, room_id))
.collect()
.await;
if rooms.is_empty() {
return Ok(RoomMessageEventContent::text_plain("User is not in any rooms."));
@@ -107,33 +108,15 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: Box<UserId>) -> Result<
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let output_plain = format!(
"Rooms {user_id} shares with us ({}):\n{}",
let output = format!(
"Rooms {user_id} shares with us ({}):\n```\n{}\n```",
rooms.len(),
rooms
.iter()
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
.map(|(id, members, name)| format!("{id} | Members: {members} | Name: {name}"))
.collect::<Vec<_>>()
.join("\n")
);
let output_html = format!(
"<table><caption>Rooms {user_id} shares with us \
({})</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
rooms.len(),
rooms
.iter()
.fold(String::new(), |mut output, (id, members, name)| {
writeln!(
output,
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
id,
members,
escape_html(name)
)
.expect("should be able to write to string buffer");
output
})
);
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
Ok(RoomMessageEventContent::text_markdown(output))
}
+2 -2
View File
@@ -36,7 +36,7 @@ pub(super) async fn delete(
let mut mxc_urls = Vec::with_capacity(4);
// parsing the PDU for any MXC URLs begins here
if let Some(event_json) = self.services.rooms.timeline.get_pdu_json(&event_id)? {
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();
@@ -300,7 +300,7 @@ pub(super) async fn delete_all_from_server(
#[admin_command]
pub(super) async fn get_file_info(&self, mxc: OwnedMxcUri) -> Result<RoomMessageEventContent> {
let mxc: Mxc<'_> = mxc.as_str().try_into()?;
let metadata = self.services.media.get_metadata(&mxc);
let metadata = self.services.media.get_metadata(&mxc).await;
Ok(RoomMessageEventContent::notice_markdown(format!("```\n{metadata:#?}\n```")))
}
+2 -5
View File
@@ -17,7 +17,7 @@ use conduit::{
utils::string::{collect_stream, common_prefix},
warn, Error, Result,
};
use futures_util::future::FutureExt;
use futures::future::FutureExt;
use ruma::{
events::{
relation::InReplyTo,
@@ -157,10 +157,7 @@ fn parse<'a>(
let message = error
.to_string()
.replace("server.name", services.globals.server_name().as_str());
Err(reply(
RoomMessageEventContent::notice_markdown(message),
input.reply_id.as_deref(),
))
Err(reply(RoomMessageEventContent::notice_plain(message), input.reply_id.as_deref()))
},
}
}
+9 -8
View File
@@ -1,9 +1,7 @@
use clap::Subcommand;
use conduit::Result;
use ruma::{
events::{room::message::RoomMessageEventContent, RoomAccountDataEventType},
RoomId, UserId,
};
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, RoomId, UserId};
use crate::Command;
@@ -25,7 +23,7 @@ pub(crate) enum AccountDataCommand {
/// Full user ID
user_id: Box<UserId>,
/// Account data event type
kind: RoomAccountDataEventType,
kind: String,
/// Optional room ID of the account data
room_id: Option<Box<RoomId>>,
},
@@ -42,9 +40,11 @@ pub(super) async fn process(subcommand: AccountDataCommand, context: &Command<'_
room_id,
} => {
let timer = tokio::time::Instant::now();
let results = services
let results: Vec<_> = services
.account_data
.changes_since(room_id.as_deref(), &user_id, since)?;
.changes_since(room_id.as_deref(), &user_id, since)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -59,7 +59,8 @@ pub(super) async fn process(subcommand: AccountDataCommand, context: &Command<'_
let timer = tokio::time::Instant::now();
let results = services
.account_data
.get(room_id.as_deref(), &user_id, kind)?;
.get_raw(room_id.as_deref(), &user_id, &kind)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
+3 -5
View File
@@ -26,10 +26,8 @@ pub(super) async fn process(subcommand: AppserviceCommand, context: &Command<'_>
appservice_id,
} => {
let timer = tokio::time::Instant::now();
let results = services
.appservice
.db
.get_registration(appservice_id.as_ref());
let results = services.appservice.get_registration(&appservice_id).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -38,7 +36,7 @@ pub(super) async fn process(subcommand: AppserviceCommand, context: &Command<'_>
},
AppserviceCommand::All => {
let timer = tokio::time::Instant::now();
let results = services.appservice.all();
let results = services.appservice.all().await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
+3 -14
View File
@@ -13,8 +13,6 @@ pub(crate) enum GlobalsCommand {
LastCheckForUpdatesId,
LoadKeypair,
/// - This returns an empty `Ok(BTreeMap<..>)` when there are no keys found
/// for the server.
SigningKeysFor {
@@ -29,7 +27,7 @@ pub(super) async fn process(subcommand: GlobalsCommand, context: &Command<'_>) -
match subcommand {
GlobalsCommand::DatabaseVersion => {
let timer = tokio::time::Instant::now();
let results = services.globals.db.database_version();
let results = services.globals.db.database_version().await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -47,16 +45,7 @@ pub(super) async fn process(subcommand: GlobalsCommand, context: &Command<'_>) -
},
GlobalsCommand::LastCheckForUpdatesId => {
let timer = tokio::time::Instant::now();
let results = services.updates.last_check_for_updates_id();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
GlobalsCommand::LoadKeypair => {
let timer = tokio::time::Instant::now();
let results = services.globals.db.load_keypair();
let results = services.updates.last_check_for_updates_id().await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -67,7 +56,7 @@ pub(super) async fn process(subcommand: GlobalsCommand, context: &Command<'_>) -
origin,
} => {
let timer = tokio::time::Instant::now();
let results = services.globals.db.verify_keys_for(&origin);
let results = services.server_keys.verify_keys_for(&origin).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
+9 -4
View File
@@ -1,5 +1,6 @@
use clap::Subcommand;
use conduit::Result;
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, UserId};
use crate::Command;
@@ -30,7 +31,7 @@ pub(super) async fn process(subcommand: PresenceCommand, context: &Command<'_>)
user_id,
} => {
let timer = tokio::time::Instant::now();
let results = services.presence.db.get_presence(&user_id)?;
let results = services.presence.db.get_presence(&user_id).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -41,12 +42,16 @@ pub(super) async fn process(subcommand: PresenceCommand, context: &Command<'_>)
since,
} => {
let timer = tokio::time::Instant::now();
let results = services.presence.db.presence_since(since);
let presence_since: Vec<(_, _, _)> = results.collect();
let results: Vec<(_, _, _)> = services
.presence
.presence_since(since)
.map(|(user_id, count, bytes)| (user_id.to_owned(), count, bytes.to_vec()))
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{presence_since:#?}\n```"
"Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"
)))
},
}
+1 -1
View File
@@ -21,7 +21,7 @@ pub(super) async fn process(subcommand: PusherCommand, context: &Command<'_>) ->
user_id,
} => {
let timer = tokio::time::Instant::now();
let results = services.pusher.get_pushers(&user_id)?;
let results = services.pusher.get_pushers(&user_id).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
+16 -5
View File
@@ -1,5 +1,6 @@
use clap::Subcommand;
use conduit::Result;
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId, RoomId};
use crate::Command;
@@ -31,7 +32,7 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Command<'_>)
alias,
} => {
let timer = tokio::time::Instant::now();
let results = services.rooms.alias.resolve_local_alias(&alias);
let results = services.rooms.alias.resolve_local_alias(&alias).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -42,8 +43,13 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Command<'_>)
room_id,
} => {
let timer = tokio::time::Instant::now();
let results = services.rooms.alias.local_aliases_for_room(&room_id);
let aliases: Vec<_> = results.collect();
let aliases: Vec<_> = services
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -52,8 +58,13 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Command<'_>)
},
RoomAliasCommand::AllLocalAliases => {
let timer = tokio::time::Instant::now();
let results = services.rooms.alias.all_local_aliases();
let aliases: Vec<_> = results.collect();
let aliases = services
.rooms
.alias
.all_local_aliases()
.map(|(room_id, alias)| (room_id.to_owned(), alias.to_owned()))
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
+75 -18
View File
@@ -1,5 +1,6 @@
use clap::Subcommand;
use conduit::Result;
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, RoomId, ServerName, UserId};
use crate::Command;
@@ -86,7 +87,11 @@ pub(super) async fn process(
room_id,
} => {
let timer = tokio::time::Instant::now();
let result = services.rooms.state_cache.server_in_room(&server, &room_id);
let result = services
.rooms
.state_cache
.server_in_room(&server, &room_id)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -97,7 +102,13 @@ pub(super) async fn process(
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services.rooms.state_cache.room_servers(&room_id).collect();
let results: Vec<_> = services
.rooms
.state_cache
.room_servers(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -108,7 +119,13 @@ pub(super) async fn process(
server,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services.rooms.state_cache.server_rooms(&server).collect();
let results: Vec<_> = services
.rooms
.state_cache
.server_rooms(&server)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -119,7 +136,13 @@ pub(super) async fn process(
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services.rooms.state_cache.room_members(&room_id).collect();
let results: Vec<_> = services
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -134,7 +157,9 @@ pub(super) async fn process(
.rooms
.state_cache
.local_users_in_room(&room_id)
.collect();
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -149,7 +174,9 @@ pub(super) async fn process(
.rooms
.state_cache
.active_local_users_in_room(&room_id)
.collect();
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -160,7 +187,7 @@ pub(super) async fn process(
room_id,
} => {
let timer = tokio::time::Instant::now();
let results = services.rooms.state_cache.room_joined_count(&room_id);
let results = services.rooms.state_cache.room_joined_count(&room_id).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -171,7 +198,11 @@ pub(super) async fn process(
room_id,
} => {
let timer = tokio::time::Instant::now();
let results = services.rooms.state_cache.room_invited_count(&room_id);
let results = services
.rooms
.state_cache
.room_invited_count(&room_id)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -182,11 +213,13 @@ pub(super) async fn process(
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services
let results: Vec<_> = services
.rooms
.state_cache
.room_useroncejoined(&room_id)
.collect();
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -197,11 +230,13 @@ pub(super) async fn process(
room_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services
let results: Vec<_> = services
.rooms
.state_cache
.room_members_invited(&room_id)
.collect();
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -216,7 +251,8 @@ pub(super) async fn process(
let results = services
.rooms
.state_cache
.get_invite_count(&room_id, &user_id);
.get_invite_count(&room_id, &user_id)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -231,7 +267,8 @@ pub(super) async fn process(
let results = services
.rooms
.state_cache
.get_left_count(&room_id, &user_id);
.get_left_count(&room_id, &user_id)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -242,7 +279,13 @@ pub(super) async fn process(
user_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services.rooms.state_cache.rooms_joined(&user_id).collect();
let results: Vec<_> = services
.rooms
.state_cache
.rooms_joined(&user_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -253,7 +296,12 @@ pub(super) async fn process(
user_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services.rooms.state_cache.rooms_invited(&user_id).collect();
let results: Vec<_> = services
.rooms
.state_cache
.rooms_invited(&user_id)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -264,7 +312,12 @@ pub(super) async fn process(
user_id,
} => {
let timer = tokio::time::Instant::now();
let results: Result<Vec<_>> = services.rooms.state_cache.rooms_left(&user_id).collect();
let results: Vec<_> = services
.rooms
.state_cache
.rooms_left(&user_id)
.collect()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -276,7 +329,11 @@ pub(super) async fn process(
room_id,
} => {
let timer = tokio::time::Instant::now();
let results = services.rooms.state_cache.invite_state(&user_id, &room_id);
let results = services
.rooms
.state_cache
.invite_state(&user_id, &room_id)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
+5 -4
View File
@@ -1,5 +1,6 @@
use clap::Subcommand;
use conduit::Result;
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, ServerName, UserId};
use service::sending::Destination;
@@ -68,7 +69,7 @@ pub(super) async fn process(subcommand: SendingCommand, context: &Command<'_>) -
SendingCommand::ActiveRequests => {
let timer = tokio::time::Instant::now();
let results = services.sending.db.active_requests();
let active_requests: Result<Vec<(_, _, _)>> = results.collect();
let active_requests = results.collect::<Vec<_>>().await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -133,7 +134,7 @@ pub(super) async fn process(subcommand: SendingCommand, context: &Command<'_>) -
},
};
let queued_requests = results.collect::<Result<Vec<(_, _)>>>();
let queued_requests = results.collect::<Vec<_>>().await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -199,7 +200,7 @@ pub(super) async fn process(subcommand: SendingCommand, context: &Command<'_>) -
},
};
let active_requests = results.collect::<Result<Vec<(_, _)>>>();
let active_requests = results.collect::<Vec<_>>().await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
@@ -210,7 +211,7 @@ pub(super) async fn process(subcommand: SendingCommand, context: &Command<'_>) -
server_name,
} => {
let timer = tokio::time::Instant::now();
let results = services.sending.db.get_latest_educount(&server_name);
let results = services.sending.db.get_latest_educount(&server_name).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
+333 -18
View File
@@ -1,29 +1,344 @@
use clap::Subcommand;
use conduit::Result;
use ruma::events::room::message::RoomMessageEventContent;
use futures::stream::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, OwnedDeviceId, OwnedRoomId, OwnedUserId};
use crate::Command;
use crate::{admin_command, admin_command_dispatch};
#[admin_command_dispatch]
#[derive(Debug, Subcommand)]
/// All the getters and iterators from src/database/key_value/users.rs
pub(crate) enum UsersCommand {
Iter,
CountUsers,
IterUsers,
PasswordHash {
user_id: OwnedUserId,
},
ListDevices {
user_id: OwnedUserId,
},
ListDevicesMetadata {
user_id: OwnedUserId,
},
GetDeviceMetadata {
user_id: OwnedUserId,
device_id: OwnedDeviceId,
},
GetDevicesVersion {
user_id: OwnedUserId,
},
CountOneTimeKeys {
user_id: OwnedUserId,
device_id: OwnedDeviceId,
},
GetDeviceKeys {
user_id: OwnedUserId,
device_id: OwnedDeviceId,
},
GetUserSigningKey {
user_id: OwnedUserId,
},
GetMasterKey {
user_id: OwnedUserId,
},
GetToDeviceEvents {
user_id: OwnedUserId,
device_id: OwnedDeviceId,
},
GetLatestBackup {
user_id: OwnedUserId,
},
GetLatestBackupVersion {
user_id: OwnedUserId,
},
GetBackupAlgorithm {
user_id: OwnedUserId,
version: String,
},
GetAllBackups {
user_id: OwnedUserId,
version: String,
},
GetRoomBackups {
user_id: OwnedUserId,
version: String,
room_id: OwnedRoomId,
},
GetBackupSession {
user_id: OwnedUserId,
version: String,
room_id: OwnedRoomId,
session_id: String,
},
}
/// All the getters and iterators in key_value/users.rs
pub(super) async fn process(subcommand: UsersCommand, context: &Command<'_>) -> Result<RoomMessageEventContent> {
let services = context.services;
#[admin_command]
async fn get_backup_session(
&self, user_id: OwnedUserId, version: String, room_id: OwnedRoomId, session_id: String,
) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_session(&user_id, &version, &room_id, &session_id)
.await;
let query_time = timer.elapsed();
match subcommand {
UsersCommand::Iter => {
let timer = tokio::time::Instant::now();
let results = services.users.db.iter();
let users = results.collect::<Vec<_>>();
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{users:#?}\n```"
)))
},
}
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn get_room_backups(
&self, user_id: OwnedUserId, version: String, room_id: OwnedRoomId,
) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_room(&user_id, &version, &room_id)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn get_all_backups(&self, user_id: OwnedUserId, version: String) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self.services.key_backups.get_all(&user_id, &version).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn get_backup_algorithm(&self, user_id: OwnedUserId, version: String) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_backup(&user_id, &version)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn get_latest_backup_version(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self
.services
.key_backups
.get_latest_backup_version(&user_id)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn get_latest_backup(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self.services.key_backups.get_latest_backup(&user_id).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn iter_users(&self) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result: Vec<OwnedUserId> = self.services.users.stream().map(Into::into).collect().await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn count_users(&self) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self.services.users.count().await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn password_hash(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self.services.users.password_hash(&user_id).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn list_devices(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let devices = self
.services
.users
.all_device_ids(&user_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```"
)))
}
#[admin_command]
async fn list_devices_metadata(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let devices = self
.services
.users
.all_devices_metadata(&user_id)
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{devices:#?}\n```"
)))
}
#[admin_command]
async fn get_device_metadata(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let device = self
.services
.users
.get_device_metadata(&user_id, &device_id)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```"
)))
}
#[admin_command]
async fn get_devices_version(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let device = self.services.users.get_devicelist_version(&user_id).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{device:#?}\n```"
)))
}
#[admin_command]
async fn count_one_time_keys(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.count_one_time_keys(&user_id, &device_id)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn get_device_keys(&self, user_id: OwnedUserId, device_id: OwnedDeviceId) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.get_device_keys(&user_id, &device_id)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn get_user_signing_key(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self.services.users.get_user_signing_key(&user_id).await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn get_master_key(&self, user_id: OwnedUserId) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.get_master_key(None, &user_id, &|_| true)
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
#[admin_command]
async fn get_to_device_events(
&self, user_id: OwnedUserId, device_id: OwnedDeviceId,
) -> Result<RoomMessageEventContent> {
let timer = tokio::time::Instant::now();
let result = self
.services
.users
.get_to_device_events(&user_id, &device_id)
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
Ok(RoomMessageEventContent::notice_markdown(format!(
"Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"
)))
}
+57 -63
View File
@@ -2,7 +2,8 @@ use std::fmt::Write;
use clap::Subcommand;
use conduit::Result;
use ruma::{events::room::message::RoomMessageEventContent, RoomAliasId, RoomId};
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId};
use crate::{escape_html, Command};
@@ -66,8 +67,8 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Command<'_>) ->
force,
room_id,
..
} => match (force, services.rooms.alias.resolve_local_alias(&room_alias)) {
(true, Ok(Some(id))) => match services
} => match (force, services.rooms.alias.resolve_local_alias(&room_alias).await) {
(true, Ok(id)) => match services
.rooms
.alias
.set_alias(&room_alias, &room_id, server_user)
@@ -77,10 +78,10 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Command<'_>) ->
))),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))),
},
(false, Ok(Some(id))) => Ok(RoomMessageEventContent::text_plain(format!(
(false, Ok(id)) => Ok(RoomMessageEventContent::text_plain(format!(
"Refusing to overwrite in use alias for {id}, use -f or --force to overwrite"
))),
(_, Ok(None)) => match services
(_, Err(_)) => match services
.rooms
.alias
.set_alias(&room_alias, &room_id, server_user)
@@ -88,12 +89,11 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Command<'_>) ->
Ok(()) => Ok(RoomMessageEventContent::text_plain("Successfully set alias")),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))),
},
(_, Err(err)) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {err}"))),
},
RoomAliasCommand::Remove {
..
} => match services.rooms.alias.resolve_local_alias(&room_alias) {
Ok(Some(id)) => match services
} => match services.rooms.alias.resolve_local_alias(&room_alias).await {
Ok(id) => match services
.rooms
.alias
.remove_alias(&room_alias, server_user)
@@ -102,15 +102,13 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Command<'_>) ->
Ok(()) => Ok(RoomMessageEventContent::text_plain(format!("Removed alias from {id}"))),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Failed to remove alias: {err}"))),
},
Ok(None) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {err}"))),
Err(_) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
},
RoomAliasCommand::Which {
..
} => match services.rooms.alias.resolve_local_alias(&room_alias) {
Ok(Some(id)) => Ok(RoomMessageEventContent::text_plain(format!("Alias resolves to {id}"))),
Ok(None) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to lookup alias: {err}"))),
} => match services.rooms.alias.resolve_local_alias(&room_alias).await {
Ok(id) => Ok(RoomMessageEventContent::text_plain(format!("Alias resolves to {id}"))),
Err(_) => Ok(RoomMessageEventContent::text_plain("Alias isn't in use.")),
},
RoomAliasCommand::List {
..
@@ -121,67 +119,63 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Command<'_>) ->
room_id,
} => {
if let Some(room_id) = room_id {
let aliases = services
let aliases: Vec<OwnedRoomAliasId> = services
.rooms
.alias
.local_aliases_for_room(&room_id)
.collect::<Result<Vec<_>, _>>();
match aliases {
Ok(aliases) => {
let plain_list = aliases.iter().fold(String::new(), |mut output, alias| {
writeln!(output, "- {alias}").expect("should be able to write to string buffer");
output
});
.map(Into::into)
.collect()
.await;
let html_list = aliases.iter().fold(String::new(), |mut output, alias| {
writeln!(output, "<li>{}</li>", escape_html(alias.as_ref()))
.expect("should be able to write to string buffer");
output
});
let plain_list = aliases.iter().fold(String::new(), |mut output, alias| {
writeln!(output, "- {alias}").expect("should be able to write to string buffer");
output
});
let plain = format!("Aliases for {room_id}:\n{plain_list}");
let html = format!("Aliases for {room_id}:\n<ul>{html_list}</ul>");
Ok(RoomMessageEventContent::text_html(plain, html))
},
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to list aliases: {err}"))),
}
let html_list = aliases.iter().fold(String::new(), |mut output, alias| {
writeln!(output, "<li>{}</li>", escape_html(alias.as_ref()))
.expect("should be able to write to string buffer");
output
});
let plain = format!("Aliases for {room_id}:\n{plain_list}");
let html = format!("Aliases for {room_id}:\n<ul>{html_list}</ul>");
Ok(RoomMessageEventContent::text_html(plain, html))
} else {
let aliases = services
.rooms
.alias
.all_local_aliases()
.collect::<Result<Vec<_>, _>>();
match aliases {
Ok(aliases) => {
let server_name = services.globals.server_name();
let plain_list = aliases
.iter()
.fold(String::new(), |mut output, (alias, id)| {
writeln!(output, "- `{alias}` -> #{id}:{server_name}")
.expect("should be able to write to string buffer");
output
});
.map(|(room_id, localpart)| (room_id.into(), localpart.into()))
.collect::<Vec<(OwnedRoomId, String)>>()
.await;
let html_list = aliases
.iter()
.fold(String::new(), |mut output, (alias, id)| {
writeln!(
output,
"<li><code>{}</code> -> #{}:{}</li>",
escape_html(alias.as_ref()),
escape_html(id.as_ref()),
server_name
)
.expect("should be able to write to string buffer");
output
});
let server_name = services.globals.server_name();
let plain_list = aliases
.iter()
.fold(String::new(), |mut output, (alias, id)| {
writeln!(output, "- `{alias}` -> #{id}:{server_name}")
.expect("should be able to write to string buffer");
output
});
let plain = format!("Aliases:\n{plain_list}");
let html = format!("Aliases:\n<ul>{html_list}</ul>");
Ok(RoomMessageEventContent::text_html(plain, html))
},
Err(e) => Ok(RoomMessageEventContent::text_plain(format!("Unable to list room aliases: {e}"))),
}
let html_list = aliases
.iter()
.fold(String::new(), |mut output, (alias, id)| {
writeln!(
output,
"<li><code>{}</code> -> #{}:{}</li>",
escape_html(alias.as_ref()),
escape_html(id),
server_name
)
.expect("should be able to write to string buffer");
output
});
let plain = format!("Aliases:\n{plain_list}");
let html = format!("Aliases:\n<ul>{html_list}</ul>");
Ok(RoomMessageEventContent::text_html(plain, html))
}
},
}
+18 -31
View File
@@ -1,5 +1,6 @@
use conduit::Result;
use ruma::events::room::message::RoomMessageEventContent;
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId};
use crate::{admin_command, get_room_info, PAGE_SIZE};
@@ -14,37 +15,16 @@ pub(super) async fn list_rooms(
.rooms
.metadata
.iter_ids()
.filter_map(|room_id| {
room_id
.ok()
.filter(|room_id| {
if exclude_disabled
&& self
.services
.rooms
.metadata
.is_disabled(room_id)
.unwrap_or(false)
{
return false;
}
if exclude_banned
&& self
.services
.rooms
.metadata
.is_banned(room_id)
.unwrap_or(false)
{
return false;
}
true
})
.map(|room_id| get_room_info(self.services, &room_id))
.filter_map(|room_id| async move {
(!exclude_disabled || !self.services.rooms.metadata.is_disabled(room_id).await).then_some(room_id)
})
.collect::<Vec<_>>();
.filter_map(|room_id| async move {
(!exclude_banned || !self.services.rooms.metadata.is_banned(room_id).await).then_some(room_id)
})
.then(|room_id| get_room_info(self.services, room_id))
.collect::<Vec<_>>()
.await;
rooms.sort_by_key(|r| r.1);
rooms.reverse();
@@ -74,3 +54,10 @@ pub(super) async fn list_rooms(
Ok(RoomMessageEventContent::notice_markdown(output_plain))
}
#[admin_command]
pub(super) async fn exists(&self, room_id: OwnedRoomId) -> Result<RoomMessageEventContent> {
let result = self.services.rooms.metadata.exists(&room_id).await;
Ok(RoomMessageEventContent::notice_markdown(format!("{result}")))
}
+20 -37
View File
@@ -1,10 +1,9 @@
use std::fmt::Write;
use clap::Subcommand;
use conduit::Result;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomId};
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, RoomId};
use crate::{escape_html, get_room_info, Command, PAGE_SIZE};
use crate::{get_room_info, Command, PAGE_SIZE};
#[derive(Debug, Subcommand)]
pub(crate) enum RoomDirectoryCommand {
@@ -31,67 +30,51 @@ pub(super) async fn process(command: RoomDirectoryCommand, context: &Command<'_>
match command {
RoomDirectoryCommand::Publish {
room_id,
} => match services.rooms.directory.set_public(&room_id) {
Ok(()) => Ok(RoomMessageEventContent::text_plain("Room published")),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to update room: {err}"))),
} => {
services.rooms.directory.set_public(&room_id);
Ok(RoomMessageEventContent::notice_plain("Room published"))
},
RoomDirectoryCommand::Unpublish {
room_id,
} => match services.rooms.directory.set_not_public(&room_id) {
Ok(()) => Ok(RoomMessageEventContent::text_plain("Room unpublished")),
Err(err) => Ok(RoomMessageEventContent::text_plain(format!("Unable to update room: {err}"))),
} => {
services.rooms.directory.set_not_public(&room_id);
Ok(RoomMessageEventContent::notice_plain("Room unpublished"))
},
RoomDirectoryCommand::List {
page,
} => {
// TODO: i know there's a way to do this with clap, but i can't seem to find it
let page = page.unwrap_or(1);
let mut rooms = services
let mut rooms: Vec<_> = services
.rooms
.directory
.public_rooms()
.filter_map(Result::ok)
.map(|id: OwnedRoomId| get_room_info(services, &id))
.collect::<Vec<_>>();
.then(|room_id| get_room_info(services, room_id))
.collect()
.await;
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let rooms = rooms
let rooms: Vec<_> = rooms
.into_iter()
.skip(page.saturating_sub(1).saturating_mul(PAGE_SIZE))
.take(PAGE_SIZE)
.collect::<Vec<_>>();
.collect();
if rooms.is_empty() {
return Ok(RoomMessageEventContent::text_plain("No more rooms."));
};
let output_plain = format!(
"Rooms:\n{}",
let output = format!(
"Rooms (page {page}):\n```\n{}\n```",
rooms
.iter()
.map(|(id, members, name)| format!("{id}\tMembers: {members}\tName: {name}"))
.map(|(id, members, name)| format!("{id} | Members: {members} | Name: {name}"))
.collect::<Vec<_>>()
.join("\n")
);
let output_html = format!(
"<table><caption>Room directory - page \
{page}</caption>\n<tr><th>id</th>\t<th>members</th>\t<th>name</th></tr>\n{}</table>",
rooms
.iter()
.fold(String::new(), |mut output, (id, members, name)| {
writeln!(
output,
"<tr><td>{}</td>\t<td>{}</td>\t<td>{}</td></tr>",
escape_html(id.as_ref()),
members,
escape_html(name.as_ref())
)
.expect("should be able to write to string buffer");
output
})
);
Ok(RoomMessageEventContent::text_html(output_plain, output_html))
Ok(RoomMessageEventContent::text_markdown(output))
},
}
}
+24 -28
View File
@@ -1,5 +1,6 @@
use clap::Subcommand;
use conduit::Result;
use conduit::{utils::ReadyExt, Result};
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, RoomId};
use crate::{admin_command, admin_command_dispatch};
@@ -32,46 +33,40 @@ async fn list_joined_members(&self, room_id: Box<RoomId>, local_only: bool) -> R
.rooms
.state_accessor
.get_name(&room_id)
.ok()
.flatten()
.unwrap_or_else(|| room_id.to_string());
.await
.unwrap_or_else(|_| room_id.to_string());
let members = self
let member_info: Vec<_> = self
.services
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|member| {
if local_only {
member
.ok()
.filter(|user| self.services.globals.user_is_local(user))
} else {
member.ok()
}
});
let member_info = members
.into_iter()
.map(|user_id| {
(
user_id.clone(),
.ready_filter(|user_id| {
local_only
.then(|| self.services.globals.user_is_local(user_id))
.unwrap_or(true)
})
.map(ToOwned::to_owned)
.filter_map(|user_id| async move {
Some((
self.services
.users
.displayname(&user_id)
.unwrap_or(None)
.unwrap_or_else(|| user_id.to_string()),
)
.await
.unwrap_or_else(|_| user_id.to_string()),
user_id,
))
})
.collect::<Vec<_>>();
.collect()
.await;
let output_plain = format!(
"{} Members in Room \"{}\":\n```\n{}\n```",
member_info.len(),
room_name,
member_info
.iter()
.map(|(mxid, displayname)| format!("{mxid} | {displayname}"))
.into_iter()
.map(|(displayname, mxid)| format!("{mxid} | {displayname}"))
.collect::<Vec<_>>()
.join("\n")
);
@@ -81,11 +76,12 @@ async fn list_joined_members(&self, room_id: Box<RoomId>, local_only: bool) -> R
#[admin_command]
async fn view_room_topic(&self, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
let Some(room_topic) = self
let Ok(room_topic) = self
.services
.rooms
.state_accessor
.get_room_topic(&room_id)?
.get_room_topic(&room_id)
.await
else {
return Ok(RoomMessageEventContent::text_plain("Room does not have a room topic set."));
};
+6
View File
@@ -6,6 +6,7 @@ mod moderation;
use clap::Subcommand;
use conduit::Result;
use ruma::OwnedRoomId;
use self::{
alias::RoomAliasCommand, directory::RoomDirectoryCommand, info::RoomInfoCommand, moderation::RoomModerationCommand,
@@ -49,4 +50,9 @@ pub(super) enum RoomCommand {
#[command(subcommand)]
/// - Manage the room directory
Directory(RoomDirectoryCommand),
/// - Check if we know about a room
Exists {
room_id: OwnedRoomId,
},
}
+163 -180
View File
@@ -1,6 +1,11 @@
use api::client::leave_room;
use clap::Subcommand;
use conduit::{debug, error, info, warn, Result};
use conduit::{
debug, error, info,
utils::{IterStream, ReadyExt},
warn, Result,
};
use futures::StreamExt;
use ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId, RoomAliasId, RoomId, RoomOrAliasId};
use crate::{admin_command, admin_command_dispatch, get_room_info};
@@ -76,7 +81,7 @@ async fn ban_room(
let admin_room_alias = &self.services.globals.admin_alias;
if let Some(admin_room_id) = self.services.admin.get_admin_room()? {
if let Ok(admin_room_id) = self.services.admin.get_admin_room().await {
if room.to_string().eq(&admin_room_id) || room.to_string().eq(admin_room_alias) {
return Ok(RoomMessageEventContent::text_plain("Not allowed to ban the admin room."));
}
@@ -95,7 +100,7 @@ async fn ban_room(
debug!("Room specified is a room ID, banning room ID");
self.services.rooms.metadata.ban_room(&room_id, true)?;
self.services.rooms.metadata.ban_room(&room_id, true);
room_id
} else if room.is_room_alias_id() {
@@ -114,7 +119,13 @@ async fn ban_room(
get_alias_helper to fetch room ID remotely"
);
let room_id = if let Some(room_id) = self.services.rooms.alias.resolve_local_alias(&room_alias)? {
let room_id = if let Ok(room_id) = 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");
@@ -138,7 +149,7 @@ async fn ban_room(
}
};
self.services.rooms.metadata.ban_room(&room_id, true)?;
self.services.rooms.metadata.ban_room(&room_id, true);
room_id
} else {
@@ -150,56 +161,40 @@ async fn ban_room(
debug!("Making all users leave the room {}", &room);
if force {
for local_user in self
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
self.services.globals.user_is_local(local_user)
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would
// fail auth check)
&& (self.services.globals.user_is_local(local_user)
// since this is a force operation, assume user is an admin
// if somehow this fails
&& self.services
.users
.is_admin(local_user)
.unwrap_or(true))
})
}) {
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
while let Some(local_user) = users.next().await {
debug!(
"Attempting leave for user {} in room {} (forced, ignoring all errors, evicting admins too)",
&local_user, &room_id
"Attempting leave for user {local_user} in room {room_id} (forced, ignoring all errors, evicting \
admins too)",
);
if let Err(e) = leave_room(self.services, &local_user, &room_id, None).await {
if let Err(e) = leave_room(self.services, local_user, &room_id, None).await {
warn!(%e, "Failed to leave room");
}
}
} else {
for local_user in self
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == self.services.globals.server_name()
// additional wrapped check here is to avoid adding remote users
// who are in the admin room to the list of local users (would fail auth check)
&& (local_user.server_name()
== self.services.globals.server_name()
&& !self.services
.users
.is_admin(local_user)
.unwrap_or(false))
})
}) {
.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 {
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
@@ -214,12 +209,14 @@ async fn ban_room(
}
// remove any local aliases, ignore errors
for ref local_alias in self
for local_alias in &self
.services
.rooms
.alias
.local_aliases_for_room(&room_id)
.filter_map(Result::ok)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await
{
_ = self
.services
@@ -230,10 +227,10 @@ async fn ban_room(
}
// unpublish from room directory, ignore errors
_ = self.services.rooms.directory.set_not_public(&room_id);
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);
return Ok(RoomMessageEventContent::text_plain(
"Room banned, removed all our local users, and disabled incoming federation with room.",
));
@@ -268,7 +265,7 @@ async fn ban_list_of_rooms(&self, force: bool, disable_federation: bool) -> Resu
for &room in &rooms_s {
match <&RoomOrAliasId>::try_from(room) {
Ok(room_alias_or_id) => {
if let Some(admin_room_id) = self.services.admin.get_admin_room()? {
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");
continue;
@@ -300,43 +297,48 @@ async fn ban_list_of_rooms(&self, force: bool, disable_federation: bool) -> Resu
if room_alias_or_id.is_room_alias_id() {
match RoomAliasId::parse(room_alias_or_id) {
Ok(room_alias) => {
let room_id =
if let Some(room_id) = self.services.rooms.alias.resolve_local_alias(&room_alias)? {
room_id
} else {
debug!(
"We don't have this room alias to a room ID locally, attempting to fetch room \
ID over federation"
);
let room_id = if let Ok(room_id) = 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"
);
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 {
warn!("Failed to resolve room alias {room} to a room ID: {e}");
continue;
}
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 {
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}"
)));
},
}
};
return Ok(RoomMessageEventContent::text_plain(format!(
"Failed to resolve room alias {room} to a room ID: {e}"
)));
},
}
};
room_ids.push(room_id);
},
@@ -374,74 +376,52 @@ async fn ban_list_of_rooms(&self, force: bool, disable_federation: bool) -> Resu
}
for room_id in room_ids {
if self
.services
.rooms
.metadata
.ban_room(&room_id, true)
.is_ok()
{
debug!("Banned {room_id} successfully");
room_ban_count = room_ban_count.saturating_add(1);
}
self.services.rooms.metadata.ban_room(&room_id, true);
debug!("Banned {room_id} successfully");
room_ban_count = room_ban_count.saturating_add(1);
debug!("Making all users leave the room {}", &room_id);
if force {
for local_user in self
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == self.services.globals.server_name()
// additional wrapped check here is to avoid adding remote
// users who are in the admin room to the list of local
// users (would fail auth check)
&& (local_user.server_name()
== self.services.globals.server_name()
// since this is a force operation, assume user is an
// admin if somehow this fails
&& self.services
.users
.is_admin(local_user)
.unwrap_or(true))
})
}) {
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
while let Some(local_user) = users.next().await {
debug!(
"Attempting leave for user {} in room {} (forced, ignoring all errors, evicting admins too)",
&local_user, room_id
"Attempting leave for user {local_user} in room {room_id} (forced, ignoring all errors, evicting \
admins too)",
);
if let Err(e) = leave_room(self.services, &local_user, &room_id, None).await {
if let Err(e) = leave_room(self.services, local_user, &room_id, None).await {
warn!(%e, "Failed to leave room");
}
}
} else {
for local_user in self
let mut users = self
.services
.rooms
.state_cache
.room_members(&room_id)
.filter_map(|user| {
user.ok().filter(|local_user| {
local_user.server_name() == self.services.globals.server_name()
// additional wrapped check here is to avoid adding remote
// users who are in the admin room to the list of local
// users (would fail auth check)
&& (local_user.server_name()
== self.services.globals.server_name()
&& !self.services
.users
.is_admin(local_user)
.unwrap_or(false))
})
}) {
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 {
.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 {} leave room {} during bulk room banning: {}",
&local_user, &room_id, e
"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 \
@@ -453,26 +433,26 @@ async fn ban_list_of_rooms(&self, force: bool, disable_federation: bool) -> Resu
}
// remove any local aliases, ignore errors
for ref local_alias in self
.services
self.services
.rooms
.alias
.local_aliases_for_room(&room_id)
.filter_map(Result::ok)
{
_ = self
.services
.rooms
.alias
.remove_alias(local_alias, &self.services.globals.server_user)
.await;
}
.map(ToOwned::to_owned)
.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
_ = self.services.rooms.directory.set_not_public(&room_id);
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);
}
}
@@ -503,7 +483,7 @@ async fn unban_room(&self, enable_federation: bool, room: Box<RoomOrAliasId>) ->
debug!("Room specified is a room ID, unbanning room ID");
self.services.rooms.metadata.ban_room(&room_id, false)?;
self.services.rooms.metadata.ban_room(&room_id, false);
room_id
} else if room.is_room_alias_id() {
@@ -522,7 +502,13 @@ async fn unban_room(&self, enable_federation: bool, room: Box<RoomOrAliasId>) ->
get_alias_helper to fetch room ID remotely"
);
let room_id = if let Some(room_id) = self.services.rooms.alias.resolve_local_alias(&room_alias)? {
let room_id = if let Ok(room_id) = 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");
@@ -546,7 +532,7 @@ async fn unban_room(&self, enable_federation: bool, room: Box<RoomOrAliasId>) ->
}
};
self.services.rooms.metadata.ban_room(&room_id, false)?;
self.services.rooms.metadata.ban_room(&room_id, false);
room_id
} else {
@@ -557,7 +543,7 @@ async fn unban_room(&self, enable_federation: bool, room: Box<RoomOrAliasId>) ->
};
if enable_federation {
self.services.rooms.metadata.disable_room(&room_id, false)?;
self.services.rooms.metadata.disable_room(&room_id, false);
return Ok(RoomMessageEventContent::text_plain("Room unbanned."));
}
@@ -569,45 +555,42 @@ async fn unban_room(&self, enable_federation: bool, room: Box<RoomOrAliasId>) ->
#[admin_command]
async fn list_banned_rooms(&self, no_details: bool) -> Result<RoomMessageEventContent> {
let rooms = self
let room_ids: Vec<OwnedRoomId> = self
.services
.rooms
.metadata
.list_banned_rooms()
.collect::<Result<Vec<_>, _>>();
.map(Into::into)
.collect()
.await;
match rooms {
Ok(room_ids) => {
if room_ids.is_empty() {
return Ok(RoomMessageEventContent::text_plain("No rooms are banned."));
}
let mut rooms = room_ids
.into_iter()
.map(|room_id| get_room_info(self.services, &room_id))
.collect::<Vec<_>>();
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let output_plain = format!(
"Rooms Banned ({}):\n```\n{}\n```",
rooms.len(),
rooms
.iter()
.map(|(id, members, name)| if no_details {
format!("{id}")
} else {
format!("{id}\tMembers: {members}\tName: {name}")
})
.collect::<Vec<_>>()
.join("\n")
);
Ok(RoomMessageEventContent::notice_markdown(output_plain))
},
Err(e) => {
error!("Failed to list banned rooms: {e}");
Ok(RoomMessageEventContent::text_plain(format!("Unable to list banned rooms: {e}")))
},
if room_ids.is_empty() {
return Ok(RoomMessageEventContent::text_plain("No rooms are banned."));
}
let mut rooms = room_ids
.iter()
.stream()
.then(|room_id| get_room_info(self.services, room_id))
.collect::<Vec<_>>()
.await;
rooms.sort_by_key(|r| r.1);
rooms.reverse();
let output_plain = format!(
"Rooms Banned ({}):\n```\n{}\n```",
rooms.len(),
rooms
.iter()
.map(|(id, members, name)| if no_details {
format!("{id}")
} else {
format!("{id}\tMembers: {members}\tName: {name}")
})
.collect::<Vec<_>>()
.join("\n")
);
Ok(RoomMessageEventContent::notice_markdown(output_plain))
}
+5 -2
View File
@@ -21,7 +21,10 @@ pub(super) async fn uptime(&self) -> Result<RoomMessageEventContent> {
#[admin_command]
pub(super) async fn show_config(&self) -> Result<RoomMessageEventContent> {
// Construct and send the response
Ok(RoomMessageEventContent::text_plain(format!("{}", self.services.globals.config)))
Ok(RoomMessageEventContent::text_markdown(format!(
"```\n{}\n```",
self.services.globals.config
)))
}
#[admin_command]
@@ -104,7 +107,7 @@ pub(super) async fn backup_database(&self) -> Result<RoomMessageEventContent> {
.runtime()
.spawn_blocking(move || match globals.db.backup() {
Ok(()) => String::new(),
Err(e) => (*e).to_string(),
Err(e) => e.to_string(),
})
.await?;
+376 -119
View File
@@ -1,7 +1,13 @@
use std::{collections::BTreeMap, fmt::Write as _};
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room};
use conduit::{error, info, utils, warn, PduBuilder, Result};
use conduit::{
debug_warn, error, info, is_equal_to,
utils::{self, ReadyExt},
warn, PduBuilder, Result,
};
use conduit_api::client::{leave_all_rooms, update_avatar_url, update_displayname};
use futures::StreamExt;
use ruma::{
events::{
room::{
@@ -10,11 +16,10 @@ use ruma::{
redaction::RoomRedactionEventContent,
},
tag::{TagEvent, TagEventContent, TagInfo},
RoomAccountDataEventType, StateEventType, TimelineEventType,
RoomAccountDataEventType, StateEventType,
},
EventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, RoomId,
EventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, RoomId, UserId,
};
use serde_json::value::to_raw_value;
use crate::{
admin_command, get_room_info,
@@ -22,19 +27,23 @@ use crate::{
};
const AUTO_GEN_PASSWORD_LENGTH: usize = 25;
const BULK_JOIN_REASON: &str = "Bulk force joining this room as initiated by the server admin.";
#[admin_command]
pub(super) async fn list_users(&self) -> Result<RoomMessageEventContent> {
match self.services.users.list_local_users() {
Ok(users) => {
let mut plain_msg = format!("Found {} local user account(s):\n```\n", users.len());
plain_msg += users.join("\n").as_str();
plain_msg += "\n```";
let users = self
.services
.users
.list_local_users()
.map(ToString::to_string)
.collect::<Vec<_>>()
.await;
Ok(RoomMessageEventContent::notice_markdown(plain_msg))
},
Err(e) => Ok(RoomMessageEventContent::text_plain(e.to_string())),
}
let mut plain_msg = format!("Found {} local user account(s):\n```\n", users.len());
plain_msg += users.join("\n").as_str();
plain_msg += "\n```";
Ok(RoomMessageEventContent::notice_markdown(plain_msg))
}
#[admin_command]
@@ -42,7 +51,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
// Validate user id
let user_id = parse_local_user_id(self.services, &username)?;
if self.services.users.exists(&user_id)? {
if self.services.users.exists(&user_id).await {
return Ok(RoomMessageEventContent::text_plain(format!("Userid {user_id} already exists")));
}
@@ -77,43 +86,51 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
self.services
.users
.set_displayname(&user_id, Some(displayname))
.await?;
.set_displayname(&user_id, Some(displayname));
// Initial account data
self.services.account_data.update(
None,
&user_id,
ruma::events::GlobalAccountDataEventType::PushRules
.to_string()
.into(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: ruma::push::Ruleset::server_default(&user_id),
},
})
.expect("to json value always works"),
)?;
self.services
.account_data
.update(
None,
&user_id,
ruma::events::GlobalAccountDataEventType::PushRules
.to_string()
.into(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: ruma::push::Ruleset::server_default(&user_id),
},
})
.expect("to json value always works"),
)
.await?;
if !self.services.globals.config.auto_join_rooms.is_empty() {
for room in &self.services.globals.config.auto_join_rooms {
let Ok(room_id) = self.services.rooms.alias.resolve(room).await else {
error!(%user_id, "Failed to resolve room alias to room ID when attempting to auto join {room}, skipping");
continue;
};
if !self
.services
.rooms
.state_cache
.server_in_room(self.services.globals.server_name(), room)?
.server_in_room(self.services.globals.server_name(), &room_id)
.await
{
warn!("Skipping room {room} to automatically join as we have never joined before.");
continue;
}
if let Some(room_id_server_name) = room.server_name() {
if let Some(room_server_name) = room.server_name() {
match join_room_by_id_helper(
self.services,
&user_id,
room,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
&[room_id_server_name.to_owned(), self.services.globals.server_name().to_owned()],
&[self.services.globals.server_name().to_owned(), room_server_name.to_owned()],
None,
&None,
)
@@ -123,6 +140,13 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
info!("Automatically joined room {room} for user {user_id}");
},
Err(e) => {
self.services
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Failed to automatically join room {room} for user {user_id}: {e}"
)))
.await
.ok();
// don't return this error so we don't fail registrations
error!("Failed to automatically join room {room} for user {user_id}: {e}");
},
@@ -135,13 +159,14 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
// if this account creation is from the CLI / --execute, invite the first user
// to admin room
if let Some(admin_room) = self.services.admin.get_admin_room()? {
if let Ok(admin_room) = self.services.admin.get_admin_room().await {
if self
.services
.rooms
.state_cache
.room_joined_count(&admin_room)?
== Some(1)
.room_joined_count(&admin_room)
.await
.is_ok_and(is_equal_to!(1))
{
self.services.admin.make_user_admin(&user_id).await?;
@@ -167,7 +192,7 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
));
}
self.services.users.deactivate_account(&user_id)?;
self.services.users.deactivate_account(&user_id).await?;
if !no_leave_rooms {
self.services
@@ -175,17 +200,22 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
.send_message(RoomMessageEventContent::text_plain(format!(
"Making {user_id} leave all rooms after deactivation..."
)))
.await;
.await
.ok();
let all_joined_rooms: Vec<OwnedRoomId> = self
.services
.rooms
.state_cache
.rooms_joined(&user_id)
.filter_map(Result::ok)
.collect();
.map(Into::into)
.collect()
.await;
full_user_deactivate(self.services, &user_id, all_joined_rooms).await?;
full_user_deactivate(self.services, &user_id, &all_joined_rooms).await?;
update_displayname(self.services, &user_id, None, &all_joined_rooms).await?;
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms).await?;
leave_all_rooms(self.services, &user_id).await;
}
Ok(RoomMessageEventContent::text_plain(format!(
@@ -238,15 +268,16 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
let mut admins = Vec::new();
for username in usernames {
match parse_active_local_user_id(self.services, username) {
match parse_active_local_user_id(self.services, username).await {
Ok(user_id) => {
if self.services.users.is_admin(&user_id)? && !force {
if self.services.users.is_admin(&user_id).await && !force {
self.services
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"{username} is an admin and --force is not set, skipping over"
)))
.await;
.await
.ok();
admins.push(username);
continue;
}
@@ -258,7 +289,8 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
.send_message(RoomMessageEventContent::text_plain(format!(
"{username} is the server service account, skipping over"
)))
.await;
.await
.ok();
continue;
}
@@ -270,7 +302,8 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
.send_message(RoomMessageEventContent::text_plain(format!(
"{username} is not a valid username, skipping over: {e}"
)))
.await;
.await
.ok();
continue;
},
}
@@ -279,7 +312,7 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
let mut deactivation_count: usize = 0;
for user_id in user_ids {
match self.services.users.deactivate_account(&user_id) {
match self.services.users.deactivate_account(&user_id).await {
Ok(()) => {
deactivation_count = deactivation_count.saturating_add(1);
if !no_leave_rooms {
@@ -289,16 +322,26 @@ pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) ->
.rooms
.state_cache
.rooms_joined(&user_id)
.filter_map(Result::ok)
.collect();
full_user_deactivate(self.services, &user_id, all_joined_rooms).await?;
.map(Into::into)
.collect()
.await;
full_user_deactivate(self.services, &user_id, &all_joined_rooms).await?;
update_displayname(self.services, &user_id, None, &all_joined_rooms)
.await
.ok();
update_avatar_url(self.services, &user_id, None, None, &all_joined_rooms)
.await
.ok();
leave_all_rooms(self.services, &user_id).await;
}
},
Err(e) => {
self.services
.admin
.send_message(RoomMessageEventContent::text_plain(format!("Failed deactivating user: {e}")))
.await;
.await
.ok();
},
}
}
@@ -326,9 +369,9 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result<RoomMess
.rooms
.state_cache
.rooms_joined(&user_id)
.filter_map(Result::ok)
.map(|room_id| get_room_info(self.services, &room_id))
.collect();
.then(|room_id| get_room_info(self.services, room_id))
.collect()
.await;
if rooms.is_empty() {
return Ok(RoomMessageEventContent::text_plain("User is not in any rooms."));
@@ -350,18 +393,247 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result<RoomMess
Ok(RoomMessageEventContent::notice_markdown(output_plain))
}
#[admin_command]
pub(super) async fn force_join_list_of_local_users(
&self, room_id: OwnedRoomOrAliasId, yes_i_want_to_do_this: bool,
) -> Result<RoomMessageEventContent> {
if self.body.len() < 2 || !self.body[0].trim().starts_with("```") || self.body.last().unwrap_or(&"").trim() != "```"
{
return Ok(RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
));
}
if !yes_i_want_to_do_this {
return Ok(RoomMessageEventContent::notice_markdown(
"You must pass the --yes-i-want-to-do-this-flag to ensure you really want to force bulk join all \
specified local users.",
));
}
let Ok(admin_room) = self.services.admin.get_admin_room().await else {
return Ok(RoomMessageEventContent::notice_markdown(
"There is not an admin room to check for server admins.",
));
};
let (room_id, servers) = self
.services
.rooms
.alias
.resolve_with_servers(&room_id, None)
.await?;
if !self
.services
.rooms
.state_cache
.server_in_room(self.services.globals.server_name(), &room_id)
.await
{
return Ok(RoomMessageEventContent::notice_markdown("We are not joined in this room."));
}
let server_admins: Vec<_> = self
.services
.rooms
.state_cache
.active_local_users_in_room(&admin_room)
.map(ToOwned::to_owned)
.collect()
.await;
if !self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
.await
{
return Ok(RoomMessageEventContent::notice_markdown(
"There is not a single server admin in the room.",
));
}
let usernames = self
.body
.to_vec()
.drain(1..self.body.len().saturating_sub(1))
.collect::<Vec<_>>();
let mut user_ids: Vec<OwnedUserId> = Vec::with_capacity(usernames.len());
for username in usernames {
match parse_active_local_user_id(self.services, username).await {
Ok(user_id) => {
// don't make the server service account join
if user_id == self.services.globals.server_user {
self.services
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"{username} is the server service account, skipping over"
)))
.await
.ok();
continue;
}
user_ids.push(user_id);
},
Err(e) => {
self.services
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"{username} is not a valid username, skipping over: {e}"
)))
.await
.ok();
continue;
},
}
}
let mut failed_joins: usize = 0;
let mut successful_joins: usize = 0;
for user_id in user_ids {
match join_room_by_id_helper(
self.services,
&user_id,
&room_id,
Some(String::from(BULK_JOIN_REASON)),
&servers,
None,
&None,
)
.await
{
Ok(_res) => {
successful_joins = successful_joins.saturating_add(1);
},
Err(e) => {
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!(
"{successful_joins} local users have been joined to {room_id}. {failed_joins} joins failed.",
)))
}
#[admin_command]
pub(super) async fn force_join_all_local_users(
&self, room_id: OwnedRoomOrAliasId, yes_i_want_to_do_this: bool,
) -> Result<RoomMessageEventContent> {
if !yes_i_want_to_do_this {
return Ok(RoomMessageEventContent::notice_markdown(
"You must pass the --yes-i-want-to-do-this-flag to ensure you really want to force bulk join all local \
users.",
));
}
let Ok(admin_room) = self.services.admin.get_admin_room().await else {
return Ok(RoomMessageEventContent::notice_markdown(
"There is not an admin room to check for server admins.",
));
};
let (room_id, servers) = self
.services
.rooms
.alias
.resolve_with_servers(&room_id, None)
.await?;
if !self
.services
.rooms
.state_cache
.server_in_room(self.services.globals.server_name(), &room_id)
.await
{
return Ok(RoomMessageEventContent::notice_markdown("We are not joined in this room."));
}
let server_admins: Vec<_> = self
.services
.rooms
.state_cache
.active_local_users_in_room(&admin_room)
.map(ToOwned::to_owned)
.collect()
.await;
if !self
.services
.rooms
.state_cache
.room_members(&room_id)
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
.await
{
return Ok(RoomMessageEventContent::notice_markdown(
"There is not a single server admin in the room.",
));
}
let mut failed_joins: usize = 0;
let mut successful_joins: usize = 0;
for user_id in &self
.services
.users
.list_local_users()
.map(UserId::to_owned)
.collect::<Vec<_>>()
.await
{
match join_room_by_id_helper(
self.services,
user_id,
&room_id,
Some(String::from(BULK_JOIN_REASON)),
&servers,
None,
&None,
)
.await
{
Ok(_res) => {
successful_joins = successful_joins.saturating_add(1);
},
Err(e) => {
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!(
"{successful_joins} local users have been joined to {room_id}. {failed_joins} joins failed.",
)))
}
#[admin_command]
pub(super) async fn force_join_room(
&self, user_id: String, room_id: OwnedRoomOrAliasId,
) -> Result<RoomMessageEventContent> {
let user_id = parse_local_user_id(self.services, &user_id)?;
let room_id = self.services.rooms.alias.resolve(&room_id).await?;
let (room_id, servers) = self
.services
.rooms
.alias
.resolve_with_servers(&room_id, None)
.await?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
join_room_by_id_helper(self.services, &user_id, &room_id, None, &[], None, &None).await?;
join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers, None, &None).await?;
Ok(RoomMessageEventContent::notice_markdown(format!(
"{user_id} has been joined to {room_id}.",
@@ -404,10 +676,9 @@ pub(super) async fn force_demote(
.services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomPowerLevels, "")?
.as_ref()
.and_then(|event| serde_json::from_str(event.content.get()).ok()?)
.and_then(|content: RoomPowerLevelsEventContent| content.into());
.room_state_get_content::<RoomPowerLevelsEventContent>(&room_id, &StateEventType::RoomPowerLevels, "")
.await
.ok();
let user_can_demote_self = room_power_levels
.as_ref()
@@ -417,9 +688,9 @@ pub(super) async fn force_demote(
.services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")?
.as_ref()
.is_some_and(|event| event.sender == user_id);
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender == user_id);
if !user_can_demote_self {
return Ok(RoomMessageEventContent::notice_markdown(
@@ -435,14 +706,7 @@ pub(super) async fn force_demote(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_content).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
PduBuilder::state(String::new(), &power_levels_content),
&user_id,
&room_id,
&state_lock,
@@ -473,33 +737,33 @@ pub(super) async fn make_user_admin(&self, user_id: String) -> Result<RoomMessag
pub(super) async fn put_room_tag(
&self, user_id: String, room_id: Box<RoomId>, tag: String,
) -> Result<RoomMessageEventContent> {
let user_id = parse_active_local_user_id(self.services, &user_id)?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
let event = self
let mut tags_event = self
.services
.account_data
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
let mut tags_event = event.map_or_else(
|| TagEvent {
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
.await
.unwrap_or(TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
},
|e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"),
);
});
tags_event
.content
.tags
.insert(tag.clone().into(), TagInfo::new());
self.services.account_data.update(
Some(&room_id),
&user_id,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)?;
self.services
.account_data
.update(
Some(&room_id),
&user_id,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)
.await?;
Ok(RoomMessageEventContent::text_plain(format!(
"Successfully updated room account data for {user_id} and room {room_id} with tag {tag}"
@@ -510,30 +774,30 @@ pub(super) async fn put_room_tag(
pub(super) async fn delete_room_tag(
&self, user_id: String, room_id: Box<RoomId>, tag: String,
) -> Result<RoomMessageEventContent> {
let user_id = parse_active_local_user_id(self.services, &user_id)?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
let event = self
let mut tags_event = self
.services
.account_data
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
let mut tags_event = event.map_or_else(
|| TagEvent {
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
.await
.unwrap_or(TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
},
|e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"),
);
});
tags_event.content.tags.remove(&tag.clone().into());
self.services.account_data.update(
Some(&room_id),
&user_id,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)?;
self.services
.account_data
.update(
Some(&room_id),
&user_id,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)
.await?;
Ok(RoomMessageEventContent::text_plain(format!(
"Successfully updated room account data for {user_id} and room {room_id}, deleting room tag {tag}"
@@ -542,21 +806,18 @@ pub(super) async fn delete_room_tag(
#[admin_command]
pub(super) async fn get_room_tags(&self, user_id: String, room_id: Box<RoomId>) -> Result<RoomMessageEventContent> {
let user_id = parse_active_local_user_id(self.services, &user_id)?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
let event = self
let tags_event = self
.services
.account_data
.get(Some(&room_id), &user_id, RoomAccountDataEventType::Tag)?;
let tags_event = event.map_or_else(
|| TagEvent {
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
.await
.unwrap_or(TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
},
|e| serde_json::from_str(e.get()).expect("Bad account data in database for user {user_id}"),
);
});
Ok(RoomMessageEventContent::notice_markdown(format!(
"```\n{:#?}\n```",
@@ -566,11 +827,12 @@ pub(super) async fn get_room_tags(&self, user_id: String, room_id: Box<RoomId>)
#[admin_command]
pub(super) async fn redact_event(&self, event_id: Box<EventId>) -> Result<RoomMessageEventContent> {
let Some(event) = self
let Ok(event) = self
.services
.rooms
.timeline
.get_non_outlier_pdu(&event_id)?
.get_non_outlier_pdu(&event_id)
.await
else {
return Ok(RoomMessageEventContent::text_plain("Event does not exist in our database."));
};
@@ -599,16 +861,11 @@ pub(super) async fn redact_event(&self, event_id: Box<EventId>) -> Result<RoomMe
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomRedaction,
content: to_raw_value(&RoomRedactionEventContent {
redacts: Some(event.event_id.clone()),
..PduBuilder::timeline(&RoomRedactionEventContent {
redacts: Some(event.event_id.clone().into()),
reason: Some(reason),
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: None,
redacts: Some(event.event_id),
timestamp: None,
},
&sender_user,
&room_id,
+27
View File
@@ -124,4 +124,31 @@ pub(super) enum UserCommand {
RedactEvent {
event_id: Box<EventId>,
},
/// - Force joins a specified list of local users to join the specified
/// room.
///
/// Specify a codeblock of usernames.
///
/// At least 1 server admin must be in the room to reduce abuse.
///
/// Requires the `--yes-i-want-to-do-this` flag.
ForceJoinListOfLocalUsers {
room_id: OwnedRoomOrAliasId,
#[arg(long)]
yes_i_want_to_do_this: bool,
},
/// - Force joins all local users to the specified room.
///
/// At least 1 server admin must be in the room to reduce abuse.
///
/// Requires the `--yes-i-want-to-do-this` flag.
ForceJoinAllLocalUsers {
room_id: OwnedRoomOrAliasId,
#[arg(long)]
yes_i_want_to_do_this: bool,
},
}
+10 -12
View File
@@ -8,23 +8,21 @@ pub(crate) fn escape_html(s: &str) -> String {
.replace('>', "&gt;")
}
pub(crate) fn get_room_info(services: &Services, id: &RoomId) -> (OwnedRoomId, u64, String) {
pub(crate) async fn get_room_info(services: &Services, room_id: &RoomId) -> (OwnedRoomId, u64, String) {
(
id.into(),
room_id.into(),
services
.rooms
.state_cache
.room_joined_count(id)
.ok()
.flatten()
.room_joined_count(room_id)
.await
.unwrap_or(0),
services
.rooms
.state_accessor
.get_name(id)
.ok()
.flatten()
.unwrap_or_else(|| id.to_string()),
.get_name(room_id)
.await
.unwrap_or_else(|_| room_id.to_string()),
)
}
@@ -46,14 +44,14 @@ pub(crate) fn parse_local_user_id(services: &Services, user_id: &str) -> Result<
}
/// Parses user ID that is an active (not guest or deactivated) local user
pub(crate) fn parse_active_local_user_id(services: &Services, user_id: &str) -> Result<OwnedUserId> {
pub(crate) async fn parse_active_local_user_id(services: &Services, user_id: &str) -> Result<OwnedUserId> {
let user_id = parse_local_user_id(services, user_id)?;
if !services.users.exists(&user_id)? {
if !services.users.exists(&user_id).await {
return Err!("User {user_id:?} does not exist on this server.");
}
if services.users.is_deactivated(&user_id)? {
if services.users.is_deactivated(&user_id).await? {
return Err!("User {user_id:?} is deactivated.");
}
+2 -2
View File
@@ -45,7 +45,7 @@ conduit-core.workspace = true
conduit-database.workspace = true
conduit-service.workspace = true
const-str.workspace = true
futures-util.workspace = true
futures.workspace = true
hmac.workspace = true
http.workspace = true
http-body-util.workspace = true
@@ -59,7 +59,7 @@ ruma.workspace = true
serde_html_form.workspace = true
serde_json.workspace = true
serde.workspace = true
sha-1.workspace = true
sha1.workspace = true
tokio.workspace = true
tracing.workspace = true
+164 -110
View File
@@ -2,7 +2,8 @@ use std::fmt::Write;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduit::{debug_info, error, info, utils, warn, Error, PduBuilder, Result};
use conduit::{debug_info, error, info, is_equal_to, utils, utils::ReadyExt, warn, Error, PduBuilder, Result};
use futures::{FutureExt, StreamExt};
use register::RegistrationKind;
use ruma::{
api::client::{
@@ -20,11 +21,10 @@ use ruma::{
message::RoomMessageEventContent,
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
GlobalAccountDataEventType, StateEventType, TimelineEventType,
GlobalAccountDataEventType, StateEventType,
},
push, OwnedRoomId, UserId,
};
use serde_json::value::to_raw_value;
use service::Services;
use super::{join_room_by_id_helper, DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
@@ -48,14 +48,30 @@ pub(crate) async fn get_register_available_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_username_availability::v3::Request>,
) -> Result<get_username_availability::v3::Response> {
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
let is_matrix_appservice_irc = body.appservice_info.as_ref().is_some_and(|appservice| {
appservice.registration.id == "irc"
|| appservice.registration.id.contains("matrix-appservice-irc")
|| appservice.registration.id.contains("matrix_appservice_irc")
});
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
body.username.clone()
} else {
body.username.to_lowercase()
};
// Validate user id
let user_id = UserId::parse_with_server_name(body.username.to_lowercase(), services.globals.server_name())
let user_id = UserId::parse_with_server_name(body_username, services.globals.server_name())
.ok()
.filter(|user_id| !user_id.is_historical() && services.globals.user_is_local(user_id))
.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."))?;
// Check if username is creative enough
if services.users.exists(&user_id)? {
if services.users.exists(&user_id).await {
return Err(Error::BadRequest(ErrorKind::UserInUse, "Desired user ID is already taken."));
}
@@ -100,8 +116,8 @@ pub(crate) async fn register_route(
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
\"{}\"",
body.username.as_deref().unwrap_or("")
);
return Err(Error::BadRequest(ErrorKind::forbidden(), "Registration has been disabled."));
}
@@ -110,12 +126,12 @@ pub(crate) async fn register_route(
if is_guest
&& (!services.globals.allow_guest_registration()
|| (services.globals.allow_registration() && services.globals.config.registration_token.is_some()))
|| (services.globals.allow_registration() && services.globals.registration_token.is_some()))
{
info!(
"Guest registration disabled / registration enabled with token configured, rejecting guest registration \
attempt, initial device name: {:?}",
body.initial_device_display_name
attempt, initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err(Error::BadRequest(
ErrorKind::GuestAccessForbidden,
@@ -125,24 +141,39 @@ pub(crate) async fn register_route(
// forbid guests from registering if there is not a real admin user yet. give
// generic user error.
if is_guest && services.users.count()? < 2 {
if is_guest && services.users.count().await < 2 {
warn!(
"Guest account attempted to register before a real admin user has been registered, rejecting \
registration. Guest's initial device name: {:?}",
body.initial_device_display_name
registration. Guest's initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err(Error::BadRequest(ErrorKind::forbidden(), "Registration temporarily disabled."));
}
let user_id = match (&body.username, is_guest) {
(Some(username), false) => {
let proposed_user_id =
UserId::parse_with_server_name(username.to_lowercase(), services.globals.server_name())
.ok()
.filter(|user_id| !user_id.is_historical() && services.globals.user_is_local(user_id))
.ok_or(Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
let is_matrix_appservice_irc = body.appservice_info.as_ref().is_some_and(|appservice| {
appservice.registration.id == "irc"
|| appservice.registration.id.contains("matrix-appservice-irc")
|| appservice.registration.id.contains("matrix_appservice_irc")
});
if services.users.exists(&proposed_user_id)? {
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
username.clone()
} else {
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."))?;
if services.users.exists(&proposed_user_id).await {
return Err(Error::BadRequest(ErrorKind::UserInUse, "Desired user ID is already taken."));
}
@@ -162,7 +193,7 @@ pub(crate) async fn register_route(
services.globals.server_name(),
)
.unwrap();
if !services.users.exists(&proposed_user_id)? {
if !services.users.exists(&proposed_user_id).await {
break proposed_user_id;
}
},
@@ -182,7 +213,7 @@ pub(crate) async fn register_route(
// UIAA
let mut uiaainfo;
let skip_auth = if services.globals.config.registration_token.is_some() {
let skip_auth = if services.globals.registration_token.is_some() {
// Registration token required
uiaainfo = UiaaInfo {
flows: vec![AuthFlow {
@@ -210,12 +241,15 @@ 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,
)?;
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));
}
@@ -227,7 +261,7 @@ pub(crate) async fn register_route(
"".into(),
&uiaainfo,
&json,
)?;
);
return Err(Error::Uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
@@ -255,21 +289,23 @@ pub(crate) async fn register_route(
services
.users
.set_displayname(&user_id, Some(displayname.clone()))
.await?;
.set_displayname(&user_id, Some(displayname.clone()));
// Initial account data
services.account_data.update(
None,
&user_id,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: push::Ruleset::server_default(&user_id),
},
})
.expect("to json always works"),
)?;
services
.account_data
.update(
None,
&user_id,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: push::Ruleset::server_default(&user_id),
},
})
.expect("to json always works"),
)
.await?;
// Inhibit login does not work for guests
if !is_guest && body.inhibit_login {
@@ -294,22 +330,27 @@ pub(crate) async fn register_route(
let token = utils::random_string(TOKEN_LENGTH);
// Create device for this account
services.users.create_device(
&user_id,
&device_id,
&token,
body.initial_device_display_name.clone(),
Some(client.to_string()),
)?;
services
.users
.create_device(
&user_id,
&device_id,
&token,
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
.await?;
debug_info!(%user_id, %device_id, "User account was created");
let device_display_name = body.initial_device_display_name.clone().unwrap_or_default();
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
// log in conduit admin channel if a non-guest user registered
if body.appservice_info.is_none() && !is_guest {
if !device_display_name.is_empty() {
info!("New user \"{user_id}\" registered on this server with device display name: {device_display_name}");
info!(
"New user \"{user_id}\" registered on this server with device display name: \"{device_display_name}\""
);
if services.globals.config.admin_room_notices {
services
@@ -318,7 +359,8 @@ pub(crate) async fn register_route(
"New user \"{user_id}\" registered on this server from IP {client} and device display name \
\"{device_display_name}\""
)))
.await;
.await
.ok();
}
} else {
info!("New user \"{user_id}\" registered on this server.");
@@ -329,7 +371,8 @@ pub(crate) async fn register_route(
.send_message(RoomMessageEventContent::notice_plain(format!(
"New user \"{user_id}\" registered on this server from IP {client}"
)))
.await;
.await
.ok();
}
}
}
@@ -346,7 +389,8 @@ pub(crate) async fn register_route(
"Guest user \"{user_id}\" with device display name \"{device_display_name}\" registered on \
this server from IP {client}"
)))
.await;
.await
.ok();
}
} else {
#[allow(clippy::collapsible_else_if)]
@@ -357,7 +401,8 @@ pub(crate) async fn register_route(
"Guest user \"{user_id}\" with no device display name registered on this server from IP \
{client}",
)))
.await;
.await
.ok();
}
}
}
@@ -365,10 +410,15 @@ 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
if !is_guest {
if let Some(admin_room) = services.admin.get_admin_room()? {
if services.rooms.state_cache.room_joined_count(&admin_room)? == Some(1) {
if let Ok(admin_room) = services.admin.get_admin_room().await {
if services
.rooms
.state_cache
.room_joined_count(&admin_room)
.await
.is_ok_and(is_equal_to!(1))
{
services.admin.make_user_admin(&user_id).await?;
warn!("Granting {user_id} admin privileges as the first user");
}
}
@@ -379,25 +429,32 @@ pub(crate) async fn register_route(
&& (services.globals.allow_guests_auto_join_rooms() || !is_guest)
{
for room in &services.globals.config.auto_join_rooms {
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
error!("Failed to resolve room alias to room ID when attempting to auto join {room}, skipping");
continue;
};
if !services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room)?
.server_in_room(services.globals.server_name(), &room_id)
.await
{
warn!("Skipping room {room} to automatically join as we have never joined before.");
continue;
}
if let Some(room_id_server_name) = room.server_name() {
if let Some(room_server_name) = room.server_name() {
if let Err(e) = join_room_by_id_helper(
&services,
&user_id,
room,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
&[room_id_server_name.to_owned(), services.globals.server_name().to_owned()],
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
None,
&body.appservice_info,
)
.boxed()
.await
{
// don't return this error so we don't fail registrations
@@ -461,16 +518,20 @@ pub(crate) async fn change_password_route(
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
// Success!
// 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)?;
.create(sender_user, sender_device, &uiaainfo, &json);
return Err(Error::Uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
@@ -482,14 +543,12 @@ pub(crate) async fn change_password_route(
if body.logout_devices {
// Logout all devices except the current one
for id in services
services
.users
.all_device_ids(sender_user)
.filter_map(Result::ok)
.filter(|id| id != sender_device)
{
services.users.remove_device(sender_user, &id)?;
}
.ready_filter(|id| id != sender_device)
.for_each(|id| services.users.remove_device(sender_user, id))
.await;
}
info!("User {sender_user} changed their password.");
@@ -500,7 +559,8 @@ pub(crate) async fn change_password_route(
.send_message(RoomMessageEventContent::notice_plain(format!(
"User {sender_user} changed their password."
)))
.await;
.await
.ok();
}
Ok(change_password::v3::Response {})
@@ -520,7 +580,7 @@ pub(crate) async fn whoami_route(
Ok(whoami::v3::Response {
user_id: sender_user.clone(),
device_id,
is_guest: services.users.is_deactivated(sender_user)? && body.appservice_info.is_none(),
is_guest: services.users.is_deactivated(sender_user).await? && body.appservice_info.is_none(),
})
}
@@ -561,7 +621,9 @@ pub(crate) async fn deactivate_route(
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
@@ -570,7 +632,8 @@ pub(crate) async fn deactivate_route(
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json)?;
.create(sender_user, sender_device, &uiaainfo, &json);
return Err(Error::Uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
@@ -581,10 +644,14 @@ pub(crate) async fn deactivate_route(
.rooms
.state_cache
.rooms_joined(sender_user)
.filter_map(Result::ok)
.collect();
.map(Into::into)
.collect()
.await;
full_user_deactivate(&services, sender_user, all_joined_rooms).await?;
super::update_displayname(&services, sender_user, None, &all_joined_rooms).await?;
super::update_avatar_url(&services, sender_user, None, None, &all_joined_rooms).await?;
full_user_deactivate(&services, sender_user, &all_joined_rooms).await?;
info!("User {sender_user} deactivated their account.");
@@ -594,7 +661,8 @@ pub(crate) async fn deactivate_route(
.send_message(RoomMessageEventContent::notice_plain(format!(
"User {sender_user} deactivated their account."
)))
.await;
.await
.ok();
}
Ok(deactivate::v3::Response {
@@ -654,7 +722,7 @@ pub(crate) async fn request_3pid_management_token_via_msisdn_route(
pub(crate) async fn check_registration_token_validity(
State(services): State<crate::State>, body: Ruma<check_registration_token_validity::v1::Request>,
) -> Result<check_registration_token_validity::v1::Response> {
let Some(reg_token) = services.globals.config.registration_token.clone() else {
let Some(reg_token) = services.globals.registration_token.clone() else {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Server does not allow token registration.",
@@ -674,34 +742,27 @@ pub(crate) async fn check_registration_token_validity(
/// - Removing all profile data
/// - Leaving all rooms (and forgets all of them)
pub async fn full_user_deactivate(
services: &Services, user_id: &UserId, all_joined_rooms: Vec<OwnedRoomId>,
services: &Services, user_id: &UserId, all_joined_rooms: &[OwnedRoomId],
) -> Result<()> {
services.users.deactivate_account(user_id)?;
services.users.deactivate_account(user_id).await?;
super::update_displayname(services, user_id, None, all_joined_rooms).await?;
super::update_avatar_url(services, user_id, None, None, all_joined_rooms).await?;
super::update_displayname(services, user_id, None, all_joined_rooms.clone()).await?;
super::update_avatar_url(services, user_id, None, None, all_joined_rooms.clone()).await?;
let all_profile_keys = services
services
.users
.all_profile_keys(user_id)
.filter_map(Result::ok);
for (profile_key, _profile_value) in all_profile_keys {
if let Err(e) = services.users.set_profile_key(user_id, &profile_key, None) {
warn!("Failed removing {user_id} profile key {profile_key}: {e}");
}
}
.ready_for_each(|(profile_key, _)| services.users.set_profile_key(user_id, &profile_key, None))
.await;
for room_id in all_joined_rooms {
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let room_power_levels = services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomPowerLevels, "")?
.as_ref()
.and_then(|event| serde_json::from_str(event.content.get()).ok()?)
.and_then(|content: RoomPowerLevelsEventContent| content.into());
.room_state_get_content::<RoomPowerLevelsEventContent>(room_id, &StateEventType::RoomPowerLevels, "")
.await
.ok();
let user_can_demote_self = room_power_levels
.as_ref()
@@ -710,9 +771,9 @@ pub async fn full_user_deactivate(
}) || services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")?
.as_ref()
.is_some_and(|event| event.sender == user_id);
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender == user_id);
if user_can_demote_self {
let mut power_levels_content = room_power_levels.unwrap_or_default();
@@ -723,16 +784,9 @@ pub async fn full_user_deactivate(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_content).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
PduBuilder::state(String::new(), &power_levels_content),
user_id,
&room_id,
room_id,
&state_lock,
)
.await
+17 -25
View File
@@ -1,11 +1,9 @@
use axum::extract::State;
use conduit::{debug, Error, Result};
use conduit::{debug, Err, Result};
use futures::StreamExt;
use rand::seq::SliceRandom;
use ruma::{
api::client::{
alias::{create_alias, delete_alias, get_alias},
error::ErrorKind,
},
api::client::alias::{create_alias, delete_alias, get_alias},
OwnedServerName, RoomAliasId, RoomId,
};
use service::Services;
@@ -33,16 +31,17 @@ pub(crate) async fn create_alias_route(
.forbidden_alias_names()
.is_match(body.room_alias.alias())
{
return Err(Error::BadRequest(ErrorKind::forbidden(), "Room alias is forbidden."));
return Err!(Request(Forbidden("Room alias is forbidden.")));
}
if services
.rooms
.alias
.resolve_local_alias(&body.room_alias)?
.is_some()
.resolve_local_alias(&body.room_alias)
.await
.is_ok()
{
return Err(Error::Conflict("Alias already exists."));
return Err!(Conflict("Alias already exists."));
}
services
@@ -87,39 +86,32 @@ pub(crate) async fn get_alias_route(
State(services): State<crate::State>, body: Ruma<get_alias::v3::Request>,
) -> Result<get_alias::v3::Response> {
let room_alias = body.body.room_alias;
let servers = None;
let Ok((room_id, pre_servers)) = services
.rooms
.alias
.resolve_alias(&room_alias, servers.as_ref())
.await
else {
return Err(Error::BadRequest(ErrorKind::NotFound, "Room with alias not found."));
let Ok((room_id, servers)) = services.rooms.alias.resolve_alias(&room_alias, None).await else {
return Err!(Request(NotFound("Room with alias not found.")));
};
let servers = room_available_servers(&services, &room_id, &room_alias, &pre_servers);
let servers = room_available_servers(&services, &room_id, &room_alias, servers).await;
debug!(?room_alias, ?room_id, "available servers: {servers:?}");
Ok(get_alias::v3::Response::new(room_id, servers))
}
fn room_available_servers(
services: &Services, room_id: &RoomId, room_alias: &RoomAliasId, pre_servers: &Option<Vec<OwnedServerName>>,
async fn room_available_servers(
services: &Services, room_id: &RoomId, room_alias: &RoomAliasId, pre_servers: Vec<OwnedServerName>,
) -> Vec<OwnedServerName> {
// find active servers in room state cache to suggest
let mut servers: Vec<OwnedServerName> = services
.rooms
.state_cache
.room_servers(room_id)
.filter_map(Result::ok)
.collect();
.map(ToOwned::to_owned)
.collect()
.await;
// push any servers we want in the list already (e.g. responded remote alias
// servers, room alias server itself)
if let Some(pre_servers) = pre_servers {
servers.extend(pre_servers.clone());
};
servers.extend(pre_servers);
servers.sort_unstable();
servers.dedup();
+143 -130
View File
@@ -1,18 +1,16 @@
use axum::extract::State;
use conduit::{err, Err};
use ruma::{
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, delete_backup_keys_for_session, delete_backup_version,
get_backup_info, get_backup_keys, get_backup_keys_for_room, get_backup_keys_for_session,
get_latest_backup_info, update_backup_version,
},
error::ErrorKind,
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, delete_backup_keys_for_session, delete_backup_version,
get_backup_info, get_backup_keys, get_backup_keys_for_room, get_backup_keys_for_session,
get_latest_backup_info, update_backup_version,
},
UInt,
};
use crate::{Error, Result, Ruma};
use crate::{Result, Ruma};
/// # `POST /_matrix/client/r0/room_keys/version`
///
@@ -20,10 +18,9 @@ use crate::{Error, Result, Ruma};
pub(crate) async fn create_backup_version_route(
State(services): State<crate::State>, body: Ruma<create_backup_version::v3::Request>,
) -> Result<create_backup_version::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let version = services
.key_backups
.create_backup(sender_user, &body.algorithm)?;
.create_backup(body.sender_user(), &body.algorithm)?;
Ok(create_backup_version::v3::Response {
version,
@@ -37,10 +34,10 @@ pub(crate) async fn create_backup_version_route(
pub(crate) async fn update_backup_version_route(
State(services): State<crate::State>, body: Ruma<update_backup_version::v3::Request>,
) -> Result<update_backup_version::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services
.key_backups
.update_backup(sender_user, &body.version, &body.algorithm)?;
.update_backup(body.sender_user(), &body.version, &body.algorithm)
.await?;
Ok(update_backup_version::v3::Response {})
}
@@ -51,18 +48,25 @@ pub(crate) async fn update_backup_version_route(
pub(crate) async fn get_latest_backup_info_route(
State(services): State<crate::State>, body: Ruma<get_latest_backup_info::v3::Request>,
) -> Result<get_latest_backup_info::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let (version, algorithm) = services
.key_backups
.get_latest_backup(sender_user)?
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Key backup does not exist."))?;
.get_latest_backup(body.sender_user())
.await
.map_err(|_| err!(Request(NotFound("Key backup does not exist."))))?;
Ok(get_latest_backup_info::v3::Response {
algorithm,
count: (UInt::try_from(services.key_backups.count_keys(sender_user, &version)?)
.expect("user backup keys count should not be that high")),
etag: services.key_backups.get_etag(sender_user, &version)?,
count: (UInt::try_from(
services
.key_backups
.count_keys(body.sender_user(), &version)
.await,
)
.expect("user backup keys count should not be that high")),
etag: services
.key_backups
.get_etag(body.sender_user(), &version)
.await,
version,
})
}
@@ -73,21 +77,23 @@ pub(crate) async fn get_latest_backup_info_route(
pub(crate) async fn get_backup_info_route(
State(services): State<crate::State>, body: Ruma<get_backup_info::v3::Request>,
) -> Result<get_backup_info::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let algorithm = services
.key_backups
.get_backup(sender_user, &body.version)?
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Key backup does not exist."))?;
.get_backup(body.sender_user(), &body.version)
.await
.map_err(|_| err!(Request(NotFound("Key backup does not exist at version {:?}", body.version))))?;
Ok(get_backup_info::v3::Response {
algorithm,
count: (UInt::try_from(
services
.key_backups
.count_keys(sender_user, &body.version)?,
)
.expect("user backup keys count should not be that high")),
etag: services.key_backups.get_etag(sender_user, &body.version)?,
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
version: body.version.clone(),
})
}
@@ -101,11 +107,10 @@ pub(crate) async fn get_backup_info_route(
pub(crate) async fn delete_backup_version_route(
State(services): State<crate::State>, body: Ruma<delete_backup_version::v3::Request>,
) -> Result<delete_backup_version::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services
.key_backups
.delete_backup(sender_user, &body.version)?;
.delete_backup(body.sender_user(), &body.version)
.await;
Ok(delete_backup_version::v3::Response {})
}
@@ -121,36 +126,36 @@ pub(crate) async fn delete_backup_version_route(
pub(crate) async fn add_backup_keys_route(
State(services): State<crate::State>, body: Ruma<add_backup_keys::v3::Request>,
) -> Result<add_backup_keys::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if Some(&body.version)
!= services
.key_backups
.get_latest_backup_version(sender_user)?
.as_ref()
if services
.key_backups
.get_latest_backup_version(body.sender_user())
.await
.is_ok_and(|version| version != body.version)
{
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"You may only manipulate the most recently created version of the backup.",
));
return Err!(Request(InvalidParam(
"You may only manipulate the most recently created version of the backup."
)));
}
for (room_id, room) in &body.rooms {
for (session_id, key_data) in &room.sessions {
services
.key_backups
.add_key(sender_user, &body.version, room_id, session_id, key_data)?;
.add_key(body.sender_user(), &body.version, room_id, session_id, key_data)
.await?;
}
}
Ok(add_backup_keys::v3::Response {
count: (UInt::try_from(
services
.key_backups
.count_keys(sender_user, &body.version)?,
)
.expect("user backup keys count should not be that high")),
etag: services.key_backups.get_etag(sender_user, &body.version)?,
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}
@@ -165,34 +170,34 @@ pub(crate) async fn add_backup_keys_route(
pub(crate) async fn add_backup_keys_for_room_route(
State(services): State<crate::State>, body: Ruma<add_backup_keys_for_room::v3::Request>,
) -> Result<add_backup_keys_for_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if Some(&body.version)
!= services
.key_backups
.get_latest_backup_version(sender_user)?
.as_ref()
if services
.key_backups
.get_latest_backup_version(body.sender_user())
.await
.is_ok_and(|version| version != body.version)
{
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"You may only manipulate the most recently created version of the backup.",
));
return Err!(Request(InvalidParam(
"You may only manipulate the most recently created version of the backup."
)));
}
for (session_id, key_data) in &body.sessions {
services
.key_backups
.add_key(sender_user, &body.version, &body.room_id, session_id, key_data)?;
.add_key(body.sender_user(), &body.version, &body.room_id, session_id, key_data)
.await?;
}
Ok(add_backup_keys_for_room::v3::Response {
count: (UInt::try_from(
services
.key_backups
.count_keys(sender_user, &body.version)?,
)
.expect("user backup keys count should not be that high")),
etag: services.key_backups.get_etag(sender_user, &body.version)?,
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}
@@ -207,32 +212,38 @@ pub(crate) async fn add_backup_keys_for_room_route(
pub(crate) async fn add_backup_keys_for_session_route(
State(services): State<crate::State>, body: Ruma<add_backup_keys_for_session::v3::Request>,
) -> Result<add_backup_keys_for_session::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if Some(&body.version)
!= services
.key_backups
.get_latest_backup_version(sender_user)?
.as_ref()
if services
.key_backups
.get_latest_backup_version(body.sender_user())
.await
.is_ok_and(|version| version != body.version)
{
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"You may only manipulate the most recently created version of the backup.",
));
return Err!(Request(InvalidParam(
"You may only manipulate the most recently created version of the backup."
)));
}
services
.key_backups
.add_key(sender_user, &body.version, &body.room_id, &body.session_id, &body.session_data)?;
.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: (UInt::try_from(
services
.key_backups
.count_keys(sender_user, &body.version)?,
)
.expect("user backup keys count should not be that high")),
etag: services.key_backups.get_etag(sender_user, &body.version)?,
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}
@@ -242,9 +253,10 @@ pub(crate) async fn add_backup_keys_for_session_route(
pub(crate) async fn get_backup_keys_route(
State(services): State<crate::State>, body: Ruma<get_backup_keys::v3::Request>,
) -> Result<get_backup_keys::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let rooms = services.key_backups.get_all(sender_user, &body.version)?;
let rooms = services
.key_backups
.get_all(body.sender_user(), &body.version)
.await;
Ok(get_backup_keys::v3::Response {
rooms,
@@ -257,11 +269,10 @@ pub(crate) async fn get_backup_keys_route(
pub(crate) async fn get_backup_keys_for_room_route(
State(services): State<crate::State>, body: Ruma<get_backup_keys_for_room::v3::Request>,
) -> Result<get_backup_keys_for_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sessions = services
.key_backups
.get_room(sender_user, &body.version, &body.room_id)?;
.get_room(body.sender_user(), &body.version, &body.room_id)
.await;
Ok(get_backup_keys_for_room::v3::Response {
sessions,
@@ -274,12 +285,11 @@ pub(crate) async fn get_backup_keys_for_room_route(
pub(crate) async fn get_backup_keys_for_session_route(
State(services): State<crate::State>, body: Ruma<get_backup_keys_for_session::v3::Request>,
) -> Result<get_backup_keys_for_session::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let key_data = services
.key_backups
.get_session(sender_user, &body.version, &body.room_id, &body.session_id)?
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Backup key not found for this user's session."))?;
.get_session(body.sender_user(), &body.version, &body.room_id, &body.session_id)
.await
.map_err(|_| err!(Request(NotFound(debug_error!("Backup key not found for this user's session.")))))?;
Ok(get_backup_keys_for_session::v3::Response {
key_data,
@@ -292,20 +302,21 @@ pub(crate) async fn get_backup_keys_for_session_route(
pub(crate) async fn delete_backup_keys_route(
State(services): State<crate::State>, body: Ruma<delete_backup_keys::v3::Request>,
) -> Result<delete_backup_keys::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services
.key_backups
.delete_all_keys(sender_user, &body.version)?;
.delete_all_keys(body.sender_user(), &body.version)
.await;
Ok(delete_backup_keys::v3::Response {
count: (UInt::try_from(
services
.key_backups
.count_keys(sender_user, &body.version)?,
)
.expect("user backup keys count should not be that high")),
etag: services.key_backups.get_etag(sender_user, &body.version)?,
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}
@@ -315,20 +326,21 @@ pub(crate) async fn delete_backup_keys_route(
pub(crate) async fn delete_backup_keys_for_room_route(
State(services): State<crate::State>, body: Ruma<delete_backup_keys_for_room::v3::Request>,
) -> Result<delete_backup_keys_for_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services
.key_backups
.delete_room_keys(sender_user, &body.version, &body.room_id)?;
.delete_room_keys(body.sender_user(), &body.version, &body.room_id)
.await;
Ok(delete_backup_keys_for_room::v3::Response {
count: (UInt::try_from(
services
.key_backups
.count_keys(sender_user, &body.version)?,
)
.expect("user backup keys count should not be that high")),
etag: services.key_backups.get_etag(sender_user, &body.version)?,
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}
@@ -338,19 +350,20 @@ pub(crate) async fn delete_backup_keys_for_room_route(
pub(crate) async fn delete_backup_keys_for_session_route(
State(services): State<crate::State>, body: Ruma<delete_backup_keys_for_session::v3::Request>,
) -> Result<delete_backup_keys_for_session::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services
.key_backups
.delete_room_key(sender_user, &body.version, &body.room_id, &body.session_id)?;
.delete_room_key(body.sender_user(), &body.version, &body.room_id, &body.session_id)
.await;
Ok(delete_backup_keys_for_session::v3::Response {
count: (UInt::try_from(
services
.key_backups
.count_keys(sender_user, &body.version)?,
)
.expect("user backup keys count should not be that high")),
etag: services.key_backups.get_etag(sender_user, &body.version)?,
count: services
.key_backups
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
}
+7 -1
View File
@@ -3,7 +3,8 @@ use std::collections::BTreeMap;
use axum::extract::State;
use ruma::{
api::client::discovery::get_capabilities::{
self, Capabilities, RoomVersionStability, RoomVersionsCapability, ThirdPartyIdChangesCapability,
self, Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability,
ThirdPartyIdChangesCapability,
},
RoomVersionId,
};
@@ -43,6 +44,11 @@ pub(crate) async fn get_capabilities_route(
enabled: false,
};
// we dont support generating tokens yet
capabilities.get_login_token = GetLoginTokenCapability {
enabled: false,
};
// MSC4133 capability
capabilities
.set("uk.tcpip.msc4133.profile_fields", json!({"enabled": true}))
+31 -31
View File
@@ -1,4 +1,5 @@
use axum::extract::State;
use conduit::err;
use ruma::{
api::client::{
config::{get_global_account_data, get_room_account_data, set_global_account_data, set_room_account_data},
@@ -22,10 +23,11 @@ pub(crate) async fn set_global_account_data_route(
set_account_data(
&services,
None,
&body.sender_user,
body.sender_user.as_ref(),
&body.event_type.to_string(),
body.data.json(),
)?;
)
.await?;
Ok(set_global_account_data::v3::Response {})
}
@@ -39,10 +41,11 @@ pub(crate) async fn set_room_account_data_route(
set_account_data(
&services,
Some(&body.room_id),
&body.sender_user,
body.sender_user.as_ref(),
&body.event_type.to_string(),
body.data.json(),
)?;
)
.await?;
Ok(set_room_account_data::v3::Response {})
}
@@ -55,17 +58,14 @@ pub(crate) async fn get_global_account_data_route(
) -> Result<get_global_account_data::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event: Box<RawJsonValue> = services
let account_data: ExtractGlobalEventContent = services
.account_data
.get(None, sender_user, body.event_type.to_string().into())?
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Data not found."))?;
let account_data = serde_json::from_str::<ExtractGlobalEventContent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
.content;
.get_global(sender_user, body.event_type.clone())
.await
.map_err(|_| err!(Request(NotFound("Data not found."))))?;
Ok(get_global_account_data::v3::Response {
account_data,
account_data: account_data.content,
})
}
@@ -77,22 +77,19 @@ pub(crate) async fn get_room_account_data_route(
) -> Result<get_room_account_data::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event: Box<RawJsonValue> = services
let account_data: ExtractRoomEventContent = services
.account_data
.get(Some(&body.room_id), sender_user, body.event_type.clone())?
.ok_or_else(|| Error::BadRequest(ErrorKind::NotFound, "Data not found."))?;
let account_data = serde_json::from_str::<ExtractRoomEventContent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
.content;
.get_room(&body.room_id, sender_user, body.event_type.clone())
.await
.map_err(|_| err!(Request(NotFound("Data not found."))))?;
Ok(get_room_account_data::v3::Response {
account_data,
account_data: account_data.content,
})
}
fn set_account_data(
services: &Services, room_id: Option<&RoomId>, sender_user: &Option<OwnedUserId>, event_type: &str,
async fn set_account_data(
services: &Services, room_id: Option<&RoomId>, sender_user: Option<&OwnedUserId>, event_type: &str,
data: &RawJsonValue,
) -> Result<()> {
let sender_user = sender_user.as_ref().expect("user is authenticated");
@@ -100,15 +97,18 @@ fn set_account_data(
let data: serde_json::Value =
serde_json::from_str(data.get()).map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Data is invalid."))?;
services.account_data.update(
room_id,
sender_user,
event_type.into(),
&json!({
"type": event_type,
"content": data,
}),
)?;
services
.account_data
.update(
room_id,
sender_user,
event_type.into(),
&json!({
"type": event_type,
"content": data,
}),
)
.await?;
Ok(())
}
+152 -150
View File
@@ -1,15 +1,31 @@
use std::collections::HashSet;
use std::iter::once;
use axum::extract::State;
use ruma::{
api::client::{context::get_context, error::ErrorKind, filter::LazyLoadOptions},
events::StateEventType,
use conduit::{
at, err, ref_at,
utils::{
future::TryExtExt,
stream::{BroadbandExt, ReadyExt, WidebandExt},
IterStream,
},
Err, Result,
};
use futures::{join, try_join, FutureExt, StreamExt, TryFutureExt};
use ruma::{
api::client::{context::get_context, filter::LazyLoadOptions},
events::StateEventType,
OwnedEventId, UserId,
};
use tracing::error;
use crate::{Error, Result, Ruma};
use crate::{
client::message::{event_filter, ignored_filter, update_lazy, visibility_filter, LazySet},
Ruma,
};
/// # `GET /_matrix/client/r0/rooms/{roomId}/context`
const LIMIT_MAX: usize = 100;
const LIMIT_DEFAULT: usize = 10;
/// # `GET /_matrix/client/r0/rooms/{roomId}/context/{eventId}`
///
/// Allows loading room history around an event.
///
@@ -18,186 +34,172 @@ use crate::{Error, Result, Ruma};
pub(crate) async fn get_context_route(
State(services): State<crate::State>, body: Ruma<get_context::v3::Request>,
) -> Result<get_context::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 filter = &body.filter;
let sender = body.sender();
let (sender_user, _) = sender;
let room_id = &body.room_id;
// Use limit or else 10, with maximum 100
let limit: usize = body
.limit
.try_into()
.unwrap_or(LIMIT_DEFAULT)
.min(LIMIT_MAX);
// some clients, at least element, seem to require knowledge of redundant
// members for "inline" profiles on the timeline to work properly
let (lazy_load_enabled, lazy_load_send_redundant) = match &body.filter.lazy_load_options {
LazyLoadOptions::Enabled {
include_redundant_members,
} => (true, *include_redundant_members),
LazyLoadOptions::Disabled => (false, cfg!(feature = "element_hacks")),
};
let lazy_load_enabled = matches!(filter.lazy_load_options, LazyLoadOptions::Enabled { .. });
let mut lazy_loaded = HashSet::new();
let lazy_load_redundant = if let LazyLoadOptions::Enabled {
include_redundant_members,
} = filter.lazy_load_options
{
include_redundant_members
} else {
false
};
let base_token = services
.rooms
.timeline
.get_pdu_count(&body.event_id)?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Base event id not found."))?;
.get_pdu_count(&body.event_id)
.map_err(|_| err!(Request(NotFound("Event not found."))));
let base_event = services
.rooms
.timeline
.get_pdu(&body.event_id)?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Base event not found."))?;
.get_pdu(&body.event_id)
.map_err(|_| err!(Request(NotFound("Base event not found."))));
let room_id = base_event.room_id.clone();
if !services
let visible = services
.rooms
.state_accessor
.user_can_see_event(sender_user, &room_id, &body.event_id)?
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view this event.",
));
.user_can_see_event(sender_user, &body.room_id, &body.event_id)
.map(Ok);
let (base_token, base_event, visible) = try_join!(base_token, base_event, visible)?;
if base_event.room_id != body.room_id || base_event.event_id != body.event_id {
return Err!(Request(NotFound("Base event not found.")));
}
if !services.rooms.lazy_loading.lazy_load_was_sent_before(
sender_user,
sender_device,
&room_id,
&base_event.sender,
)? || lazy_load_send_redundant
if !visible
|| ignored_filter(&services, (base_token, base_event.clone()), sender_user)
.await
.is_none()
{
lazy_loaded.insert(base_event.sender.as_str().to_owned());
return Err!(Request(Forbidden("You don't have permission to view this event.")));
}
// Use limit or else 10, with maximum 100
let limit = usize::try_from(body.limit).unwrap_or(10).min(100);
let base_event = base_event.to_room_event();
let events_before: Vec<_> = services
let events_before = services
.rooms
.timeline
.pdus_until(sender_user, &room_id, base_token)?
.pdus_rev(Some(sender_user), room_id, Some(base_token));
let events_after = services
.rooms
.timeline
.pdus(Some(sender_user), room_id, Some(base_token));
let (events_before, events_after) = try_join!(events_before, events_after)?;
let events_before = events_before
.ready_filter_map(|item| event_filter(item, filter))
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
.take(limit / 2)
.filter_map(Result::ok) // Remove buggy events
.filter(|(_, pdu)| {
services
.rooms
.state_accessor
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
.unwrap_or(false)
})
.collect();
for (_, event) in &events_before {
if !services.rooms.lazy_loading.lazy_load_was_sent_before(
sender_user,
sender_device,
&room_id,
&event.sender,
)? || lazy_load_send_redundant
{
lazy_loaded.insert(event.sender.as_str().to_owned());
}
}
let events_after = events_after
.ready_filter_map(|item| event_filter(item, filter))
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
.take(limit / 2)
.collect();
let start_token = events_before
let (events_before, events_after): (Vec<_>, Vec<_>) = join!(events_before, events_after);
let state_at = events_after
.last()
.map_or_else(|| base_token.stringify(), |(count, _)| count.stringify());
let events_before: Vec<_> = events_before
.into_iter()
.map(|(_, pdu)| pdu.to_room_event())
.collect();
let events_after: Vec<_> = services
.rooms
.timeline
.pdus_after(sender_user, &room_id, base_token)?
.take(limit / 2)
.filter_map(Result::ok) // Remove buggy events
.filter(|(_, pdu)| {
services
.rooms
.state_accessor
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
.unwrap_or(false)
})
.collect();
for (_, event) in &events_after {
if !services.rooms.lazy_loading.lazy_load_was_sent_before(
sender_user,
sender_device,
&room_id,
&event.sender,
)? || lazy_load_send_redundant
{
lazy_loaded.insert(event.sender.as_str().to_owned());
}
}
let shortstatehash = services
.rooms
.state_accessor
.pdu_shortstatehash(
events_after
.last()
.map_or(&*body.event_id, |(_, e)| &*e.event_id),
)?
.map_or(
services
.rooms
.state
.get_room_shortstatehash(&room_id)?
.expect("All rooms have state"),
|hash| hash,
);
.map(ref_at!(1))
.map_or(body.event_id.as_ref(), |e| e.event_id.as_ref());
let state_ids = services
.rooms
.state_accessor
.state_full_ids(shortstatehash)
.pdu_shortstatehash(state_at)
.or_else(|_| services.rooms.state.get_room_shortstatehash(room_id))
.and_then(|shortstatehash| services.rooms.state_accessor.state_full_ids(shortstatehash))
.map_err(|e| err!(Database("State not found: {e}")))
.await?;
let end_token = events_after
.last()
.map_or_else(|| base_token.stringify(), |(count, _)| count.stringify());
let lazy = once(&(base_token, base_event.clone()))
.chain(events_before.iter())
.chain(events_after.iter())
.stream()
.fold(LazySet::new(), |lazy, item| {
update_lazy(&services, room_id, sender, lazy, item, lazy_load_redundant)
})
.await;
let events_after: Vec<_> = events_after
.into_iter()
.map(|(_, pdu)| pdu.to_room_event())
.collect();
let lazy = &lazy;
let state: Vec<_> = state_ids
.iter()
.stream()
.broad_filter_map(|(shortstatekey, event_id)| {
services
.rooms
.short
.get_statekey_from_short(*shortstatekey)
.map_ok(move |(event_type, state_key)| (event_type, state_key, event_id))
.ok()
})
.ready_filter_map(|(event_type, state_key, event_id)| {
if !lazy_load_enabled || event_type != StateEventType::RoomMember {
return Some(event_id);
}
let mut state = Vec::with_capacity(state_ids.len());
for (shortstatekey, id) in state_ids {
let (event_type, state_key) = services
.rooms
.short
.get_statekey_from_short(shortstatekey)?;
if event_type != StateEventType::RoomMember {
let Some(pdu) = services.rooms.timeline.get_pdu(&id)? else {
error!("Pdu in state not found: {}", id);
continue;
};
state.push(pdu.to_state_event());
} else if !lazy_load_enabled || lazy_loaded.contains(&state_key) {
let Some(pdu) = services.rooms.timeline.get_pdu(&id)? else {
error!("Pdu in state not found: {}", id);
continue;
};
state.push(pdu.to_state_event());
}
}
state_key
.as_str()
.try_into()
.ok()
.filter(|&user_id: &&UserId| lazy.contains(user_id))
.map(|_| event_id)
})
.broad_filter_map(|event_id: &OwnedEventId| services.rooms.timeline.get_pdu(event_id).ok())
.map(|pdu| pdu.to_state_event())
.collect()
.await;
Ok(get_context::v3::Response {
start: Some(start_token),
end: Some(end_token),
events_before,
event: Some(base_event),
events_after,
event: Some(base_event.to_room_event()),
start: events_before
.last()
.map(at!(0))
.or(Some(base_token))
.as_ref()
.map(ToString::to_string),
end: events_after
.last()
.map(at!(0))
.or(Some(base_token))
.as_ref()
.map(ToString::to_string),
events_before: events_before
.into_iter()
.map(at!(1))
.map(|pdu| pdu.to_room_event())
.collect(),
events_after: events_after
.into_iter()
.map(at!(1))
.map(|pdu| pdu.to_room_event())
.collect(),
state,
})
}
+45 -21
View File
@@ -1,8 +1,14 @@
use axum::extract::State;
use ruma::api::client::{
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
error::ErrorKind,
uiaa::{AuthFlow, AuthType, UiaaInfo},
use axum_client_ip::InsecureClientIp;
use conduit::{err, Err};
use futures::StreamExt;
use ruma::{
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;
@@ -19,8 +25,8 @@ pub(crate) async fn get_devices_route(
let devices: Vec<device::Device> = services
.users
.all_devices_metadata(sender_user)
.filter_map(Result::ok) // Filter out buggy devices
.collect();
.collect()
.await;
Ok(get_devices::v3::Response {
devices,
@@ -37,8 +43,9 @@ pub(crate) async fn get_device_route(
let device = services
.users
.get_device_metadata(sender_user, &body.body.device_id)?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Device not found."))?;
.get_device_metadata(sender_user, &body.body.device_id)
.await
.map_err(|_| err!(Request(NotFound("Device not found."))))?;
Ok(get_device::v3::Response {
device,
@@ -48,21 +55,29 @@ pub(crate) async fn get_device_route(
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
///
/// Updates the metadata on a given device of the sender user.
#[tracing::instrument(skip_all, fields(%client), name = "update_device")]
pub(crate) async fn update_device_route(
State(services): State<crate::State>, body: Ruma<update_device::v3::Request>,
State(services): State<crate::State>, 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 mut device = services
.users
.get_device_metadata(sender_user, &body.device_id)?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Device not found."))?;
.get_device_metadata(sender_user, &body.device_id)
.await
.map_err(|_| err!(Request(NotFound("Device not found."))))?;
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)?;
.update_device_metadata(sender_user, &body.device_id, &device)
.await?;
Ok(update_device::v3::Response {})
}
@@ -97,22 +112,28 @@ pub(crate) async fn delete_device_route(
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
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)?;
return Err(Error::Uiaa(uiaainfo));
.create(sender_user, sender_device, &uiaainfo, &json);
return Err!(Uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
return Err!(Request(NotJson("Not json.")));
}
services.users.remove_device(sender_user, &body.device_id)?;
services
.users
.remove_device(sender_user, &body.device_id)
.await;
Ok(delete_device::v3::Response {})
}
@@ -149,7 +170,9 @@ pub(crate) async fn delete_devices_route(
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
@@ -158,14 +181,15 @@ pub(crate) async fn delete_devices_route(
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json)?;
.create(sender_user, sender_device, &uiaainfo, &json);
return Err(Error::Uiaa(uiaainfo));
} else {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
}
for device_id in &body.devices {
services.users.remove_device(sender_user, device_id)?;
services.users.remove_device(sender_user, device_id).await;
}
Ok(delete_devices::v3::Response {})
+106 -103
View File
@@ -1,6 +1,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduit::{err, info, warn, Err, Error, Result};
use conduit::{info, warn, Err, Error, Result};
use futures::{StreamExt, TryFutureExt};
use ruma::{
api::{
client::{
@@ -18,7 +19,7 @@ use ruma::{
},
StateEventType,
},
uint, RoomId, ServerName, UInt, UserId,
uint, OwnedRoomId, RoomId, ServerName, UInt, UserId,
};
use service::Services;
@@ -36,14 +37,12 @@ pub(crate) async fn get_public_rooms_filtered_route(
) -> Result<get_public_rooms_filtered::v3::Response> {
if let Some(server) = &body.server {
if services
.globals
.forbidden_remote_room_directory_server_names()
.server
.config
.forbidden_remote_room_directory_server_names
.contains(server)
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Server is banned on this homeserver.",
));
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
}
@@ -76,14 +75,12 @@ pub(crate) async fn get_public_rooms_route(
) -> Result<get_public_rooms::v3::Response> {
if let Some(server) = &body.server {
if services
.globals
.forbidden_remote_room_directory_server_names()
.server
.config
.forbidden_remote_room_directory_server_names
.contains(server)
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Server is banned on this homeserver.",
));
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
}
@@ -119,16 +116,22 @@ pub(crate) async fn set_room_visibility_route(
) -> Result<set_room_visibility::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services.rooms.metadata.exists(&body.room_id)? {
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"));
}
if services.users.is_deactivated(sender_user).unwrap_or(false) && body.appservice_info.is_none() {
if services
.users
.is_deactivated(sender_user)
.await
.unwrap_or(false)
&& body.appservice_info.is_none()
{
return Err!(Request(Forbidden("Guests cannot publish to room directories")));
}
if !user_can_publish_room(&services, sender_user, &body.room_id)? {
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",
@@ -138,7 +141,7 @@ pub(crate) async fn set_room_visibility_route(
match &body.visibility {
room::Visibility::Public => {
if services.globals.config.lockdown_public_room_directory
&& !services.users.is_admin(sender_user)?
&& !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none()
{
info!(
@@ -164,7 +167,7 @@ pub(crate) async fn set_room_visibility_route(
));
}
services.rooms.directory.set_public(&body.room_id)?;
services.rooms.directory.set_public(&body.room_id);
if services.globals.config.admin_room_notices {
services
@@ -174,7 +177,7 @@ pub(crate) async fn set_room_visibility_route(
}
info!("{sender_user} made {0} public to the room directory", body.room_id);
},
room::Visibility::Private => services.rooms.directory.set_not_public(&body.room_id)?,
room::Visibility::Private => services.rooms.directory.set_not_public(&body.room_id),
_ => {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
@@ -192,13 +195,13 @@ pub(crate) async fn set_room_visibility_route(
pub(crate) async fn get_room_visibility_route(
State(services): State<crate::State>, body: Ruma<get_room_visibility::v3::Request>,
) -> Result<get_room_visibility::v3::Response> {
if !services.rooms.metadata.exists(&body.room_id)? {
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"));
}
Ok(get_room_visibility::v3::Response {
visibility: if services.rooms.directory.is_public_room(&body.room_id)? {
visibility: if services.rooms.directory.is_public_room(&body.room_id).await {
room::Visibility::Public
} else {
room::Visibility::Private
@@ -257,101 +260,41 @@ pub(crate) async fn get_public_rooms_filtered_helper(
}
}
let mut all_rooms: Vec<_> = services
let mut all_rooms: Vec<PublicRoomsChunk> = services
.rooms
.directory
.public_rooms()
.map(|room_id| {
let room_id = room_id?;
let chunk = PublicRoomsChunk {
canonical_alias: services
.rooms
.state_accessor
.get_canonical_alias(&room_id)?,
name: services.rooms.state_accessor.get_name(&room_id)?,
num_joined_members: services
.rooms
.state_cache
.room_joined_count(&room_id)?
.unwrap_or_else(|| {
warn!("Room {} has no member count", room_id);
0
})
.try_into()
.expect("user count should not be that big"),
topic: services
.rooms
.state_accessor
.get_room_topic(&room_id)
.unwrap_or(None),
world_readable: services.rooms.state_accessor.is_world_readable(&room_id)?,
guest_can_join: services
.rooms
.state_accessor
.guest_can_join(&room_id)?,
avatar_url: services
.rooms
.state_accessor
.get_avatar(&room_id)?
.into_option()
.unwrap_or_default()
.url,
join_rule: services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomJoinRules, "")?
.map(|s| {
serde_json::from_str(s.content.get())
.map(|c: RoomJoinRulesEventContent| match c.join_rule {
JoinRule::Public => Some(PublicRoomJoinRule::Public),
JoinRule::Knock => Some(PublicRoomJoinRule::Knock),
_ => None,
})
.map_err(|e| {
err!(Database(error!("Invalid room join rule event in database: {e}")))
})
})
.transpose()?
.flatten()
.ok_or_else(|| Error::bad_database("Missing room join rule event for room."))?,
room_type: services
.rooms
.state_accessor
.get_room_type(&room_id)?,
room_id,
};
Ok(chunk)
})
.filter_map(|r: Result<_>| r.ok()) // Filter out buggy rooms
.filter(|chunk| {
.map(ToOwned::to_owned)
.then(|room_id| public_rooms_chunk(services, room_id))
.filter_map(|chunk| async move {
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) {
return true;
return Some(chunk);
}
}
if let Some(topic) = &chunk.topic {
if topic.to_lowercase().contains(&query) {
return true;
return Some(chunk);
}
}
if let Some(canonical_alias) = &chunk.canonical_alias {
if canonical_alias.as_str().to_lowercase().contains(&query) {
return true;
return Some(chunk);
}
}
false
} else {
// No search term
true
return None;
}
// No search term
Some(chunk)
})
// We need to collect all, so we can sort by member count
.collect();
.collect()
.await;
all_rooms.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members));
@@ -394,22 +337,23 @@ pub(crate) async fn get_public_rooms_filtered_helper(
/// Check whether the user can publish to the room directory via power levels of
/// room history visibility event or room creator
fn user_can_publish_room(services: &Services, user_id: &UserId, room_id: &RoomId) -> Result<bool> {
if let Some(event) = services
async fn user_can_publish_room(services: &Services, user_id: &UserId, room_id: &RoomId) -> Result<bool> {
if let Ok(event) = services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")?
.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"))
.map(|content: RoomPowerLevelsEventContent| {
RoomPowerLevels::from(content).user_can_send_state(user_id, StateEventType::RoomHistoryVisibility)
})
} else if let Some(event) =
services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")?
} else if let Ok(event) = services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await
{
Ok(event.sender == user_id)
} else {
@@ -419,3 +363,62 @@ fn user_can_publish_room(services: &Services, user_id: &UserId, room_id: &RoomId
));
}
}
async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> PublicRoomsChunk {
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(),
room_id,
}
}
+13 -12
View File
@@ -1,10 +1,8 @@
use axum::extract::State;
use ruma::api::client::{
error::ErrorKind,
filter::{create_filter, get_filter},
};
use conduit::err;
use ruma::api::client::filter::{create_filter, get_filter};
use crate::{Error, Result, Ruma};
use crate::{Result, Ruma};
/// # `GET /_matrix/client/r0/user/{userId}/filter/{filterId}`
///
@@ -15,11 +13,13 @@ pub(crate) async fn get_filter_route(
State(services): State<crate::State>, body: Ruma<get_filter::v3::Request>,
) -> Result<get_filter::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let Some(filter) = services.users.get_filter(sender_user, &body.filter_id)? else {
return Err(Error::BadRequest(ErrorKind::NotFound, "Filter not found."));
};
Ok(get_filter::v3::Response::new(filter))
services
.users
.get_filter(sender_user, &body.filter_id)
.await
.map(get_filter::v3::Response::new)
.map_err(|_| err!(Request(NotFound("Filter not found."))))
}
/// # `PUT /_matrix/client/r0/user/{userId}/filter`
@@ -29,7 +29,8 @@ pub(crate) async fn create_filter_route(
State(services): State<crate::State>, body: Ruma<create_filter::v3::Request>,
) -> Result<create_filter::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
Ok(create_filter::v3::Response::new(
services.users.create_filter(sender_user, &body.filter)?,
))
let filter_id = services.users.create_filter(sender_user, &body.filter);
Ok(create_filter::v3::Response::new(filter_id))
}
+109 -91
View File
@@ -4,8 +4,8 @@ use std::{
};
use axum::extract::State;
use conduit::{utils, utils::math::continue_exponential_backoff_secs, Err, Error, Result};
use futures_util::{stream::FuturesUnordered, StreamExt};
use conduit::{err, utils, utils::math::continue_exponential_backoff_secs, Err, Error, Result};
use futures::{stream::FuturesUnordered, StreamExt};
use ruma::{
api::{
client::{
@@ -16,12 +16,15 @@ use ruma::{
federation,
},
serde::Raw,
DeviceKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
};
use serde_json::json;
use super::SESSION_ID_LENGTH;
use crate::{service::Services, Ruma};
use crate::{
service::{users::parse_master_key, Services},
Ruma,
};
/// # `POST /_matrix/client/r0/keys/upload`
///
@@ -33,13 +36,13 @@ use crate::{service::Services, Ruma};
pub(crate) async fn upload_keys_route(
State(services): State<crate::State>, body: Ruma<upload_keys::v3::Request>,
) -> Result<upload_keys::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();
for (key_key, key_value) in &body.one_time_keys {
for (key_id, one_time_key) in &body.one_time_keys {
services
.users
.add_one_time_key(sender_user, sender_device, key_key, key_value)?;
.add_one_time_key(sender_user, sender_device, key_id, one_time_key)
.await?;
}
if let Some(device_keys) = &body.device_keys {
@@ -47,19 +50,22 @@ pub(crate) async fn upload_keys_route(
// This check is needed to assure that signatures are kept
if services
.users
.get_device_keys(sender_user, sender_device)?
.is_none()
.get_device_keys(sender_user, sender_device)
.await
.is_err()
{
services
.users
.add_device_keys(sender_user, sender_device, device_keys)?;
.add_device_keys(sender_user, sender_device, device_keys)
.await;
}
}
Ok(upload_keys::v3::Response {
one_time_key_counts: services
.users
.count_one_time_keys(sender_user, sender_device)?,
.count_one_time_keys(sender_user, sender_device)
.await,
})
}
@@ -120,7 +126,9 @@ pub(crate) async fn upload_signing_keys_route(
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services
.uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
.try_auth(sender_user, sender_device, auth, &uiaainfo)
.await?;
if !worked {
return Err(Error::Uiaa(uiaainfo));
}
@@ -129,20 +137,24 @@ pub(crate) async fn upload_signing_keys_route(
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json)?;
.create(sender_user, sender_device, &uiaainfo, &json);
return Err(Error::Uiaa(uiaainfo));
} else {
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
)?;
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?;
}
Ok(upload_signing_keys::v3::Response {})
@@ -179,9 +191,11 @@ pub(crate) async fn upload_signatures_route(
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Invalid signature value."))?
.to_owned(),
);
services
.users
.sign_key(user_id, key_id, signature, sender_user)?;
.sign_key(user_id, key_id, signature, sender_user)
.await?;
}
}
}
@@ -204,56 +218,52 @@ pub(crate) async fn get_key_changes_route(
let mut device_list_updates = HashSet::new();
let from = body
.from
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?;
let to = body
.to
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?;
device_list_updates.extend(
services
.users
.keys_changed(
sender_user.as_str(),
body.from
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?,
Some(
body.to
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?,
),
)
.filter_map(Result::ok),
.keys_changed(sender_user, from, Some(to))
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
for room_id in services
.rooms
.state_cache
.rooms_joined(sender_user)
.filter_map(Result::ok)
{
let mut rooms_joined = services.rooms.state_cache.rooms_joined(sender_user).boxed();
while let Some(room_id) = rooms_joined.next().await {
device_list_updates.extend(
services
.users
.keys_changed(
room_id.as_ref(),
body.from
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?,
Some(
body.to
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?,
),
)
.filter_map(Result::ok),
.room_keys_changed(room_id, from, Some(to))
.map(|(user_id, _)| user_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
}
Ok(get_key_changes::v3::Response {
changed: device_list_updates.into_iter().collect(),
left: Vec::new(), // TODO
})
}
pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool + Send>(
pub(crate) async fn get_keys_helper<F>(
services: &Services, sender_user: Option<&UserId>, device_keys_input: &BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>,
allowed_signatures: F, include_display_names: bool,
) -> Result<get_keys::v3::Response> {
) -> Result<get_keys::v3::Response>
where
F: Fn(&UserId) -> bool + Send + Sync,
{
let mut master_keys = BTreeMap::new();
let mut self_signing_keys = BTreeMap::new();
let mut user_signing_keys = BTreeMap::new();
@@ -274,56 +284,60 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool + Send>(
if device_ids.is_empty() {
let mut container = BTreeMap::new();
for device_id in services.users.all_device_ids(user_id) {
let device_id = device_id?;
if let Some(mut keys) = services.users.get_device_keys(user_id, &device_id)? {
let mut devices = services.users.all_device_ids(user_id).boxed();
while let Some(device_id) = devices.next().await {
if let Ok(mut keys) = services.users.get_device_keys(user_id, device_id).await {
let metadata = services
.users
.get_device_metadata(user_id, &device_id)?
.ok_or_else(|| Error::bad_database("all_device_keys contained nonexistent device."))?;
.get_device_metadata(user_id, device_id)
.await
.map_err(|_| err!(Database("all_device_keys contained nonexistent device.")))?;
add_unsigned_device_display_name(&mut keys, metadata, include_display_names)
.map_err(|_| Error::bad_database("invalid device keys in database"))?;
.map_err(|_| err!(Database("invalid device keys in database")))?;
container.insert(device_id, keys);
container.insert(device_id.to_owned(), keys);
}
}
device_keys.insert(user_id.to_owned(), container);
} else {
for device_id in device_ids {
let mut container = BTreeMap::new();
if let Some(mut keys) = services.users.get_device_keys(user_id, device_id)? {
if let Ok(mut keys) = services.users.get_device_keys(user_id, device_id).await {
let metadata = services
.users
.get_device_metadata(user_id, device_id)?
.ok_or(Error::BadRequest(
ErrorKind::InvalidParam,
"Tried to get keys for nonexistent device.",
))?;
.get_device_metadata(user_id, device_id)
.await
.map_err(|_| err!(Request(InvalidParam("Tried to get keys for nonexistent device."))))?;
add_unsigned_device_display_name(&mut keys, metadata, include_display_names)
.map_err(|_| Error::bad_database("invalid device keys in database"))?;
.map_err(|_| err!(Database("invalid device keys in database")))?;
container.insert(device_id.to_owned(), keys);
}
device_keys.insert(user_id.to_owned(), container);
}
}
if let Some(master_key) = services
if let Ok(master_key) = services
.users
.get_master_key(sender_user, user_id, &allowed_signatures)?
.get_master_key(sender_user, user_id, &allowed_signatures)
.await
{
master_keys.insert(user_id.to_owned(), master_key);
}
if let Some(self_signing_key) =
services
.users
.get_self_signing_key(sender_user, user_id, &allowed_signatures)?
if let Ok(self_signing_key) = services
.users
.get_self_signing_key(sender_user, user_id, &allowed_signatures)
.await
{
self_signing_keys.insert(user_id.to_owned(), self_signing_key);
}
if Some(user_id) == sender_user {
if let Some(user_signing_key) = services.users.get_user_signing_key(user_id)? {
if let Ok(user_signing_key) = services.users.get_user_signing_key(user_id).await {
user_signing_keys.insert(user_id.to_owned(), user_signing_key);
}
}
@@ -385,24 +399,27 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool + Send>(
while let Some((server, response)) = futures.next().await {
if let Ok(Ok(response)) = response {
for (user, masterkey) in response.master_keys {
let (master_key_id, mut master_key) = services.users.parse_master_key(&user, &masterkey)?;
for (user, master_key) in response.master_keys {
let (master_key_id, mut master_key) = parse_master_key(&user, &master_key)?;
if let Some(our_master_key) =
services
.users
.get_key(&master_key_id, sender_user, &user, &allowed_signatures)?
if let Ok(our_master_key) = services
.users
.get_key(&master_key_id, sender_user, &user, &allowed_signatures)
.await
{
let (_, our_master_key) = services.users.parse_master_key(&user, &our_master_key)?;
master_key.signatures.extend(our_master_key.signatures);
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 */
)?;
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);
}
@@ -449,7 +466,7 @@ fn add_unsigned_device_display_name(
}
pub(crate) async fn claim_keys_helper(
services: &Services, one_time_keys_input: &BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, DeviceKeyAlgorithm>>,
services: &Services, one_time_keys_input: &BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, OneTimeKeyAlgorithm>>,
) -> Result<claim_keys::v3::Response> {
let mut one_time_keys = BTreeMap::new();
@@ -465,9 +482,10 @@ pub(crate) async fn claim_keys_helper(
let mut container = BTreeMap::new();
for (device_id, key_algorithm) in map {
if let Some(one_time_keys) = services
if let Ok(one_time_keys) = services
.users
.take_one_time_key(user_id, device_id, key_algorithm)?
.take_one_time_key(user_id, device_id, key_algorithm)
.await
{
let mut c = BTreeMap::new();
c.insert(one_time_keys.0, one_time_keys.1);
+18 -7
View File
@@ -11,6 +11,7 @@ use conduit_service::{
media::{Dim, FileMeta, CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, MXC_LENGTH},
Services,
};
use reqwest::Url;
use ruma::{
api::client::{
authenticated_media::{
@@ -165,23 +166,33 @@ pub(crate) async fn get_media_preview_route(
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let url = &body.url;
if !services.media.url_preview_allowed(url) {
let url = Url::parse(&body.url).map_err(|e| {
err!(Request(InvalidParam(
debug_warn!(%sender_user, %url, "Requested URL is not valid: {e}")
)))
})?;
if !services.media.url_preview_allowed(&url) {
return Err!(Request(Forbidden(
debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
)));
}
let preview = services.media.get_url_preview(url).await.map_err(|error| {
err!(Request(Unknown(
debug_error!(%sender_user, %url, ?error, "Failed to fetch URL preview.")
)))
})?;
let preview = services
.media
.get_url_preview(&url)
.await
.map_err(|error| {
err!(Request(Unknown(
debug_error!(%sender_user, %url, "Failed to fetch URL preview: {error}")
)))
})?;
serde_json::value::to_raw_value(&preview)
.map(get_media_preview::v1::Response::from_raw_value)
.map_err(|error| {
err!(Request(Unknown(
debug_error!(%sender_user, %url, ?error, "Failed to parse URL preview.")
debug_error!(%sender_user, %url, "Failed to parse URL preview: {error}")
)))
})
}
+16 -9
View File
@@ -8,6 +8,7 @@ use conduit::{
Err, Result,
};
use conduit_service::media::{Dim, FileMeta, CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN};
use reqwest::Url;
use ruma::{
api::client::media::{
create_content, get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
@@ -55,25 +56,31 @@ pub(crate) async fn get_media_preview_legacy_route(
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let url = &body.url;
if !services.media.url_preview_allowed(url) {
let url = Url::parse(&body.url).map_err(|e| {
err!(Request(InvalidParam(
debug_warn!(%sender_user, %url, "Requested URL is not valid: {e}")
)))
})?;
if !services.media.url_preview_allowed(&url) {
return Err!(Request(Forbidden(
debug_warn!(%sender_user, %url, "URL is not allowed to be previewed")
)));
}
let preview = services.media.get_url_preview(url).await.map_err(|e| {
let preview = services.media.get_url_preview(&url).await.map_err(|e| {
err!(Request(Unknown(
debug_error!(%sender_user, %url, "Failed to fetch a URL preview: {e}")
)))
})?;
let res = serde_json::value::to_raw_value(&preview).map_err(|e| {
err!(Request(Unknown(
debug_error!(%sender_user, %url, "Failed to parse a URL preview: {e}")
)))
})?;
Ok(get_media_preview::v3::Response::from_raw_value(res))
serde_json::value::to_raw_value(&preview)
.map(get_media_preview::v3::Response::from_raw_value)
.map_err(|error| {
err!(Request(Unknown(
debug_error!(%sender_user, %url, "Failed to parse URL preview: {error}")
)))
})
}
/// # `GET /_matrix/media/v1/preview_url`
File diff suppressed because it is too large Load Diff
+193 -234
View File
@@ -1,109 +1,53 @@
use std::collections::{BTreeMap, HashSet};
use std::collections::HashSet;
use axum::extract::State;
use conduit::PduCount;
use ruma::{
api::client::{
error::ErrorKind,
filter::{RoomEventFilter, UrlFilter},
message::{get_message_events, send_message_event},
use conduit::{
at, is_equal_to,
utils::{
result::{FlatOk, LogErr},
stream::{BroadbandExt, WidebandExt},
IterStream, ReadyExt,
},
events::{MessageLikeEventType, StateEventType},
RoomId, UserId,
Event, PduCount, Result,
};
use serde_json::{from_str, Value};
use crate::{
service::{pdu::PduBuilder, Services},
utils, Error, PduEvent, Result, Ruma,
use futures::{FutureExt, StreamExt};
use ruma::{
api::{
client::{filter::RoomEventFilter, message::get_message_events},
Direction,
},
events::{AnyStateEvent, StateEventType, TimelineEventType, TimelineEventType::*},
serde::Raw,
DeviceId, OwnedUserId, RoomId, UserId,
};
use service::{rooms::timeline::PdusIterItem, Services};
/// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`
///
/// Send a message event into the room.
///
/// - Is a NOOP if the txn id was already used before and returns the same event
/// id again
/// - The only requirement for the content is that it has to be valid json
/// - Tries to send the event into the room, auth rules will determine if it is
/// allowed
pub(crate) async fn send_message_event_route(
State(services): State<crate::State>, body: Ruma<send_message_event::v3::Request>,
) -> Result<send_message_event::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_deref();
use crate::Ruma;
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
pub(crate) type LazySet = HashSet<OwnedUserId>;
// Forbid m.room.encrypted if encryption is disabled
if MessageLikeEventType::RoomEncrypted == body.event_type && !services.globals.allow_encryption() {
return Err(Error::BadRequest(ErrorKind::forbidden(), "Encryption has been disabled"));
}
/// list of safe and common non-state events to ignore if the user is ignored
const IGNORED_MESSAGE_TYPES: &[TimelineEventType; 16] = &[
RoomMessage,
Sticker,
CallInvite,
CallNotify,
RoomEncrypted,
Image,
File,
Audio,
Voice,
Video,
UnstablePollStart,
PollStart,
KeyVerificationStart,
Reaction,
Emote,
Location,
];
if body.event_type == MessageLikeEventType::CallInvite && services.rooms.directory.is_public_room(&body.room_id)? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Room call invites are not allowed in public rooms",
));
}
// Check if this is a new transaction id
if let Some(response) = services
.transaction_ids
.existing_txnid(sender_user, sender_device, &body.txn_id)?
{
// The client might have sent a txnid of the /sendToDevice endpoint
// This txnid has no response associated with it
if response.is_empty() {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Tried to use txn id already used for an incompatible endpoint.",
));
}
let event_id = utils::string_from_bytes(&response)
.map_err(|_| Error::bad_database("Invalid txnid bytes in database."))?
.try_into()
.map_err(|_| Error::bad_database("Invalid event id in txnid data."))?;
return Ok(send_message_event::v3::Response {
event_id,
});
}
let mut unsigned = BTreeMap::new();
unsigned.insert("transaction_id".to_owned(), body.txn_id.to_string().into());
let event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: body.event_type.to_string().into(),
content: from_str(body.body.body.json().get())
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid JSON body."))?,
unsigned: Some(unsigned),
state_key: None,
redacts: None,
timestamp: if body.appservice_info.is_some() {
body.timestamp
} else {
None
},
},
sender_user,
&body.room_id,
&state_lock,
)
.await?;
services
.transaction_ids
.add_txnid(sender_user, sender_device, &body.txn_id, event_id.as_bytes())?;
drop(state_lock);
Ok(send_message_event::v3::Response::new((*event_id).to_owned()))
}
const LIMIT_MAX: usize = 100;
const LIMIT_DEFAULT: usize = 10;
/// # `GET /_matrix/client/r0/rooms/{roomId}/messages`
///
@@ -114,169 +58,184 @@ pub(crate) async fn send_message_event_route(
pub(crate) async fn get_message_events_route(
State(services): State<crate::State>, body: Ruma<get_message_events::v3::Request>,
) -> Result<get_message_events::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 = body.sender();
let (sender_user, sender_device) = sender;
let room_id = &body.room_id;
let filter = &body.filter;
let from = match body.from.clone() {
Some(from) => PduCount::try_from_string(&from)?,
None => match body.dir {
ruma::api::Direction::Forward => PduCount::min(),
ruma::api::Direction::Backward => PduCount::max(),
},
};
let from: PduCount = body
.from
.as_deref()
.map(str::parse)
.transpose()?
.unwrap_or_else(|| match body.dir {
Direction::Forward => PduCount::min(),
Direction::Backward => PduCount::max(),
});
let to = body
.to
.as_ref()
.and_then(|t| PduCount::try_from_string(t).ok());
let to: Option<PduCount> = body.to.as_deref().map(str::parse).flat_ok();
let limit: usize = body
.limit
.try_into()
.unwrap_or(LIMIT_DEFAULT)
.min(LIMIT_MAX);
services
.rooms
.lazy_loading
.lazy_load_confirm_delivery(sender_user, sender_device, &body.room_id, from)
.await?;
.lazy_load_confirm_delivery(sender_user, sender_device, room_id, from);
let limit = usize::try_from(body.limit).unwrap_or(10).min(100);
let next_token;
let mut resp = get_message_events::v3::Response::new();
let mut lazy_loaded = HashSet::new();
match body.dir {
ruma::api::Direction::Forward => {
let events_after: Vec<_> = services
.rooms
.timeline
.pdus_after(sender_user, &body.room_id, from)?
.filter_map(Result::ok) // Filter out buggy events
.filter(|(_, pdu)| { contains_url_filter(pdu, &body.filter) && visibility_filter(&services, pdu, sender_user, &body.room_id)
})
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
.take(limit)
.collect();
for (_, event) in &events_after {
/* TODO: Remove the not "element_hacks" check when these are resolved:
* https://github.com/vector-im/element-android/issues/3417
* https://github.com/vector-im/element-web/issues/21034
*/
if !cfg!(feature = "element_hacks")
&& !services.rooms.lazy_loading.lazy_load_was_sent_before(
sender_user,
sender_device,
&body.room_id,
&event.sender,
)? {
lazy_loaded.insert(event.sender.clone());
}
lazy_loaded.insert(event.sender.clone());
}
next_token = events_after.last().map(|(count, _)| count).copied();
let events_after: Vec<_> = events_after
.into_iter()
.map(|(_, pdu)| pdu.to_room_event())
.collect();
resp.start = from.stringify();
resp.end = next_token.map(|count| count.stringify());
resp.chunk = events_after;
},
ruma::api::Direction::Backward => {
services
.rooms
.timeline
.backfill_if_required(&body.room_id, from)
.await?;
let events_before: Vec<_> = services
.rooms
.timeline
.pdus_until(sender_user, &body.room_id, from)?
.filter_map(Result::ok) // Filter out buggy events
.filter(|(_, pdu)| {contains_url_filter(pdu, &body.filter) && visibility_filter(&services, pdu, sender_user, &body.room_id)})
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
.take(limit)
.collect();
for (_, event) in &events_before {
/* TODO: Remove the not "element_hacks" check when these are resolved:
* https://github.com/vector-im/element-android/issues/3417
* https://github.com/vector-im/element-web/issues/21034
*/
if !cfg!(feature = "element_hacks")
&& !services.rooms.lazy_loading.lazy_load_was_sent_before(
sender_user,
sender_device,
&body.room_id,
&event.sender,
)? {
lazy_loaded.insert(event.sender.clone());
}
lazy_loaded.insert(event.sender.clone());
}
next_token = events_before.last().map(|(count, _)| count).copied();
let events_before: Vec<_> = events_before
.into_iter()
.map(|(_, pdu)| pdu.to_room_event())
.collect();
resp.start = from.stringify();
resp.end = next_token.map(|count| count.stringify());
resp.chunk = events_before;
},
if matches!(body.dir, Direction::Backward) {
services
.rooms
.timeline
.backfill_if_required(room_id, from)
.boxed()
.await
.log_err()
.ok();
}
resp.state = Vec::new();
for ll_id in &lazy_loaded {
if let Some(member_event) =
services
.rooms
.state_accessor
.room_state_get(&body.room_id, &StateEventType::RoomMember, ll_id.as_str())?
{
resp.state.push(member_event.to_state_event());
}
}
let it = match body.dir {
Direction::Forward => services
.rooms
.timeline
.pdus(Some(sender_user), room_id, Some(from))
.await?
.boxed(),
Direction::Backward => services
.rooms
.timeline
.pdus_rev(Some(sender_user), room_id, Some(from))
.await?
.boxed(),
};
let events: Vec<_> = it
.ready_take_while(|(count, _)| Some(*count) != to)
.ready_filter_map(|item| event_filter(item, filter))
.wide_filter_map(|item| ignored_filter(&services, item, sender_user))
.wide_filter_map(|item| visibility_filter(&services, item, sender_user))
.take(limit)
.collect()
.await;
let lazy = events
.iter()
.stream()
.fold(LazySet::new(), |lazy, item| {
update_lazy(&services, room_id, sender, lazy, item, false)
})
.await;
let state = lazy
.iter()
.stream()
.broad_filter_map(|user_id| get_member_event(&services, room_id, user_id))
.collect()
.await;
let next_token = events.last().map(at!(0));
// remove the feature check when we are sure clients like element can handle it
if !cfg!(feature = "element_hacks") {
if let Some(next_token) = next_token {
services
.rooms
.lazy_loading
.lazy_load_mark_sent(sender_user, sender_device, &body.room_id, lazy_loaded, next_token)
.await;
.lazy_load_mark_sent(sender_user, sender_device, room_id, lazy, next_token);
}
}
Ok(resp)
let chunk = events
.into_iter()
.map(at!(1))
.map(|pdu| pdu.to_room_event())
.collect();
Ok(get_message_events::v3::Response {
start: from.to_string(),
end: next_token.as_ref().map(ToString::to_string),
chunk,
state,
})
}
fn visibility_filter(services: &Services, pdu: &PduEvent, user_id: &UserId, room_id: &RoomId) -> bool {
async fn get_member_event(services: &Services, room_id: &RoomId, user_id: &UserId) -> Option<Raw<AnyStateEvent>> {
services
.rooms
.state_accessor
.user_can_see_event(user_id, room_id, &pdu.event_id)
.unwrap_or(false)
.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())
.await
.map(|member_event| member_event.to_state_event())
.ok()
}
fn contains_url_filter(pdu: &PduEvent, filter: &RoomEventFilter) -> bool {
if filter.url_filter.is_none() {
return true;
pub(crate) async fn update_lazy(
services: &Services, room_id: &RoomId, sender: (&UserId, &DeviceId), mut lazy: LazySet, item: &PdusIterItem,
force: bool,
) -> LazySet {
let (_, event) = &item;
let (sender_user, sender_device) = sender;
/* TODO: Remove the not "element_hacks" check when these are resolved:
* https://github.com/vector-im/element-android/issues/3417
* https://github.com/vector-im/element-web/issues/21034
*/
if force || cfg!(features = "element_hacks") {
lazy.insert(event.sender().into());
return lazy;
}
let content: Value = from_str(pdu.content.get()).unwrap();
match filter.url_filter {
Some(UrlFilter::EventsWithoutUrl) => !content["url"].is_string(),
Some(UrlFilter::EventsWithUrl) => content["url"].is_string(),
None => true,
if lazy.contains(event.sender()) {
return lazy;
}
if !services
.rooms
.lazy_loading
.lazy_load_was_sent_before(sender_user, sender_device, room_id, event.sender())
.await
{
lazy.insert(event.sender().into());
}
lazy
}
pub(crate) async fn ignored_filter(services: &Services, item: PdusIterItem, user_id: &UserId) -> Option<PdusIterItem> {
let (_, pdu) = &item;
// 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;
}
if IGNORED_MESSAGE_TYPES.iter().any(is_equal_to!(&pdu.kind))
&& services.users.user_is_ignored(&pdu.sender, user_id).await
{
return None;
}
Some(item)
}
pub(crate) async fn visibility_filter(
services: &Services, item: PdusIterItem, user_id: &UserId,
) -> Option<PdusIterItem> {
let (_, pdu) = &item;
services
.rooms
.state_accessor
.user_can_see_event(user_id, &pdu.room_id, &pdu.event_id)
.await
.then_some(item)
}
pub(crate) fn event_filter(item: PdusIterItem, filter: &RoomEventFilter) -> Option<PdusIterItem> {
let (_, pdu) = &item;
pdu.matches(filter).then_some(item)
}
+5 -1
View File
@@ -23,6 +23,7 @@ pub(super) mod relations;
pub(super) mod report;
pub(super) mod room;
pub(super) mod search;
pub(super) mod send;
pub(super) mod session;
pub(super) mod space;
pub(super) mod state;
@@ -36,6 +37,7 @@ pub(super) mod unstable;
pub(super) mod unversioned;
pub(super) mod user_directory;
pub(super) mod voip;
pub(super) mod well_known;
pub use account::full_user_deactivate;
pub(super) use account::*;
@@ -52,7 +54,7 @@ pub(super) use keys::*;
pub(super) use media::*;
pub(super) use media_legacy::*;
pub(super) use membership::*;
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room, validate_and_add_event_id};
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room};
pub(super) use message::*;
pub(super) use openid::*;
pub(super) use presence::*;
@@ -65,6 +67,7 @@ pub(super) use relations::*;
pub(super) use report::*;
pub(super) use room::*;
pub(super) use search::*;
pub(super) use send::*;
pub(super) use session::*;
pub(super) use space::*;
pub(super) use state::*;
@@ -78,6 +81,7 @@ pub(super) use unstable::*;
pub(super) use unversioned::*;
pub(super) use user_directory::*;
pub(super) use voip::*;
pub(super) use well_known::*;
/// generated device ID length
const DEVICE_ID_LENGTH: usize = 10;
+9 -7
View File
@@ -28,7 +28,8 @@ pub(crate) async fn set_presence_route(
services
.presence
.set_presence(sender_user, &body.presence, None, None, body.status_msg.clone())?;
.set_presence(sender_user, &body.presence, None, None, body.status_msg.clone())
.await?;
Ok(set_presence::v3::Response {})
}
@@ -49,14 +50,15 @@ pub(crate) async fn get_presence_route(
let mut presence_event = None;
for _room_id in services
let has_shared_rooms = services
.rooms
.user
.get_shared_rooms(vec![sender_user.clone(), body.user_id.clone()])?
{
if let Some(presence) = services.presence.get_presence(&body.user_id)? {
.state_cache
.user_sees_user(sender_user, &body.user_id)
.await;
if has_shared_rooms {
if let Ok(presence) = services.presence.get_presence(&body.user_id).await {
presence_event = Some(presence);
break;
}
}
+106 -128
View File
@@ -1,5 +1,10 @@
use axum::extract::State;
use conduit::{pdu::PduBuilder, warn, Err, Error, Result};
use conduit::{
pdu::PduBuilder,
utils::{stream::TryIgnore, IterStream},
warn, Err, Error, Result,
};
use futures::{StreamExt, TryStreamExt};
use ruma::{
api::{
client::{
@@ -8,11 +13,10 @@ use ruma::{
},
federation,
},
events::{room::member::RoomMemberEventContent, StateEventType, TimelineEventType},
events::{room::member::RoomMemberEventContent, StateEventType},
presence::PresenceState,
OwnedMxcUri, OwnedRoomId, UserId,
};
use serde_json::value::to_raw_value;
use service::Services;
use crate::Ruma;
@@ -35,16 +39,18 @@ pub(crate) async fn set_displayname_route(
.rooms
.state_cache
.rooms_joined(&body.user_id)
.filter_map(Result::ok)
.collect();
.map(ToOwned::to_owned)
.collect()
.await;
update_displayname(&services, &body.user_id, body.displayname.clone(), all_joined_rooms).await?;
update_displayname(&services, &body.user_id, body.displayname.clone(), &all_joined_rooms).await?;
if services.globals.allow_local_presence() {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)?;
.ping_presence(&body.user_id, &PresenceState::Online)
.await?;
}
Ok(set_display_name::v3::Response {})
@@ -72,22 +78,19 @@ pub(crate) async fn get_displayname_route(
)
.await
{
if !services.users.exists(&body.user_id)? {
if !services.users.exists(&body.user_id).await {
services.users.create(&body.user_id, None)?;
}
services
.users
.set_displayname(&body.user_id, response.displayname.clone())
.await?;
.set_displayname(&body.user_id, response.displayname.clone());
services
.users
.set_avatar_url(&body.user_id, response.avatar_url.clone())
.await?;
.set_avatar_url(&body.user_id, response.avatar_url.clone());
services
.users
.set_blurhash(&body.user_id, response.blurhash.clone())
.await?;
.set_blurhash(&body.user_id, response.blurhash.clone());
return Ok(get_display_name::v3::Response {
displayname: response.displayname,
@@ -95,14 +98,14 @@ pub(crate) async fn get_displayname_route(
}
}
if !services.users.exists(&body.user_id)? {
if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over
// federation
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
}
Ok(get_display_name::v3::Response {
displayname: services.users.displayname(&body.user_id)?,
displayname: services.users.displayname(&body.user_id).await.ok(),
})
}
@@ -124,15 +127,16 @@ pub(crate) async fn set_avatar_url_route(
.rooms
.state_cache
.rooms_joined(&body.user_id)
.filter_map(Result::ok)
.collect();
.map(ToOwned::to_owned)
.collect()
.await;
update_avatar_url(
&services,
&body.user_id,
body.avatar_url.clone(),
body.blurhash.clone(),
all_joined_rooms,
&all_joined_rooms,
)
.await?;
@@ -140,7 +144,9 @@ pub(crate) async fn set_avatar_url_route(
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)?;
.ping_presence(&body.user_id, &PresenceState::Online)
.await
.ok();
}
Ok(set_avatar_url::v3::Response {})
@@ -168,22 +174,21 @@ pub(crate) async fn get_avatar_url_route(
)
.await
{
if !services.users.exists(&body.user_id)? {
if !services.users.exists(&body.user_id).await {
services.users.create(&body.user_id, None)?;
}
services
.users
.set_displayname(&body.user_id, response.displayname.clone())
.await?;
.set_displayname(&body.user_id, response.displayname.clone());
services
.users
.set_avatar_url(&body.user_id, response.avatar_url.clone())
.await?;
.set_avatar_url(&body.user_id, response.avatar_url.clone());
services
.users
.set_blurhash(&body.user_id, response.blurhash.clone())
.await?;
.set_blurhash(&body.user_id, response.blurhash.clone());
return Ok(get_avatar_url::v3::Response {
avatar_url: response.avatar_url,
@@ -192,15 +197,15 @@ pub(crate) async fn get_avatar_url_route(
}
}
if !services.users.exists(&body.user_id)? {
if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over
// federation
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
}
Ok(get_avatar_url::v3::Response {
avatar_url: services.users.avatar_url(&body.user_id)?,
blurhash: services.users.blurhash(&body.user_id)?,
avatar_url: services.users.avatar_url(&body.user_id).await.ok(),
blurhash: services.users.blurhash(&body.user_id).await.ok(),
})
}
@@ -226,31 +231,30 @@ pub(crate) async fn get_profile_route(
)
.await
{
if !services.users.exists(&body.user_id)? {
if !services.users.exists(&body.user_id).await {
services.users.create(&body.user_id, None)?;
}
services
.users
.set_displayname(&body.user_id, response.displayname.clone())
.await?;
.set_displayname(&body.user_id, response.displayname.clone());
services
.users
.set_avatar_url(&body.user_id, response.avatar_url.clone())
.await?;
.set_avatar_url(&body.user_id, response.avatar_url.clone());
services
.users
.set_blurhash(&body.user_id, response.blurhash.clone())
.await?;
.set_blurhash(&body.user_id, response.blurhash.clone());
services
.users
.set_timezone(&body.user_id, response.tz.clone())
.await?;
.set_timezone(&body.user_id, response.tz.clone());
for (profile_key, profile_key_value) in &response.custom_profile_fields {
services
.users
.set_profile_key(&body.user_id, profile_key, Some(profile_key_value.clone()))?;
.set_profile_key(&body.user_id, profile_key, Some(profile_key_value.clone()));
}
return Ok(get_profile::v3::Response {
@@ -263,134 +267,108 @@ pub(crate) async fn get_profile_route(
}
}
if !services.users.exists(&body.user_id)? {
if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over
// federation
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found."));
}
Ok(get_profile::v3::Response {
avatar_url: services.users.avatar_url(&body.user_id)?,
blurhash: services.users.blurhash(&body.user_id)?,
displayname: services.users.displayname(&body.user_id)?,
tz: services.users.timezone(&body.user_id)?,
avatar_url: services.users.avatar_url(&body.user_id).await.ok(),
blurhash: services.users.blurhash(&body.user_id).await.ok(),
displayname: services.users.displayname(&body.user_id).await.ok(),
tz: services.users.timezone(&body.user_id).await.ok(),
custom_profile_fields: services
.users
.all_profile_keys(&body.user_id)
.filter_map(Result::ok)
.collect(),
.collect()
.await,
})
}
pub async fn update_displayname(
services: &Services, user_id: &UserId, displayname: Option<String>, all_joined_rooms: Vec<OwnedRoomId>,
services: &Services, user_id: &UserId, displayname: Option<String>, all_joined_rooms: &[OwnedRoomId],
) -> Result<()> {
let current_display_name = services.users.displayname(user_id).unwrap_or_default();
let current_display_name = services.users.displayname(user_id).await.ok();
if displayname == current_display_name {
return Ok(());
}
services
.users
.set_displayname(user_id, displayname.clone())
.await?;
services.users.set_displayname(user_id, displayname.clone());
// Send a new join membership event into all joined rooms
let all_joined_rooms: Vec<_> = all_joined_rooms
.iter()
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
displayname: displayname.clone(),
join_authorized_via_users_server: None,
..serde_json::from_str(
services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())?
.ok_or_else(|| {
Error::bad_database("Tried to send display name update for user not in the room.")
})?
.content
.get(),
)
.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(user_id.to_string()),
redacts: None,
timestamp: None,
},
room_id,
))
})
.filter_map(Result::ok)
.collect();
let mut joined_rooms = Vec::new();
for room_id in all_joined_rooms {
let Ok(content) = services
.rooms
.state_accessor
.room_state_get_content(room_id, &StateEventType::RoomMember, user_id.as_str())
.await
else {
continue;
};
update_all_rooms(services, all_joined_rooms, user_id).await;
let pdu = PduBuilder::state(
user_id.to_string(),
&RoomMemberEventContent {
displayname: displayname.clone(),
join_authorized_via_users_server: None,
..content
},
);
joined_rooms.push((pdu, room_id));
}
update_all_rooms(services, joined_rooms, user_id).await;
Ok(())
}
pub async fn update_avatar_url(
services: &Services, user_id: &UserId, avatar_url: Option<OwnedMxcUri>, blurhash: Option<String>,
all_joined_rooms: Vec<OwnedRoomId>,
all_joined_rooms: &[OwnedRoomId],
) -> Result<()> {
let current_avatar_url = services.users.avatar_url(user_id).unwrap_or_default();
let current_blurhash = services.users.blurhash(user_id).unwrap_or_default();
let current_avatar_url = services.users.avatar_url(user_id).await.ok();
let current_blurhash = services.users.blurhash(user_id).await.ok();
if current_avatar_url == avatar_url && current_blurhash == blurhash {
return Ok(());
}
services
.users
.set_avatar_url(user_id, avatar_url.clone())
.await?;
services
.users
.set_blurhash(user_id, blurhash.clone())
.await?;
services.users.set_avatar_url(user_id, avatar_url.clone());
services.users.set_blurhash(user_id, blurhash.clone());
// Send a new join membership event into all joined rooms
let avatar_url = &avatar_url;
let blurhash = &blurhash;
let all_joined_rooms: Vec<_> = all_joined_rooms
.iter()
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
avatar_url: avatar_url.clone(),
blurhash: blurhash.clone(),
join_authorized_via_users_server: None,
..serde_json::from_str(
services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())?
.ok_or_else(|| {
Error::bad_database("Tried to send avatar URL update for user not in the room.")
})?
.content
.get(),
)
.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(user_id.to_string()),
redacts: None,
timestamp: None,
.try_stream()
.and_then(|room_id: &OwnedRoomId| async move {
let content = services
.rooms
.state_accessor
.room_state_get_content(room_id, &StateEventType::RoomMember, user_id.as_str())
.await?;
let pdu = PduBuilder::state(
user_id.to_string(),
&RoomMemberEventContent {
avatar_url: avatar_url.clone(),
blurhash: blurhash.clone(),
join_authorized_via_users_server: None,
..content
},
room_id,
))
);
Ok((pdu, room_id))
})
.filter_map(Result::ok)
.collect();
.ignore_err()
.collect()
.await;
update_all_rooms(services, all_joined_rooms, user_id).await;
+228 -171
View File
@@ -1,19 +1,19 @@
use axum::extract::State;
use conduit::err;
use conduit::{err, Err};
use ruma::{
api::client::{
error::ErrorKind,
push::{
delete_pushrule, get_pushers, get_pushrule, get_pushrule_actions, get_pushrule_enabled, get_pushrules_all,
set_pusher, set_pushrule, set_pushrule_actions, set_pushrule_enabled, RuleScope,
get_pushrules_global_scope, set_pusher, set_pushrule, set_pushrule_actions, set_pushrule_enabled,
},
},
events::{
push_rules::{PushRulesEvent, PushRulesEventContent},
GlobalAccountDataEventType,
},
push::{InsertPushRuleError, RemovePushRuleError, Ruleset},
CanonicalJsonObject,
push::{InsertPushRuleError, PredefinedContentRuleId, PredefinedOverrideRuleId, RemovePushRuleError, Ruleset},
CanonicalJsonObject, CanonicalJsonValue,
};
use service::Services;
@@ -27,48 +27,113 @@ pub(crate) async fn get_pushrules_all_route(
) -> Result<get_pushrules_all::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let global_ruleset: Ruleset;
let Ok(event) =
services
.account_data
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())
let Some(content_value) = services
.account_data
.get_global::<CanonicalJsonObject>(sender_user, GlobalAccountDataEventType::PushRules)
.await
.ok()
.and_then(|event| event.get("content").cloned())
.filter(CanonicalJsonValue::is_object)
else {
// push rules event doesn't exist, create it and return default
return recreate_push_rules_and_return(&services, sender_user);
};
if let Some(event) = event {
let value = serde_json::from_str::<CanonicalJsonObject>(event.get())
.map_err(|e| err!(Database(warn!("Invalid push rules account data event in database: {e}"))))?;
let Some(content_value) = value.get("content") else {
// user somehow has a push rule event with no content key, recreate it and
// return server default silently
return recreate_push_rules_and_return(&services, sender_user);
};
if content_value.to_string().is_empty() {
// user somehow has a push rule event with empty content, recreate it and return
// server default silently
return recreate_push_rules_and_return(&services, sender_user);
}
let account_data_content = serde_json::from_value::<PushRulesEventContent>(content_value.clone().into())
.map_err(|e| err!(Database(warn!("Invalid push rules account data event in database: {e}"))))?;
global_ruleset = account_data_content.global;
} else {
// user somehow has non-existent push rule event. recreate it and return server
// default silently
return recreate_push_rules_and_return(&services, sender_user);
}
return recreate_push_rules_and_return(&services, sender_user).await;
};
let account_data_content = serde_json::from_value::<PushRulesEventContent>(content_value.into())
.map_err(|e| err!(Database(warn!("Invalid push rules account data event in database: {e}"))))?;
let mut global_ruleset = account_data_content.global;
// remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)]
{
use ruma::push::RuleKind::*;
global_ruleset
.remove(Override, PredefinedOverrideRuleId::ContainsDisplayName)
.ok();
global_ruleset
.remove(Override, PredefinedOverrideRuleId::RoomNotif)
.ok();
global_ruleset
.remove(Content, PredefinedContentRuleId::ContainsUserName)
.ok();
};
Ok(get_pushrules_all::v3::Response {
global: global_ruleset,
})
}
/// # `GET /_matrix/client/r0/pushrules/global/`
///
/// Retrieves the push rules event for this user.
///
/// This appears to be the exact same as `GET /_matrix/client/r0/pushrules/`.
pub(crate) async fn get_pushrules_global_route(
State(services): State<crate::State>, body: Ruma<get_pushrules_global_scope::v3::Request>,
) -> Result<get_pushrules_global_scope::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let Some(content_value) = services
.account_data
.get_global::<CanonicalJsonObject>(sender_user, GlobalAccountDataEventType::PushRules)
.await
.ok()
.and_then(|event| event.get("content").cloned())
.filter(CanonicalJsonValue::is_object)
else {
// user somehow has non-existent push rule event. recreate it and return server
// default silently
services
.account_data
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(PushRulesEvent {
content: PushRulesEventContent {
global: Ruleset::server_default(sender_user),
},
})
.expect("to json always works"),
)
.await?;
return Ok(get_pushrules_global_scope::v3::Response {
global: Ruleset::server_default(sender_user),
});
};
let account_data_content = serde_json::from_value::<PushRulesEventContent>(content_value.into())
.map_err(|e| err!(Database(warn!("Invalid push rules account data event in database: {e}"))))?;
let mut global_ruleset = account_data_content.global;
// remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)]
{
use ruma::push::RuleKind::*;
global_ruleset
.remove(Override, PredefinedOverrideRuleId::ContainsDisplayName)
.ok();
global_ruleset
.remove(Override, PredefinedOverrideRuleId::RoomNotif)
.ok();
global_ruleset
.remove(Content, PredefinedContentRuleId::ContainsUserName)
.ok();
};
Ok(get_pushrules_global_scope::v3::Response {
global: global_ruleset,
})
}
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
///
/// Retrieves a single specified push rule for this user.
@@ -77,16 +142,23 @@ pub(crate) async fn get_pushrule_route(
) -> Result<get_pushrule::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event = services
// remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)]
if body.rule_id.as_str() == PredefinedContentRuleId::ContainsUserName.as_str()
|| body.rule_id.as_str() == PredefinedOverrideRuleId::ContainsDisplayName.as_str()
|| body.rule_id.as_str() == PredefinedOverrideRuleId::RoomNotif.as_str()
{
return Err!(Request(NotFound("Push rule not found.")));
}
let event: PushRulesEvent = services
.account_data
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
.await
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
.content;
let rule = account_data
let rule = event
.content
.global
.get(body.kind.clone(), &body.rule_id)
.map(Into::into);
@@ -100,7 +172,7 @@ pub(crate) async fn get_pushrule_route(
}
}
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
/// # `PUT /_matrix/client/r0/pushrules/global/{kind}/{ruleId}`
///
/// Creates a single specified push rule for this user.
pub(crate) async fn set_pushrule_route(
@@ -109,20 +181,11 @@ pub(crate) async fn set_pushrule_route(
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let body = body.body;
if body.scope != RuleScope::Global {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Scopes other than 'global' are not supported.",
));
}
let event = services
let mut account_data: PushRulesEvent = services
.account_data
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
.await
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
if let Err(error) =
account_data
@@ -155,17 +218,20 @@ pub(crate) async fn set_pushrule_route(
return Err(err);
}
services.account_data.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)?;
services
.account_data
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?;
Ok(set_pushrule::v3::Response {})
}
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
/// # `GET /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/actions`
///
/// Gets the actions of a single specified push rule for this user.
pub(crate) async fn get_pushrule_actions_route(
@@ -173,34 +239,34 @@ pub(crate) async fn get_pushrule_actions_route(
) -> Result<get_pushrule_actions::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if body.scope != RuleScope::Global {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Scopes other than 'global' are not supported.",
));
// remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)]
if body.rule_id.as_str() == PredefinedContentRuleId::ContainsUserName.as_str()
|| body.rule_id.as_str() == PredefinedOverrideRuleId::ContainsDisplayName.as_str()
|| body.rule_id.as_str() == PredefinedOverrideRuleId::RoomNotif.as_str()
{
return Err!(Request(NotFound("Push rule not found.")));
}
let event = services
let event: PushRulesEvent = services
.account_data
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
.await
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
.content;
let global = account_data.global;
let actions = global
let actions = event
.content
.global
.get(body.kind.clone(), &body.rule_id)
.map(|rule| rule.actions().to_owned())
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."))?;
.ok_or(err!(Request(NotFound("Push rule not found."))))?;
Ok(get_pushrule_actions::v3::Response {
actions,
})
}
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
/// # `PUT /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/actions`
///
/// Sets the actions of a single specified push rule for this user.
pub(crate) async fn set_pushrule_actions_route(
@@ -208,20 +274,11 @@ pub(crate) async fn set_pushrule_actions_route(
) -> Result<set_pushrule_actions::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if body.scope != RuleScope::Global {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Scopes other than 'global' are not supported.",
));
}
let event = services
let mut account_data: PushRulesEvent = services
.account_data
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
.await
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
if account_data
.content
@@ -232,17 +289,20 @@ pub(crate) async fn set_pushrule_actions_route(
return Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."));
}
services.account_data.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)?;
services
.account_data
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?;
Ok(set_pushrule_actions::v3::Response {})
}
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
/// # `GET /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/enabled`
///
/// Gets the enabled status of a single specified push rule for this user.
pub(crate) async fn get_pushrule_enabled_route(
@@ -250,33 +310,36 @@ pub(crate) async fn get_pushrule_enabled_route(
) -> Result<get_pushrule_enabled::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if body.scope != RuleScope::Global {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Scopes other than 'global' are not supported.",
));
// remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)]
if body.rule_id.as_str() == PredefinedContentRuleId::ContainsUserName.as_str()
|| body.rule_id.as_str() == PredefinedOverrideRuleId::ContainsDisplayName.as_str()
|| body.rule_id.as_str() == PredefinedOverrideRuleId::RoomNotif.as_str()
{
return Ok(get_pushrule_enabled::v3::Response {
enabled: false,
});
}
let event = services
let event: PushRulesEvent = services
.account_data
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
.await
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
let global = account_data.content.global;
let enabled = global
let enabled = event
.content
.global
.get(body.kind.clone(), &body.rule_id)
.map(ruma::push::AnyPushRuleRef::enabled)
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."))?;
.ok_or(err!(Request(NotFound("Push rule not found."))))?;
Ok(get_pushrule_enabled::v3::Response {
enabled,
})
}
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
/// # `PUT /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/enabled`
///
/// Sets the enabled status of a single specified push rule for this user.
pub(crate) async fn set_pushrule_enabled_route(
@@ -284,20 +347,11 @@ pub(crate) async fn set_pushrule_enabled_route(
) -> Result<set_pushrule_enabled::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if body.scope != RuleScope::Global {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Scopes other than 'global' are not supported.",
));
}
let event = services
let mut account_data: PushRulesEvent = services
.account_data
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
.await
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
if account_data
.content
@@ -308,17 +362,20 @@ pub(crate) async fn set_pushrule_enabled_route(
return Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found."));
}
services.account_data.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)?;
services
.account_data
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?;
Ok(set_pushrule_enabled::v3::Response {})
}
/// # `DELETE /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
/// # `DELETE /_matrix/client/r0/pushrules/global/{kind}/{ruleId}`
///
/// Deletes a single specified push rule for this user.
pub(crate) async fn delete_pushrule_route(
@@ -326,20 +383,11 @@ pub(crate) async fn delete_pushrule_route(
) -> Result<delete_pushrule::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if body.scope != RuleScope::Global {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Scopes other than 'global' are not supported.",
));
}
let event = services
let mut account_data: PushRulesEvent = services
.account_data
.get(None, sender_user, GlobalAccountDataEventType::PushRules.to_string().into())?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "PushRules event not found."))?;
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
.get_global(sender_user, GlobalAccountDataEventType::PushRules)
.await
.map_err(|_| err!(Request(NotFound("PushRules event not found."))))?;
if let Err(error) = account_data
.content
@@ -357,12 +405,15 @@ pub(crate) async fn delete_pushrule_route(
return Err(err);
}
services.account_data.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)?;
services
.account_data
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?;
Ok(delete_pushrule::v3::Response {})
}
@@ -376,7 +427,7 @@ pub(crate) async fn get_pushers_route(
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
Ok(get_pushers::v3::Response {
pushers: services.pusher.get_pushers(sender_user)?,
pushers: services.pusher.get_pushers(sender_user).await,
})
}
@@ -390,27 +441,33 @@ pub(crate) async fn set_pushers_route(
) -> Result<set_pusher::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services.pusher.set_pusher(sender_user, &body.action)?;
services
.pusher
.set_pusher(sender_user, &body.action)
.await?;
Ok(set_pusher::v3::Response::default())
Ok(set_pusher::v3::Response::new())
}
/// user somehow has bad push rules, these must always exist per spec.
/// so recreate it and return server default silently
fn recreate_push_rules_and_return(
async fn recreate_push_rules_and_return(
services: &Services, sender_user: &ruma::UserId,
) -> Result<get_pushrules_all::v3::Response> {
services.account_data.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(PushRulesEvent {
content: PushRulesEventContent {
global: Ruleset::server_default(sender_user),
},
})
.expect("to json always works"),
)?;
services
.account_data
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(PushRulesEvent {
content: PushRulesEventContent {
global: Ruleset::server_default(sender_user),
},
})
.expect("to json always works"),
)
.await?;
Ok(get_pushrules_all::v3::Response {
global: Ruleset::server_default(sender_user),
+54 -36
View File
@@ -31,27 +31,32 @@ pub(crate) async fn set_read_marker_route(
event_id: fully_read.clone(),
},
};
services.account_data.update(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::FullyRead,
&serde_json::to_value(fully_read_event).expect("to json value always works"),
)?;
services
.account_data
.update(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::FullyRead,
&serde_json::to_value(fully_read_event).expect("to json value always works"),
)
.await?;
}
if body.private_read_receipt.is_some() || body.read_receipt.is_some() {
services
.rooms
.user
.reset_notification_counts(sender_user, &body.room_id)?;
.reset_notification_counts(sender_user, &body.room_id);
}
if let Some(event) = &body.private_read_receipt {
let count = services
.rooms
.timeline
.get_pdu_count(event)?
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Event does not exist."))?;
.get_pdu_count(event)
.await
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Event does not exist."))?;
let count = match count {
PduCount::Backfilled(_) => {
return Err(Error::BadRequest(
@@ -64,7 +69,7 @@ pub(crate) async fn set_read_marker_route(
services
.rooms
.read_receipt
.private_read_set(&body.room_id, sender_user, count)?;
.private_read_set(&body.room_id, sender_user, count);
}
if let Some(event) = &body.read_receipt {
@@ -83,14 +88,18 @@ pub(crate) async fn set_read_marker_route(
let mut receipt_content = BTreeMap::new();
receipt_content.insert(event.to_owned(), receipts);
services.rooms.read_receipt.readreceipt_update(
sender_user,
&body.room_id,
&ruma::events::receipt::ReceiptEvent {
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
room_id: body.room_id.clone(),
},
)?;
services
.rooms
.read_receipt
.readreceipt_update(
sender_user,
&body.room_id,
&ruma::events::receipt::ReceiptEvent {
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
room_id: body.room_id.clone(),
},
)
.await;
}
Ok(set_read_marker::v3::Response {})
@@ -111,7 +120,7 @@ pub(crate) async fn create_receipt_route(
services
.rooms
.user
.reset_notification_counts(sender_user, &body.room_id)?;
.reset_notification_counts(sender_user, &body.room_id);
}
match body.receipt_type {
@@ -121,12 +130,15 @@ pub(crate) async fn create_receipt_route(
event_id: body.event_id.clone(),
},
};
services.account_data.update(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::FullyRead,
&serde_json::to_value(fully_read_event).expect("to json value always works"),
)?;
services
.account_data
.update(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::FullyRead,
&serde_json::to_value(fully_read_event).expect("to json value always works"),
)
.await?;
},
create_receipt::v3::ReceiptType::Read => {
let mut user_receipts = BTreeMap::new();
@@ -143,21 +155,27 @@ pub(crate) async fn create_receipt_route(
let mut receipt_content = BTreeMap::new();
receipt_content.insert(body.event_id.clone(), receipts);
services.rooms.read_receipt.readreceipt_update(
sender_user,
&body.room_id,
&ruma::events::receipt::ReceiptEvent {
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
room_id: body.room_id.clone(),
},
)?;
services
.rooms
.read_receipt
.readreceipt_update(
sender_user,
&body.room_id,
&ruma::events::receipt::ReceiptEvent {
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
room_id: body.room_id.clone(),
},
)
.await;
},
create_receipt::v3::ReceiptType::ReadPrivate => {
let count = services
.rooms
.timeline
.get_pdu_count(&body.event_id)?
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Event does not exist."))?;
.get_pdu_count(&body.event_id)
.await
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Event does not exist."))?;
let count = match count {
PduCount::Backfilled(_) => {
return Err(Error::BadRequest(
@@ -170,7 +188,7 @@ pub(crate) async fn create_receipt_route(
services
.rooms
.read_receipt
.private_read_set(&body.room_id, sender_user, count)?;
.private_read_set(&body.room_id, sender_user, count);
},
_ => return Err(Error::bad_database("Unsupported receipt type")),
}
+4 -14
View File
@@ -1,9 +1,5 @@
use axum::extract::State;
use ruma::{
api::client::redact::redact_event,
events::{room::redaction::RoomRedactionEventContent, TimelineEventType},
};
use serde_json::value::to_raw_value;
use ruma::{api::client::redact::redact_event, events::room::redaction::RoomRedactionEventContent};
use crate::{service::pdu::PduBuilder, Result, Ruma};
@@ -25,16 +21,11 @@ pub(crate) async fn redact_event_route(
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomRedaction,
content: to_raw_value(&RoomRedactionEventContent {
redacts: Some(body.event_id.clone().into()),
..PduBuilder::timeline(&RoomRedactionEventContent {
redacts: Some(body.event_id.clone()),
reason: body.reason.clone(),
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: None,
redacts: Some(body.event_id.into()),
timestamp: None,
},
sender_user,
&body.room_id,
@@ -44,8 +35,7 @@ pub(crate) async fn redact_event_route(
drop(state_lock);
let event_id = (*event_id).to_owned();
Ok(redact_event::v3::Response {
event_id,
event_id: event_id.into(),
})
}
+132 -36
View File
@@ -1,30 +1,43 @@
use axum::extract::State;
use ruma::api::client::relations::{
get_relating_events, get_relating_events_with_rel_type, get_relating_events_with_rel_type_and_event_type,
use conduit::{
at,
utils::{result::FlatOk, stream::WidebandExt, IterStream, ReadyExt},
PduCount, Result,
};
use futures::StreamExt;
use ruma::{
api::{
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,
};
use service::{rooms::timeline::PdusIterItem, Services};
use crate::{Result, Ruma};
use crate::Ruma;
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route(
State(services): State<crate::State>, body: Ruma<get_relating_events_with_rel_type_and_event_type::v1::Request>,
) -> Result<get_relating_events_with_rel_type_and_event_type::v1::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let res = services.rooms.pdu_metadata.paginate_relations_with_filter(
sender_user,
paginate_relations_with_filter(
&services,
body.sender_user(),
&body.room_id,
&body.event_id,
&Some(body.event_type.clone()),
&Some(body.rel_type.clone()),
&body.from,
&body.to,
&body.limit,
body.event_type.clone().into(),
body.rel_type.clone().into(),
body.from.as_deref(),
body.to.as_deref(),
body.limit,
body.recurse,
body.dir,
)?;
Ok(get_relating_events_with_rel_type_and_event_type::v1::Response {
)
.await
.map(|res| get_relating_events_with_rel_type_and_event_type::v1::Response {
chunk: res.chunk,
next_batch: res.next_batch,
prev_batch: res.prev_batch,
@@ -36,22 +49,21 @@ pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route(
pub(crate) async fn get_relating_events_with_rel_type_route(
State(services): State<crate::State>, body: Ruma<get_relating_events_with_rel_type::v1::Request>,
) -> Result<get_relating_events_with_rel_type::v1::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let res = services.rooms.pdu_metadata.paginate_relations_with_filter(
sender_user,
paginate_relations_with_filter(
&services,
body.sender_user(),
&body.room_id,
&body.event_id,
&None,
&Some(body.rel_type.clone()),
&body.from,
&body.to,
&body.limit,
None,
body.rel_type.clone().into(),
body.from.as_deref(),
body.to.as_deref(),
body.limit,
body.recurse,
body.dir,
)?;
Ok(get_relating_events_with_rel_type::v1::Response {
)
.await
.map(|res| get_relating_events_with_rel_type::v1::Response {
chunk: res.chunk,
next_batch: res.next_batch,
prev_batch: res.prev_batch,
@@ -63,18 +75,102 @@ pub(crate) async fn get_relating_events_with_rel_type_route(
pub(crate) async fn get_relating_events_route(
State(services): State<crate::State>, body: Ruma<get_relating_events::v1::Request>,
) -> Result<get_relating_events::v1::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services.rooms.pdu_metadata.paginate_relations_with_filter(
sender_user,
paginate_relations_with_filter(
&services,
body.sender_user(),
&body.room_id,
&body.event_id,
&None,
&None,
&body.from,
&body.to,
&body.limit,
None,
None,
body.from.as_deref(),
body.to.as_deref(),
body.limit,
body.recurse,
body.dir,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn paginate_relations_with_filter(
services: &Services, sender_user: &UserId, room_id: &RoomId, target: &EventId,
filter_event_type: Option<TimelineEventType>, filter_rel_type: Option<RelationType>, from: Option<&str>,
to: Option<&str>, limit: Option<UInt>, recurse: bool, dir: Direction,
) -> Result<get_relating_events::v1::Response> {
let start: PduCount = from
.map(str::parse)
.transpose()?
.unwrap_or_else(|| match dir {
Direction::Forward => PduCount::min(),
Direction::Backward => PduCount::max(),
});
let to: Option<PduCount> = to.map(str::parse).flat_ok();
// Use limit or else 30, with maximum 100
let limit: usize = limit
.map(TryInto::try_into)
.flat_ok()
.unwrap_or(30)
.min(100);
// Spec (v1.10) recommends depth of at least 3
let depth: u8 = if recurse {
3
} else {
1
};
let events: Vec<PdusIterItem> = services
.rooms
.pdu_metadata
.get_relations(sender_user, room_id, target, start, limit, depth, dir)
.await
.into_iter()
.filter(|(_, pdu)| {
filter_event_type
.as_ref()
.is_none_or(|kind| *kind == pdu.kind)
})
.filter(|(_, pdu)| {
filter_rel_type
.as_ref()
.is_none_or(|rel_type| pdu.relation_type_equal(rel_type))
})
.stream()
.ready_take_while(|(count, _)| Some(*count) != to)
.wide_filter_map(|item| visibility_filter(services, sender_user, item))
.take(limit)
.collect()
.await;
let next_batch = match dir {
Direction::Forward => events.last(),
Direction::Backward => events.first(),
}
.map(at!(0))
.as_ref()
.map(ToString::to_string);
Ok(get_relating_events::v1::Response {
next_batch,
prev_batch: from.map(Into::into),
recursion_depth: recurse.then_some(depth.into()),
chunk: events
.into_iter()
.map(at!(1))
.map(|pdu| pdu.to_message_like_event())
.collect(),
})
}
async fn visibility_filter(services: &Services, sender_user: &UserId, item: PdusIterItem) -> Option<PdusIterItem> {
let (_, pdu) = &item;
services
.rooms
.state_accessor
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id)
.await
.then_some(item)
}
+101 -58
View File
@@ -1,87 +1,130 @@
use std::time::Duration;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduit::{info, utils::ReadyExt, Err};
use rand::Rng;
use ruma::{
api::client::{error::ErrorKind, room::report_content},
api::client::{
error::ErrorKind,
room::{report_content, report_room},
},
events::room::message,
int, EventId, RoomId, UserId,
};
use tokio::time::sleep;
use tracing::info;
use crate::{
debug_info,
service::{pdu::PduEvent, Services},
utils::HtmlEscape,
Error, Result, Ruma,
};
/// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}`
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
///
/// Reports an inappropriate event to homeserver admins
pub(crate) async fn report_event_route(
State(services): State<crate::State>, body: Ruma<report_content::v3::Request>,
) -> Result<report_content::v3::Response> {
/// Reports an abusive room to homeserver admins
#[tracing::instrument(skip_all, fields(%client), name = "report_room")]
pub(crate) async fn report_room_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<report_room::v3::Request>,
) -> Result<report_room::v3::Response> {
// user authentication
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
info!(
"Received /report request by user {sender_user} for room {} and event ID {}",
body.room_id, body.event_id
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
body.room_id,
body.reason.as_deref().unwrap_or("")
);
delay_response().await;
// check if we know about the reported event ID or if it's invalid
let Some(pdu) = services.rooms.timeline.get_pdu(&body.event_id)? else {
if body.reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err(Error::BadRequest(
ErrorKind::NotFound,
"Event ID is not known to us or Event ID is invalid",
ErrorKind::InvalidParam,
"Reason too long, should be 750 characters or fewer",
));
};
is_report_valid(
&services,
&pdu.event_id,
&body.room_id,
sender_user,
&body.reason,
body.score,
&pdu,
)?;
delay_response().await;
if !services
.rooms
.state_cache
.server_in_room(&services.globals.config.server_name, &body.room_id)
.await
{
return Err!(Request(NotFound(
"Room does not exist to us, no local users have joined at all"
)));
}
// send admin room message that we received the report with an @room ping for
// urgency
services
.admin
.send_message(message::RoomMessageEventContent::text_html(
format!(
"@room Report received from: {}\n\nEvent ID: {}\nRoom ID: {}\nSent By: {}\n\nReport Score: {}\nReport \
Reason: {}",
sender_user.to_owned(),
pdu.event_id,
pdu.room_id,
pdu.sender.clone(),
body.score.unwrap_or_else(|| ruma::Int::from(0)),
body.reason.as_deref().unwrap_or("")
),
format!(
"<details><summary>@room Report received from: <a href=\"https://matrix.to/#/{0}\">{0}\
</a></summary><ul><li>Event Info<ul><li>Event ID: <code>{1}</code>\
<a href=\"https://matrix.to/#/{2}/{1}\">🔗</a></li><li>Room ID: <code>{2}</code>\
</li><li>Sent By: <a href=\"https://matrix.to/#/{3}\">{3}</a></li></ul></li><li>\
Report Info<ul><li>Report Score: {4}</li><li>Report Reason: {5}</li></ul></li>\
</ul></details>",
sender_user.to_owned(),
pdu.event_id.clone(),
pdu.room_id.clone(),
pdu.sender.clone(),
body.score.unwrap_or_else(|| ruma::Int::from(0)),
HtmlEscape(body.reason.as_deref().unwrap_or(""))
),
))
.await;
.send_message(message::RoomMessageEventContent::text_markdown(format!(
"@room Room report received from {} -\n\nRoom ID: {}\n\nReport Reason: {}",
sender_user.to_owned(),
body.room_id,
body.reason.as_deref().unwrap_or("")
)))
.await
.ok();
Ok(report_room::v3::Response {})
}
/// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}`
///
/// Reports an inappropriate event to homeserver admins
#[tracing::instrument(skip_all, fields(%client), name = "report_event")]
pub(crate) async fn report_event_route(
State(services): State<crate::State>, InsecureClientIp(client): InsecureClientIp,
body: Ruma<report_content::v3::Request>,
) -> Result<report_content::v3::Response> {
// user authentication
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
info!(
"Received event report by user {sender_user} for room {} and event ID {}, with reason: \"{}\"",
body.room_id,
body.event_id,
body.reason.as_deref().unwrap_or("")
);
delay_response().await;
// check if we know about the reported event ID or if it's invalid
let Ok(pdu) = services.rooms.timeline.get_pdu(&body.event_id).await else {
return Err!(Request(NotFound("Event ID is not known to us or Event ID is invalid")));
};
is_event_report_valid(
&services,
&pdu.event_id,
&body.room_id,
sender_user,
body.reason.as_ref(),
body.score,
&pdu,
)
.await?;
// send admin room message that we received the report with an @room ping for
// urgency
services
.admin
.send_message(message::RoomMessageEventContent::text_markdown(format!(
"@room Event report received from {} -\n\nEvent ID: {}\nRoom ID: {}\nSent By: {}\n\nReport Score: \
{}\nReport Reason: {}",
sender_user.to_owned(),
pdu.event_id,
pdu.room_id,
pdu.sender,
body.score.unwrap_or_else(|| ruma::Int::from(0)),
body.reason.as_deref().unwrap_or("")
)))
.await
.ok();
Ok(report_content::v3::Response {})
}
@@ -92,9 +135,9 @@ pub(crate) async fn report_event_route(
/// check if score is in valid range
/// check if report reasoning is less than or equal to 750 characters
/// check if reporting user is in the reporting room
fn is_report_valid(
services: &Services, event_id: &EventId, room_id: &RoomId, sender_user: &UserId, reason: &Option<String>,
score: Option<ruma::Int>, pdu: &std::sync::Arc<PduEvent>,
async fn is_event_report_valid(
services: &Services, event_id: &EventId, room_id: &RoomId, sender_user: &UserId, reason: Option<&String>,
score: Option<ruma::Int>, pdu: &PduEvent,
) -> Result<()> {
debug_info!("Checking if report from user {sender_user} for event {event_id} in room {room_id} is valid");
@@ -123,8 +166,8 @@ fn is_report_valid(
.rooms
.state_cache
.room_members(room_id)
.filter_map(Result::ok)
.any(|user_id| user_id == *sender_user)
.ready_any(|user_id| user_id == sender_user)
.await
{
return Err(Error::BadRequest(
ErrorKind::NotFound,
@@ -139,7 +182,7 @@ fn is_report_valid(
/// random delay sending a response per spec suggestion regarding
/// enumerating for potential events existing in our server.
async fn delay_response() {
let time_to_wait = rand::thread_rng().gen_range(3..10);
let time_to_wait = rand::thread_rng().gen_range(2..5);
debug_info!("Got successful /report request, waiting {time_to_wait} seconds before sending successful response.");
sleep(Duration::from_secs(time_to_wait)).await;
}
+40
View File
@@ -0,0 +1,40 @@
use axum::extract::State;
use conduit::{Error, Result};
use futures::StreamExt;
use ruma::api::client::{error::ErrorKind, room::aliases};
use crate::Ruma;
/// # `GET /_matrix/client/r0/rooms/{roomId}/aliases`
///
/// Lists all aliases of the room.
///
/// - Only users joined to the room are allowed to call this, or if
/// `history_visibility` is world readable in the room
pub(crate) async fn get_room_aliases_route(
State(services): State<crate::State>, body: Ruma<aliases::v3::Request>,
) -> Result<aliases::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, &body.room_id)
.await
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view this room.",
));
}
Ok(aliases::v3::Response {
aliases: services
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.map(ToOwned::to_owned)
.collect()
.await,
})
}
@@ -1,11 +1,12 @@
use std::{cmp::max, collections::BTreeMap};
use std::collections::BTreeMap;
use axum::extract::State;
use conduit::{debug_info, debug_warn, err, Err};
use conduit::{debug_info, debug_warn, error, info, pdu::PduBuilder, warn, Err, Error, Result};
use futures::FutureExt;
use ruma::{
api::client::{
error::ErrorKind,
room::{self, aliases, create_room, get_room_event, upgrade_room},
room::{self, create_room},
},
events::{
room::{
@@ -17,36 +18,18 @@ use ruma::{
member::{MembershipState, RoomMemberEventContent},
name::RoomNameEventContent,
power_levels::RoomPowerLevelsEventContent,
tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent,
},
StateEventType, TimelineEventType,
TimelineEventType,
},
int,
serde::{JsonObject, Raw},
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId,
};
use serde_json::{json, value::to_raw_value};
use tracing::{error, info, warn};
use service::{appservice::RegistrationInfo, Services};
use super::invite_helper;
use crate::{
service::{appservice::RegistrationInfo, pdu::PduBuilder, Services},
Error, Result, Ruma,
};
/// Recommended transferable state events list from the spec
const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 9] = &[
StateEventType::RoomServerAcl,
StateEventType::RoomEncryption,
StateEventType::RoomName,
StateEventType::RoomAvatar,
StateEventType::RoomTopic,
StateEventType::RoomGuestAccess,
StateEventType::RoomHistoryVisibility,
StateEventType::RoomJoinRules,
StateEventType::RoomPowerLevels,
];
use crate::{client::invite_helper, Ruma};
/// # `POST /_matrix/client/v3/createRoom`
///
@@ -74,7 +57,7 @@ pub(crate) async fn create_room_route(
if !services.globals.allow_room_creation()
&& body.appservice_info.is_none()
&& !services.users.is_admin(sender_user)?
&& !services.users.is_admin(sender_user).await
{
return Err(Error::BadRequest(ErrorKind::forbidden(), "Room creation has been disabled."));
}
@@ -86,7 +69,7 @@ pub(crate) async fn create_room_route(
};
// check if room ID doesn't already exist instead of erroring on auth check
if services.rooms.short.get_shortroomid(&room_id)?.is_some() {
if services.rooms.short.get_shortroomid(&room_id).await.is_ok() {
return Err(Error::BadRequest(
ErrorKind::RoomInUse,
"Room with that custom room ID already exists",
@@ -95,7 +78,7 @@ pub(crate) async fn create_room_route(
if body.visibility == room::Visibility::Public
&& services.globals.config.lockdown_public_room_directory
&& !services.users.is_admin(sender_user)?
&& !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none()
{
info!(
@@ -118,11 +101,15 @@ pub(crate) async fn create_room_route(
return Err!(Request(Forbidden("Publishing rooms to the room directory is not allowed")));
}
let _short_id = services.rooms.short.get_or_create_shortroomid(&room_id)?;
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&room_id)
.await;
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
let alias: Option<OwnedRoomAliasId> = if let Some(alias) = &body.room_alias_name {
Some(room_alias_check(&services, alias, &body.appservice_info).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
};
@@ -145,8 +132,7 @@ pub(crate) async fn create_room_route(
None => services.globals.default_room_version(),
};
#[allow(clippy::single_match_else)]
let content = match &body.creation_content {
let create_content = match &body.creation_content {
Some(content) => {
use RoomVersionId::*;
@@ -208,16 +194,15 @@ pub(crate) async fn create_room_route(
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&content).expect("event is valid, we just created it"),
unsigned: None,
content: to_raw_value(&create_content).expect("create event content serialization"),
state_key: Some(String::new()),
redacts: None,
timestamp: None,
..Default::default()
},
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
// 2. Let the room creator join
@@ -225,28 +210,21 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: services.users.displayname(sender_user)?,
avatar_url: services.users.avatar_url(sender_user)?,
PduBuilder::state(
sender_user.to_string(),
&RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
is_direct: Some(body.is_direct),
third_party_invite: None,
blurhash: services.users.blurhash(sender_user)?,
reason: None,
join_authorized_via_users_server: None,
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.to_string()),
redacts: None,
timestamp: None,
},
..RoomMemberEventContent::new(MembershipState::Join)
},
),
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
// 3. Power levels
@@ -260,13 +238,21 @@ pub(crate) async fn create_room_route(
let mut users = BTreeMap::from_iter([(sender_user.clone(), int!(100))]);
if preset == RoomPreset::TrustedPrivateChat {
for invite_ in &body.invite {
users.insert(invite_.clone(), int!(100));
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.")));
} 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
continue;
}
users.insert(invite.clone(), int!(100));
}
}
let power_levels_content =
default_power_levels_content(&body.power_level_content_override, &body.visibility, users)?;
default_power_levels_content(body.power_level_content_override.as_ref(), &body.visibility, users)?;
services
.rooms
@@ -274,16 +260,15 @@ pub(crate) async fn create_room_route(
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_content).expect("to_raw_value always works on serde_json::Value"),
unsigned: None,
content: to_raw_value(&power_levels_content).expect("serialized power_levels event content"),
state_key: Some(String::new()),
redacts: None,
timestamp: None,
..Default::default()
},
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
// 4. Canonical room alias
@@ -292,22 +277,18 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCanonicalAlias,
content: to_raw_value(&RoomCanonicalAliasEventContent {
PduBuilder::state(
String::new(),
&RoomCanonicalAliasEventContent {
alias: Some(room_alias_id.to_owned()),
alt_aliases: vec![],
})
.expect("We checked that alias earlier, it must be fine"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
},
),
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
}
@@ -318,23 +299,19 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomJoinRules,
content: to_raw_value(&RoomJoinRulesEventContent::new(match preset {
PduBuilder::state(
String::new(),
&RoomJoinRulesEventContent::new(match preset {
RoomPreset::PublicChat => JoinRule::Public,
// according to spec "invite" is the default
_ => JoinRule::Invite,
}))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
}),
),
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
// 5.2 History Visibility
@@ -342,19 +319,15 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomHistoryVisibility,
content: to_raw_value(&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
PduBuilder::state(
String::new(),
&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared),
),
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
// 5.3 Guest Access
@@ -362,22 +335,18 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomGuestAccess,
content: to_raw_value(&RoomGuestAccessEventContent::new(match preset {
PduBuilder::state(
String::new(),
&RoomGuestAccessEventContent::new(match preset {
RoomPreset::PublicChat => GuestAccess::Forbidden,
_ => GuestAccess::CanJoin,
}))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
}),
),
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
// 6. Events listed in initial_state
@@ -410,6 +379,7 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
.boxed()
.await?;
}
@@ -419,19 +389,12 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomName,
content: to_raw_value(&RoomNameEventContent::new(name.clone()))
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
PduBuilder::state(String::new(), &RoomNameEventContent::new(name.clone())),
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
}
@@ -440,28 +403,35 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomTopic,
content: to_raw_value(&RoomTopicEventContent {
PduBuilder::state(
String::new(),
&RoomTopicEventContent {
topic: topic.clone(),
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
},
),
sender_user,
&room_id,
&state_lock,
)
.boxed()
.await?;
}
// 8. Events implied by invite (and TODO: invite_3pid)
drop(state_lock);
for user_id in &body.invite {
if let Err(e) = invite_helper(&services, sender_user, user_id, &room_id, None, body.is_direct).await {
if services.users.user_is_ignored(sender_user, user_id).await {
return Err!(Request(Forbidden("You cannot invite users you have ignored to rooms.")));
} else if services.users.user_is_ignored(user_id, sender_user).await {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
continue;
}
if let Err(e) = invite_helper(&services, sender_user, user_id, &room_id, None, body.is_direct)
.boxed()
.await
{
warn!(%e, "Failed to send invite");
}
}
@@ -475,7 +445,7 @@ pub(crate) async fn create_room_route(
}
if body.visibility == room::Visibility::Public {
services.rooms.directory.set_public(&room_id)?;
services.rooms.directory.set_public(&room_id);
if services.globals.config.admin_room_notices {
services
@@ -491,353 +461,9 @@ pub(crate) async fn create_room_route(
Ok(create_room::v3::Response::new(room_id))
}
/// # `GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}`
///
/// Gets a single event.
///
/// - You have to currently be joined to the room (TODO: Respect history
/// visibility)
pub(crate) async fn get_room_event_route(
State(services): State<crate::State>, body: Ruma<get_room_event::v3::Request>,
) -> Result<get_room_event::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event = services
.rooms
.timeline
.get_pdu(&body.event_id)?
.ok_or_else(|| err!(Request(NotFound("Event {} not found.", &body.event_id))))?;
if !services
.rooms
.state_accessor
.user_can_see_event(sender_user, &event.room_id, &body.event_id)?
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view this event.",
));
}
let mut event = (*event).clone();
event.add_age()?;
Ok(get_room_event::v3::Response {
event: event.to_room_event(),
})
}
/// # `GET /_matrix/client/r0/rooms/{roomId}/aliases`
///
/// Lists all aliases of the room.
///
/// - Only users joined to the room are allowed to call this, or if
/// `history_visibility` is world readable in the room
pub(crate) async fn get_room_aliases_route(
State(services): State<crate::State>, body: Ruma<aliases::v3::Request>,
) -> Result<aliases::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, &body.room_id)?
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view this room.",
));
}
Ok(aliases::v3::Response {
aliases: services
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.filter_map(Result::ok)
.collect(),
})
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
///
/// Upgrades the room.
///
/// - Creates a replacement room
/// - Sends a tombstone event into the current room
/// - Sender user joins the room
/// - Transfers some state events
/// - Moves local aliases
/// - Modifies old room power levels to prevent users from speaking
pub(crate) async fn upgrade_room_route(
State(services): State<crate::State>, body: Ruma<upgrade_room::v3::Request>,
) -> Result<upgrade_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services
.globals
.supported_room_versions()
.contains(&body.new_version)
{
return Err(Error::BadRequest(
ErrorKind::UnsupportedRoomVersion,
"This server does not support that room version.",
));
}
// Create a replacement room
let replacement_room = RoomId::new(services.globals.server_name());
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&replacement_room)?;
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
// Send a m.room.tombstone event to the old room to indicate that it is not
// intended to be used any further Fail if the sender does not have the required
// permissions
let tombstone_event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomTombstone,
content: to_raw_value(&RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: replacement_room.clone(),
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
sender_user,
&body.room_id,
&state_lock,
)
.await?;
// Change lock to replacement room
drop(state_lock);
let state_lock = services.rooms.state.mutex.lock(&replacement_room).await;
// Get the old room creation event
let mut create_event_content = serde_json::from_str::<CanonicalJsonObject>(
services
.rooms
.state_accessor
.room_state_get(&body.room_id, &StateEventType::RoomCreate, "")?
.ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
.content
.get(),
)
.map_err(|_| Error::bad_database("Invalid room event in database."))?;
// 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(),
));
// Send a m.room.create event containing a predecessor field and the applicable
// room_version
{
use RoomVersionId::*;
match body.new_version {
V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
create_event_content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|e| {
info!("Error forming creation event: {e}");
Error::BadRequest(ErrorKind::BadJson, "Error forming creation event")
})?,
);
},
_ => {
// "creator" key no longer exists in V11+ rooms
create_event_content.remove("creator");
},
}
}
create_event_content.insert(
"room_version".into(),
json!(&body.new_version)
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
);
create_event_content.insert(
"predecessor".into(),
json!(predecessor)
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
);
// Validate creation event content
if serde_json::from_str::<CanonicalJsonObject>(
to_raw_value(&create_event_content)
.expect("Error forming creation event")
.get(),
)
.is_err()
{
return Err(Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"));
}
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_event_content).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
sender_user,
&replacement_room,
&state_lock,
)
.await?;
// Join the new room
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: services.users.displayname(sender_user)?,
avatar_url: services.users.avatar_url(sender_user)?,
is_direct: None,
third_party_invite: None,
blurhash: services.users.blurhash(sender_user)?,
reason: None,
join_authorized_via_users_server: None,
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.to_string()),
redacts: None,
timestamp: None,
},
sender_user,
&replacement_room,
&state_lock,
)
.await?;
// Replicate transferable state events to the new room
for event_type in TRANSFERABLE_STATE_EVENTS {
let event_content = match services
.rooms
.state_accessor
.room_state_get(&body.room_id, event_type, "")?
{
Some(v) => v.content.clone(),
None => continue, // Skipping missing events.
};
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: event_type.to_string().into(),
content: event_content,
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
sender_user,
&replacement_room,
&state_lock,
)
.await?;
}
// Moves any local aliases to the new room
for alias in services
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.filter_map(Result::ok)
{
services
.rooms
.alias
.remove_alias(&alias, sender_user)
.await?;
services
.rooms
.alias
.set_alias(&alias, &replacement_room, sender_user)?;
}
// Get the old room power levels
let mut power_levels_event_content: RoomPowerLevelsEventContent = serde_json::from_str(
services
.rooms
.state_accessor
.room_state_get(&body.room_id, &StateEventType::RoomPowerLevels, "")?
.ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
.content
.get(),
)
.map_err(|_| Error::bad_database("Invalid room event in database."))?;
// Setting events_default and invite to the greater of 50 and users_default + 1
let new_level = max(
int!(50),
power_levels_event_content
.users_default
.checked_add(int!(1))
.ok_or_else(|| {
Error::BadRequest(ErrorKind::BadJson, "users_default power levels event content is not valid")
})?,
);
power_levels_event_content.events_default = new_level;
power_levels_event_content.invite = new_level;
// Modify the power levels in the old room to prevent sending of events and
// inviting new users
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_event_content).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
sender_user,
&body.room_id,
&state_lock,
)
.await?;
drop(state_lock);
// Return the replacement room id
Ok(upgrade_room::v3::Response {
replacement_room,
})
}
/// creates the power_levels_content for the PDU builder
fn default_power_levels_content(
power_level_content_override: &Option<Raw<RoomPowerLevelsEventContent>>, visibility: &room::Visibility,
power_level_content_override: Option<&Raw<RoomPowerLevelsEventContent>>, visibility: &room::Visibility,
users: BTreeMap<OwnedUserId, Int>,
) -> Result<serde_json::Value> {
let mut power_levels_content = serde_json::to_value(RoomPowerLevelsEventContent {
@@ -887,7 +513,7 @@ fn default_power_levels_content(
/// if a room is being created with a room alias, run our checks
async fn room_alias_check(
services: &Services, room_alias_name: &str, appservice_info: &Option<RegistrationInfo>,
services: &Services, room_alias_name: &str, appservice_info: Option<&RegistrationInfo>,
) -> Result<OwnedRoomAliasId> {
// Basic checks on the room alias validity
if room_alias_name.contains(':') {
@@ -921,13 +547,14 @@ async fn room_alias_check(
if services
.rooms
.alias
.resolve_local_alias(&full_room_alias)?
.is_some()
.resolve_local_alias(&full_room_alias)
.await
.is_ok()
{
return Err(Error::BadRequest(ErrorKind::RoomInUse, "Room alias already exists."));
}
if let Some(ref info) = appservice_info {
if let Some(info) = appservice_info {
if !info.aliases.is_match(full_room_alias.as_str()) {
return Err(Error::BadRequest(ErrorKind::Exclusive, "Room alias is not in namespace."));
}
+53
View File
@@ -0,0 +1,53 @@
use axum::extract::State;
use conduit::{err, Err, Event, Result};
use futures::{try_join, FutureExt, TryFutureExt};
use ruma::api::client::room::get_room_event;
use crate::{client::ignored_filter, Ruma};
/// # `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>, ref body: Ruma<get_room_event::v3::Request>,
) -> Result<get_room_event::v3::Response> {
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."))));
let visible = services
.rooms
.state_accessor
.user_can_see_event(body.sender_user(), &body.room_id, &body.event_id)
.map(Ok);
let (token, mut event, visible) = try_join!(token, event, visible)?;
if !visible
|| ignored_filter(&services, (token, event.clone()), body.sender_user())
.await
.is_none()
{
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")));
}
event.add_age().ok();
let event = event.to_room_event();
Ok(get_room_event::v3::Response {
event,
})
}
+72
View File
@@ -0,0 +1,72 @@
use axum::extract::State;
use conduit::{at, utils::BoolExt, Err, Result};
use futures::StreamExt;
use ruma::api::client::room::initial_sync::v3::{PaginationChunk, Request, Response};
use crate::Ruma;
const LIMIT_MAX: usize = 100;
pub(crate) async fn room_initial_sync_route(
State(services): State<crate::State>, body: Ruma<Request>,
) -> Result<Response> {
let room_id = &body.room_id;
if !services
.rooms
.state_accessor
.user_can_see_state_events(body.sender_user(), room_id)
.await
{
return Err!(Request(Forbidden("No room preview available.")));
}
let limit = LIMIT_MAX;
let events: Vec<_> = services
.rooms
.timeline
.pdus_rev(None, room_id, None)
.await?
.take(limit)
.collect()
.await;
let state: Vec<_> = services
.rooms
.state_accessor
.room_state_full_pdus(room_id)
.await?
.into_iter()
.map(|pdu| pdu.to_state_event())
.collect();
let messages = PaginationChunk {
start: events.last().map(at!(0)).as_ref().map(ToString::to_string),
end: events
.first()
.map(at!(0))
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
chunk: events
.into_iter()
.map(at!(1))
.map(|pdu| pdu.to_room_event())
.collect(),
};
Ok(Response {
room_id: room_id.to_owned(),
account_data: None,
state: state.into(),
messages: messages.chunk.is_empty().or_some(messages),
visibility: services.rooms.directory.visibility(room_id).await.into(),
membership: services
.rooms
.state_cache
.user_membership(body.sender_user(), room_id)
.await,
})
}
+10
View File
@@ -0,0 +1,10 @@
mod aliases;
mod create;
mod event;
mod initial_sync;
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,
};
+294
View File
@@ -0,0 +1,294 @@
use std::cmp::max;
use axum::extract::State;
use conduit::{err, info, pdu::PduBuilder, Error, Result};
use futures::StreamExt;
use ruma::{
api::client::{error::ErrorKind, room::upgrade_room},
events::{
room::{
member::{MembershipState, RoomMemberEventContent},
power_levels::RoomPowerLevelsEventContent,
tombstone::RoomTombstoneEventContent,
},
StateEventType, TimelineEventType,
},
int, CanonicalJsonObject, RoomId, RoomVersionId,
};
use serde_json::{json, value::to_raw_value};
use crate::Ruma;
/// Recommended transferable state events list from the spec
const TRANSFERABLE_STATE_EVENTS: &[StateEventType; 9] = &[
StateEventType::RoomServerAcl,
StateEventType::RoomEncryption,
StateEventType::RoomName,
StateEventType::RoomAvatar,
StateEventType::RoomTopic,
StateEventType::RoomGuestAccess,
StateEventType::RoomHistoryVisibility,
StateEventType::RoomJoinRules,
StateEventType::RoomPowerLevels,
];
/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
///
/// Upgrades the room.
///
/// - Creates a replacement room
/// - Sends a tombstone event into the current room
/// - Sender user joins the room
/// - Transfers some state events
/// - Moves local aliases
/// - Modifies old room power levels to prevent users from speaking
pub(crate) async fn upgrade_room_route(
State(services): State<crate::State>, body: Ruma<upgrade_room::v3::Request>,
) -> Result<upgrade_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services
.globals
.supported_room_versions()
.contains(&body.new_version)
{
return Err(Error::BadRequest(
ErrorKind::UnsupportedRoomVersion,
"This server does not support that room version.",
));
}
// Create a replacement room
let replacement_room = RoomId::new(services.globals.server_name());
let _short_id = services
.rooms
.short
.get_or_create_shortroomid(&replacement_room)
.await;
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
// Send a m.room.tombstone event to the old room to indicate that it is not
// intended to be used any further Fail if the sender does not have the required
// permissions
let tombstone_event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(
String::new(),
&RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: replacement_room.clone(),
},
),
sender_user,
&body.room_id,
&state_lock,
)
.await?;
// Change lock to replacement room
drop(state_lock);
let state_lock = services.rooms.state.mutex.lock(&replacement_room).await;
// Get the old room creation event
let mut create_event_content: CanonicalJsonObject = services
.rooms
.state_accessor
.room_state_get_content(&body.room_id, &StateEventType::RoomCreate, "")
.await
.map_err(|_| err!(Database("Found room without m.room.create event.")))?;
// 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(),
));
// Send a m.room.create event containing a predecessor field and the applicable
// room_version
{
use RoomVersionId::*;
match body.new_version {
V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
create_event_content.insert(
"creator".into(),
json!(&sender_user).try_into().map_err(|e| {
info!("Error forming creation event: {e}");
Error::BadRequest(ErrorKind::BadJson, "Error forming creation event")
})?,
);
},
_ => {
// "creator" key no longer exists in V11+ rooms
create_event_content.remove("creator");
},
}
}
create_event_content.insert(
"room_version".into(),
json!(&body.new_version)
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
);
create_event_content.insert(
"predecessor".into(),
json!(predecessor)
.try_into()
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"))?,
);
// Validate creation event content
if serde_json::from_str::<CanonicalJsonObject>(
to_raw_value(&create_event_content)
.expect("Error forming creation event")
.get(),
)
.is_err()
{
return Err(Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"));
}
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_event_content).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(String::new()),
redacts: None,
timestamp: None,
},
sender_user,
&replacement_room,
&state_lock,
)
.await?;
// Join the new room
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
is_direct: None,
third_party_invite: None,
blurhash: services.users.blurhash(sender_user).await.ok(),
reason: None,
join_authorized_via_users_server: None,
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.to_string()),
redacts: None,
timestamp: None,
},
sender_user,
&replacement_room,
&state_lock,
)
.await?;
// Replicate transferable state events to the new room
for event_type in TRANSFERABLE_STATE_EVENTS {
let event_content = match services
.rooms
.state_accessor
.room_state_get(&body.room_id, event_type, "")
.await
{
Ok(v) => v.content.clone(),
Err(_) => continue, // Skipping missing events.
};
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: event_type.to_string().into(),
content: event_content,
state_key: Some(String::new()),
..Default::default()
},
sender_user,
&replacement_room,
&state_lock,
)
.await?;
}
// Moves any local aliases to the new room
let mut local_aliases = services
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.boxed();
while let Some(alias) = local_aliases.next().await {
services
.rooms
.alias
.remove_alias(alias, sender_user)
.await?;
services
.rooms
.alias
.set_alias(alias, &replacement_room, sender_user)?;
}
// Get the old room power levels
let power_levels_event_content: RoomPowerLevelsEventContent = services
.rooms
.state_accessor
.room_state_get_content(&body.room_id, &StateEventType::RoomPowerLevels, "")
.await
.map_err(|_| err!(Database("Found room without m.room.power_levels event.")))?;
// Setting events_default and invite to the greater of 50 and users_default + 1
let new_level = max(
int!(50),
power_levels_event_content
.users_default
.checked_add(int!(1))
.ok_or_else(|| err!(Request(BadJson("users_default power levels event content is not valid"))))?,
);
// Modify the power levels in the old room to prevent sending of events and
// inviting new users
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(
String::new(),
&RoomPowerLevelsEventContent {
events_default: new_level,
invite: new_level,
..power_levels_event_content
},
),
sender_user,
&body.room_id,
&state_lock,
)
.await?;
drop(state_lock);
// Return the replacement room id
Ok(upgrade_room::v3::Response {
replacement_room,
})
}
+176 -151
View File
@@ -1,21 +1,33 @@
use std::collections::BTreeMap;
use axum::extract::State;
use conduit::{
at, is_true,
result::FlatOk,
utils::{stream::ReadyExt, IterStream},
Err, PduEvent, Result,
};
use futures::{future::OptionFuture, FutureExt, StreamExt, TryFutureExt};
use ruma::{
api::client::{
error::ErrorKind,
search::search_events::{
self,
v3::{EventContextResult, ResultCategories, ResultRoomEvents, SearchResult},
},
api::client::search::search_events::{
self,
v3::{Criteria, EventContextResult, ResultCategories, ResultRoomEvents, SearchResult},
},
events::AnyStateEvent,
serde::Raw,
uint, OwnedRoomId,
OwnedRoomId, RoomId, UInt, UserId,
};
use tracing::debug;
use search_events::v3::{Request, Response};
use service::{rooms::search::RoomQuery, Services};
use crate::{Error, Result, Ruma};
use crate::Ruma;
type RoomStates = BTreeMap<OwnedRoomId, RoomState>;
type RoomState = Vec<Raw<AnyStateEvent>>;
const LIMIT_DEFAULT: usize = 10;
const LIMIT_MAX: usize = 100;
const BATCH_MAX: usize = 20;
/// # `POST /_matrix/client/r0/search`
///
@@ -23,160 +35,173 @@ use crate::{Error, Result, Ruma};
///
/// - Only works if the user is currently joined to the room (TODO: Respect
/// history visibility)
pub(crate) async fn search_events_route(
State(services): State<crate::State>, body: Ruma<search_events::v3::Request>,
) -> Result<search_events::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
pub(crate) async fn search_events_route(State(services): State<crate::State>, body: Ruma<Request>) -> Result<Response> {
let sender_user = body.sender_user();
let next_batch = body.next_batch.as_deref();
let room_events_result: OptionFuture<_> = body
.search_categories
.room_events
.as_ref()
.map(|criteria| category_room_events(&services, sender_user, next_batch, criteria))
.into();
let search_criteria = body.search_categories.room_events.as_ref().unwrap();
let filter = &search_criteria.filter;
let include_state = &search_criteria.include_state;
Ok(Response {
search_categories: ResultCategories {
room_events: room_events_result
.await
.unwrap_or_else(|| Ok(ResultRoomEvents::default()))?,
},
})
}
let room_ids = filter.rooms.clone().unwrap_or_else(|| {
services
.rooms
.state_cache
.rooms_joined(sender_user)
.filter_map(Result::ok)
.collect()
});
#[allow(clippy::map_unwrap_or)]
async fn category_room_events(
services: &Services, sender_user: &UserId, next_batch: Option<&str>, criteria: &Criteria,
) -> Result<ResultRoomEvents> {
let filter = &criteria.filter;
// Use limit or else 10, with maximum 100
let limit: usize = filter
.limit
.unwrap_or_else(|| uint!(10))
.try_into()
.unwrap_or(10)
.min(100);
.map(TryInto::try_into)
.flat_ok()
.unwrap_or(LIMIT_DEFAULT)
.min(LIMIT_MAX);
let mut room_states: BTreeMap<OwnedRoomId, Vec<Raw<AnyStateEvent>>> = BTreeMap::new();
let next_batch: usize = next_batch
.map(str::parse)
.transpose()?
.unwrap_or(0)
.min(limit.saturating_mul(BATCH_MAX));
if include_state.is_some_and(|include_state| include_state) {
for room_id in &room_ids {
if !services.rooms.state_cache.is_joined(sender_user, room_id)? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view this room.",
));
}
// check if sender_user can see state events
if services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, room_id)?
{
let room_state = services
.rooms
.state_accessor
.room_state_full(room_id)
.await?
.values()
.map(|pdu| pdu.to_state_event())
.collect::<Vec<_>>();
debug!("Room state: {:?}", room_state);
room_states.insert(room_id.clone(), room_state);
} else {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view this room.",
));
}
}
}
let mut searches = Vec::new();
for room_id in &room_ids {
if !services.rooms.state_cache.is_joined(sender_user, room_id)? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view this room.",
));
}
if let Some(search) = services
.rooms
.search
.search_pdus(room_id, &search_criteria.search_term)?
{
searches.push(search.0.peekable());
}
}
let skip: usize = match body.next_batch.as_ref().map(|s| s.parse()) {
Some(Ok(s)) => s,
Some(Err(_)) => return Err(Error::BadRequest(ErrorKind::InvalidParam, "Invalid next_batch token.")),
None => 0, // Default to the start
};
let mut results = Vec::new();
let next_batch = skip.saturating_add(limit);
for _ in 0..next_batch {
if let Some(s) = searches
.iter_mut()
.map(|s| (s.peek().cloned(), s))
.max_by_key(|(peek, _)| peek.clone())
.and_then(|(_, i)| i.next())
{
results.push(s);
}
}
let results: Vec<_> = results
.iter()
.skip(skip)
.filter_map(|result| {
let rooms = filter
.rooms
.clone()
.map(IntoIterator::into_iter)
.map(IterStream::stream)
.map(StreamExt::boxed)
.unwrap_or_else(|| {
services
.rooms
.timeline
.get_pdu_from_id(result)
.ok()?
.filter(|pdu| {
!pdu.is_redacted()
&& services
.rooms
.state_accessor
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id)
.unwrap_or(false)
})
.map(|pdu| pdu.to_room_event())
.state_cache
.rooms_joined(sender_user)
.map(ToOwned::to_owned)
.boxed()
});
let results: Vec<_> = rooms
.filter_map(|room_id| async move {
check_room_visible(services, sender_user, &room_id, criteria)
.await
.is_ok()
.then_some(room_id)
})
.map(|result| {
Ok::<_, Error>(SearchResult {
context: EventContextResult {
end: None,
events_after: Vec::new(),
events_before: Vec::new(),
profile_info: BTreeMap::new(),
start: None,
},
rank: None,
result: Some(result),
})
.filter_map(|room_id| async move {
let query = RoomQuery {
room_id: &room_id,
user_id: Some(sender_user),
criteria,
skip: next_batch,
limit,
};
let (count, results) = services.rooms.search.search_pdus(&query).await.ok()?;
results
.collect::<Vec<_>>()
.map(|results| (room_id.clone(), count, results))
.map(Some)
.await
})
.filter_map(Result::ok)
.take(limit)
.collect()
.await;
let total: UInt = results
.iter()
.fold(0, |a: usize, (_, count, _)| a.saturating_add(*count))
.try_into()?;
let state: RoomStates = results
.iter()
.stream()
.ready_filter(|_| criteria.include_state.is_some_and(is_true!()))
.filter_map(|(room_id, ..)| async move {
procure_room_state(services, room_id)
.map_ok(|state| (room_id.clone(), state))
.await
.ok()
})
.collect()
.await;
let results: Vec<SearchResult> = results
.into_iter()
.map(at!(2))
.flatten()
.stream()
.map(|pdu| pdu.to_room_event())
.map(|result| SearchResult {
rank: None,
result: Some(result),
context: EventContextResult {
profile_info: BTreeMap::new(), //TODO
events_after: Vec::new(), //TODO
events_before: Vec::new(), //TODO
start: None, //TODO
end: None, //TODO
},
})
.collect()
.await;
let highlights = criteria
.search_term
.split_terminator(|c: char| !c.is_alphanumeric())
.map(str::to_lowercase)
.collect();
let more_unloaded_results = searches.iter_mut().any(|s| s.peek().is_some());
let next_batch = more_unloaded_results.then(|| next_batch.to_string());
let next_batch = (results.len() >= limit)
.then_some(next_batch.saturating_add(results.len()))
.as_ref()
.map(ToString::to_string);
Ok(search_events::v3::Response::new(ResultCategories {
room_events: ResultRoomEvents {
count: Some(results.len().try_into().unwrap_or_else(|_| uint!(0))),
groups: BTreeMap::new(), // TODO
next_batch,
results,
state: room_states,
highlights: search_criteria
.search_term
.split_terminator(|c: char| !c.is_alphanumeric())
.map(str::to_lowercase)
.collect(),
},
}))
Ok(ResultRoomEvents {
count: Some(total),
next_batch,
results,
state,
highlights,
groups: BTreeMap::new(), // TODO
})
}
async fn procure_room_state(services: &Services, room_id: &RoomId) -> Result<RoomState> {
let state_map = services
.rooms
.state_accessor
.room_state_full(room_id)
.await?;
let state_events = state_map.values().map(PduEvent::to_state_event).collect();
Ok(state_events)
}
async fn check_room_visible(services: &Services, user_id: &UserId, room_id: &RoomId, search: &Criteria) -> Result {
let check_visible = search.filter.rooms.is_some();
let check_state = check_visible && search.include_state.is_some_and(is_true!());
let is_joined = !check_visible || services.rooms.state_cache.is_joined(user_id, room_id).await;
let state_visible = !check_state
|| services
.rooms
.state_accessor
.user_can_see_state_events(user_id, room_id)
.await;
if !is_joined || !state_visible {
return Err!(Request(Forbidden("You don't have permission to view {room_id:?}")));
}
Ok(())
}
+92
View File
@@ -0,0 +1,92 @@
use std::collections::BTreeMap;
use axum::extract::State;
use conduit::{err, Err};
use ruma::{api::client::message::send_message_event, events::MessageLikeEventType};
use serde_json::from_str;
use crate::{service::pdu::PduBuilder, utils, Result, Ruma};
/// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`
///
/// Send a message event into the room.
///
/// - Is a NOOP if the txn id was already used before and returns the same event
/// id again
/// - The only requirement for the content is that it has to be valid json
/// - Tries to send the event into the room, auth rules will determine if it is
/// allowed
pub(crate) async fn send_message_event_route(
State(services): State<crate::State>, body: Ruma<send_message_event::v3::Request>,
) -> Result<send_message_event::v3::Response> {
let sender_user = body.sender_user();
let sender_device = body.sender_device.as_deref();
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() {
return Err!(Request(Forbidden("Encryption has been disabled")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
if body.event_type == MessageLikeEventType::CallInvite
&& services.rooms.directory.is_public_room(&body.room_id).await
{
return Err!(Request(Forbidden("Room call invites are not allowed in public rooms")));
}
// Check if this is a new transaction id
if let Ok(response) = services
.transaction_ids
.existing_txnid(sender_user, sender_device, &body.txn_id)
.await
{
// The client might have sent a txnid of the /sendToDevice endpoint
// This txnid has no response associated with it
if response.is_empty() {
return Err!(Request(InvalidParam(
"Tried to use txn id already used for an incompatible endpoint."
)));
}
return Ok(send_message_event::v3::Response {
event_id: utils::string_from_bytes(&response)
.map(TryInto::try_into)
.map_err(|e| err!(Database("Invalid event_id in txnid data: {e:?}")))??,
});
}
let mut unsigned = BTreeMap::new();
unsigned.insert("transaction_id".to_owned(), body.txn_id.to_string().into());
let content =
from_str(body.body.body.json().get()).map_err(|e| err!(Request(BadJson("Invalid JSON body: {e}"))))?;
let event_id = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: body.event_type.clone().into(),
content,
unsigned: Some(unsigned),
timestamp: appservice_info.and(body.timestamp),
..Default::default()
},
sender_user,
&body.room_id,
&state_lock,
)
.await?;
services
.transaction_ids
.add_txnid(sender_user, sender_device, &body.txn_id, event_id.as_bytes());
drop(state_lock);
Ok(send_message_event::v3::Response {
event_id: event_id.into(),
})
}
+48 -33
View File
@@ -1,5 +1,7 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduit::{debug, err, info, utils::ReadyExt, warn, Err};
use futures::StreamExt;
use ruma::{
api::client::{
error::ErrorKind,
@@ -19,7 +21,6 @@ use ruma::{
UserId,
};
use serde::Deserialize;
use tracing::{debug, info, warn};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::{utils, utils::hash, Error, Result, Ruma};
@@ -79,21 +80,22 @@ pub(crate) async fn login_route(
UserId::parse(user)
} else {
warn!("Bad login type: {:?}", &body.login_info);
return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
return Err!(Request(Forbidden("Bad login type.")));
}
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
let hash = services
.users
.password_hash(&user_id)?
.ok_or(Error::BadRequest(ErrorKind::forbidden(), "Wrong username or password."))?;
.password_hash(&user_id)
.await
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
if hash.is_empty() {
return Err(Error::BadRequest(ErrorKind::UserDeactivated, "The user has been deactivated"));
return Err!(Request(UserDeactivated("The user has been deactivated")));
}
if hash::verify_password(password, &hash).is_err() {
return Err(Error::BadRequest(ErrorKind::forbidden(), "Wrong username or password."));
return Err!(Request(Forbidden("Wrong username or password.")));
}
user_id
@@ -112,15 +114,12 @@ pub(crate) async fn login_route(
let username = token.claims.sub.to_lowercase();
UserId::parse_with_server_name(username, services.globals.server_name()).map_err(|e| {
warn!("Failed to parse username from user logging in: {e}");
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
})?
UserId::parse_with_server_name(username, services.globals.server_name())
.map_err(|e| err!(Request(InvalidUsername(debug_error!(?e, "Failed to parse login username")))))?
} else {
return Err(Error::BadRequest(
ErrorKind::Unknown,
"Token login is not supported (server has no jwt decoding key).",
));
return Err!(Request(Unknown(
"Token login is not supported (server has no jwt decoding key)."
)));
}
},
#[allow(deprecated)]
@@ -169,29 +168,40 @@ pub(crate) async fn login_route(
let token = utils::random_string(TOKEN_LENGTH);
// Determine if device_id was provided and exists in the db for this user
let device_exists = body.device_id.as_ref().map_or(false, |device_id| {
let device_exists = if body.device_id.is_some() {
services
.users
.all_device_ids(&user_id)
.any(|x| x.as_ref().map_or(false, |v| v == device_id))
});
.ready_any(|v| v == device_id)
.await
} else {
false
};
if device_exists {
services.users.set_token(&user_id, &device_id, &token)?;
services
.users
.set_token(&user_id, &device_id, &token)
.await?;
} else {
services.users.create_device(
&user_id,
&device_id,
&token,
body.initial_device_display_name.clone(),
Some(client.to_string()),
)?;
services
.users
.create_device(
&user_id,
&device_id,
&token,
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
.await?;
}
// send client well-known if specified so the client knows to reconfigure itself
let client_discovery_info: Option<DiscoveryInfo> = services
.globals
.well_known_client()
.server
.config
.well_known
.client
.as_ref()
.map(|server| DiscoveryInfo::new(HomeserverInfo::new(server.to_string())));
@@ -228,10 +238,13 @@ pub(crate) async fn logout_route(
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
services.users.remove_device(sender_user, sender_device)?;
services
.users
.remove_device(sender_user, sender_device)
.await;
// send device list update for user after logout
services.users.mark_device_key_update(sender_user)?;
services.users.mark_device_key_update(sender_user).await;
Ok(logout::v3::Response::new())
}
@@ -256,12 +269,14 @@ pub(crate) async fn logout_all_route(
) -> Result<logout_all::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
for device_id in services.users.all_device_ids(sender_user).flatten() {
services.users.remove_device(sender_user, &device_id)?;
}
services
.users
.all_device_ids(sender_user)
.for_each(|device_id| services.users.remove_device(sender_user, device_id))
.await;
// send device list update for user after logout
services.users.mark_device_key_update(sender_user)?;
services.users.mark_device_key_update(sender_user).await;
Ok(logout_all::v3::Response::new())
}
+44 -52
View File
@@ -1,7 +1,7 @@
use std::sync::Arc;
use axum::extract::State;
use conduit::{debug_info, error, pdu::PduBuilder, Error, Result};
use conduit::{err, pdu::PduBuilder, utils::BoolExt, Err, Error, PduEvent, Result};
use ruma::{
api::client::{
error::ErrorKind,
@@ -84,12 +84,10 @@ pub(crate) async fn get_state_events_route(
if !services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, &body.room_id)?
.user_can_see_state_events(sender_user, &body.room_id)
.await
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view the room state.",
));
return Err!(Request(Forbidden("You don't have permission to view the room state.")));
}
Ok(get_state_events::v3::Response {
@@ -99,7 +97,7 @@ pub(crate) async fn get_state_events_route(
.room_state_full(&body.room_id)
.await?
.values()
.map(|pdu| pdu.to_state_event())
.map(PduEvent::to_state_event)
.collect(),
})
}
@@ -120,43 +118,34 @@ pub(crate) async fn get_state_events_for_key_route(
if !services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, &body.room_id)?
.user_can_see_state_events(sender_user, &body.room_id)
.await
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You don't have permission to view the room state.",
));
return Err!(Request(Forbidden("You don't have permission to view the room state.")));
}
let event = services
.rooms
.state_accessor
.room_state_get(&body.room_id, &body.event_type, &body.state_key)?
.ok_or_else(|| {
debug_info!("State event {:?} not found in room {:?}", &body.event_type, &body.room_id);
Error::BadRequest(ErrorKind::NotFound, "State event not found.")
.room_state_get(&body.room_id, &body.event_type, &body.state_key)
.await
.map_err(|_| {
err!(Request(NotFound(debug_warn!(
room_id = ?body.room_id,
event_type = ?body.event_type,
"State event not found in room.",
))))
})?;
if body
let event_format = body
.format
.as_ref()
.is_some_and(|f| f.to_lowercase().eq("event"))
{
Ok(get_state_events_for_key::v3::Response {
content: None,
event: serde_json::from_str(event.to_state_event().json().get()).map_err(|e| {
error!("Invalid room state event in database: {}", e);
Error::bad_database("Invalid room state event in database")
})?,
})
} else {
Ok(get_state_events_for_key::v3::Response {
content: Some(serde_json::from_str(event.content.get()).map_err(|e| {
error!("Invalid room state event content in database: {}", e);
Error::bad_database("Invalid room state event content in database")
})?),
event: None,
})
}
.is_some_and(|f| f.to_lowercase().eq("event"));
Ok(get_state_events_for_key::v3::Response {
content: event_format.or(|| event.get_content_as_value()),
event: event_format.then(|| event.to_state_event_value()),
})
}
/// # `GET /_matrix/client/v3/rooms/{roomid}/state/{eventType}`
@@ -187,11 +176,10 @@ async fn send_state_event_for_key_helper(
.build_and_append_pdu(
PduBuilder {
event_type: event_type.to_string().into(),
content: serde_json::from_str(json.json().get()).expect("content is valid json"),
unsigned: None,
content: serde_json::from_str(json.json().get())?,
state_key: Some(state_key),
redacts: None,
timestamp,
..Default::default()
},
sender,
room_id,
@@ -204,7 +192,7 @@ async fn send_state_event_for_key_helper(
async fn allowed_to_send_state_event(
services: &Services, room_id: &RoomId, event_type: &StateEventType, json: &Raw<AnyStateEventContent>,
) -> Result<()> {
) -> Result {
match event_type {
// Forbid m.room.encryption if encryption is disabled
StateEventType::RoomEncryption => {
@@ -214,7 +202,7 @@ async fn allowed_to_send_state_event(
},
// admin room is a sensitive room, it should not ever be made public
StateEventType::RoomJoinRules => {
if let Some(admin_room_id) = services.admin.get_admin_room()? {
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
if admin_room_id == room_id {
if let Ok(join_rule) = serde_json::from_str::<RoomJoinRulesEventContent>(json.json().get()) {
if join_rule.join_rule == JoinRule::Public {
@@ -229,7 +217,7 @@ async fn allowed_to_send_state_event(
},
// admin room is a sensitive room, it should not ever be made world readable
StateEventType::RoomHistoryVisibility => {
if let Some(admin_room_id) = services.admin.get_admin_room()? {
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
if admin_room_id == room_id {
if let Ok(visibility_content) =
serde_json::from_str::<RoomHistoryVisibilityEventContent>(json.json().get())
@@ -254,23 +242,27 @@ async fn allowed_to_send_state_event(
}
for alias in aliases {
if !services.globals.server_is_ours(alias.server_name())
|| services
.rooms
.alias
.resolve_local_alias(&alias)?
.filter(|room| room == room_id) // Make sure it's the right room
.is_none()
if !services.globals.server_is_ours(alias.server_name()) {
return Err!(Request(Forbidden("canonical_alias must be for this server")));
}
if !services
.rooms
.alias
.resolve_local_alias(&alias)
.await
.is_ok_and(|room| room == room_id)
// Make sure it's the right room
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"You are only allowed to send canonical_alias events when its aliases already exist",
));
return Err!(Request(Forbidden(
"You are only allowed to send canonical_alias events when its aliases already exist"
)));
}
}
}
},
_ => (),
}
Ok(())
}
File diff suppressed because it is too large Load Diff
+68
View File
@@ -0,0 +1,68 @@
mod v3;
mod v4;
use conduit::{
utils::stream::{BroadbandExt, ReadyExt},
PduCount,
};
use futures::StreamExt;
use ruma::{RoomId, UserId};
pub(crate) use self::{v3::sync_events_route, v4::sync_events_v4_route};
use crate::{service::Services, Error, PduEvent, Result};
async fn load_timeline(
services: &Services, sender_user: &UserId, room_id: &RoomId, roomsincecount: PduCount,
next_batch: Option<PduCount>, limit: usize,
) -> Result<(Vec<(PduCount, PduEvent)>, bool), Error> {
let last_timeline_count = services
.rooms
.timeline
.last_timeline_count(Some(sender_user), room_id)
.await?;
if last_timeline_count <= roomsincecount {
return Ok((Vec::new(), false));
}
let mut non_timeline_pdus = services
.rooms
.timeline
.pdus_rev(Some(sender_user), room_id, None)
.await?
.ready_skip_while(|&(pducount, _)| pducount > next_batch.unwrap_or_else(PduCount::max))
.ready_take_while(|&(pducount, _)| pducount > roomsincecount);
// Take the last events for the timeline
let timeline_pdus: Vec<_> = non_timeline_pdus
.by_ref()
.take(limit)
.collect::<Vec<_>>()
.await
.into_iter()
.rev()
.collect();
// They /sync response doesn't always return all messages, so we say the output
// is limited unless there are events in non_timeline_pdus
let limited = non_timeline_pdus.next().await.is_some();
Ok((timeline_pdus, limited))
}
async fn share_encrypted_room(
services: &Services, sender_user: &UserId, user_id: &UserId, ignore_room: Option<&RoomId>,
) -> bool {
services
.rooms
.state_cache
.get_shared_rooms(sender_user, user_id)
.ready_filter(|&room_id| Some(room_id) != ignore_room)
.broad_any(|other_room_id| {
services
.rooms
.state_accessor
.is_encrypted_room(other_room_id)
})
.await
}
File diff suppressed because it is too large Load Diff
+771
View File
@@ -0,0 +1,771 @@
use std::{
cmp::{self, Ordering},
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
time::Duration,
};
use axum::extract::State;
use conduit::{
debug, error, extract_variant,
utils::{
math::{ruma_from_usize, usize_from_ruma, usize_from_u64_truncated},
BoolExt, IterStream, ReadyExt, TryFutureExtExt,
},
warn, Error, PduCount, Result,
};
use futures::{FutureExt, StreamExt, TryFutureExt};
use ruma::{
api::client::{
error::ErrorKind,
sync::sync_events::{
self,
v4::{SlidingOp, SlidingSyncRoomHero},
DeviceLists, UnreadNotificationsCount,
},
},
directory::RoomTypeFilter,
events::{
room::member::{MembershipState, RoomMemberEventContent},
AnyRawAccountDataEvent, StateEventType,
TimelineEventType::{self, *},
},
state_res::Event,
uint, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, UInt, UserId,
};
use service::{rooms::read_receipt::pack_receipts, Services};
use super::{load_timeline, share_encrypted_room};
use crate::{client::ignored_filter, Ruma};
const SINGLE_CONNECTION_SYNC: &str = "single_connection_sync";
const DEFAULT_BUMP_TYPES: &[TimelineEventType; 6] =
&[RoomMessage, RoomEncrypted, Sticker, CallInvite, PollStart, Beacon];
/// POST `/_matrix/client/unstable/org.matrix.msc3575/sync`
///
/// Sliding Sync endpoint (future endpoint: `/_matrix/client/v4/sync`)
pub(crate) async fn sync_events_v4_route(
State(services): State<crate::State>, body: Ruma<sync_events::v4::Request>,
) -> Result<sync_events::v4::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.expect("user is authenticated");
let mut body = body.body;
// Setup watchers, so if there's no response, we can wait for them
let watcher = services.sync.watch(sender_user, &sender_device);
let next_batch = services.globals.next_count()?;
let conn_id = body
.conn_id
.clone()
.unwrap_or_else(|| SINGLE_CONNECTION_SYNC.to_owned());
let globalsince = body
.pos
.as_ref()
.and_then(|string| string.parse().ok())
.unwrap_or(0);
if globalsince != 0
&& !services
.sync
.remembered(sender_user.clone(), sender_device.clone(), conn_id.clone())
{
debug!("Restarting sync stream because it was gone from the database");
return Err(Error::Request(
ErrorKind::UnknownPos,
"Connection data lost since last time".into(),
http::StatusCode::BAD_REQUEST,
));
}
if globalsince == 0 {
services
.sync
.forget_sync_request_connection(sender_user.clone(), sender_device.clone(), conn_id.clone());
}
// Get sticky parameters from cache
let known_rooms =
services
.sync
.update_sync_request_with_cache(sender_user.clone(), sender_device.clone(), &mut body);
let all_joined_rooms: Vec<_> = services
.rooms
.state_cache
.rooms_joined(sender_user)
.map(ToOwned::to_owned)
.collect()
.await;
let all_invited_rooms: Vec<_> = services
.rooms
.state_cache
.rooms_invited(sender_user)
.map(|r| r.0)
.collect()
.await;
let all_rooms = all_joined_rooms
.iter()
.chain(all_invited_rooms.iter())
.map(Clone::clone)
.collect();
if body.extensions.to_device.enabled.unwrap_or(false) {
services
.users
.remove_to_device_events(sender_user, &sender_device, globalsince)
.await;
}
let mut left_encrypted_users = HashSet::new(); // Users that have left any encrypted rooms the sender was in
let mut device_list_changes = HashSet::new();
let mut device_list_left = HashSet::new();
let mut receipts = sync_events::v4::Receipts {
rooms: BTreeMap::new(),
};
let mut account_data = sync_events::v4::AccountData {
global: Vec::new(),
rooms: BTreeMap::new(),
};
if body.extensions.account_data.enabled.unwrap_or(false) {
account_data.global = services
.account_data
.changes_since(None, sender_user, globalsince)
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Global))
.collect()
.await;
if let Some(rooms) = body.extensions.account_data.rooms {
for room in rooms {
account_data.rooms.insert(
room.clone(),
services
.account_data
.changes_since(Some(&room), sender_user, globalsince)
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
.collect()
.await,
);
}
}
}
if body.extensions.e2ee.enabled.unwrap_or(false) {
// Look for device list updates of this account
device_list_changes.extend(
services
.users
.keys_changed(sender_user, globalsince, None)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
for room_id in &all_joined_rooms {
let Ok(current_shortstatehash) = services.rooms.state.get_room_shortstatehash(room_id).await else {
error!("Room {room_id} has no state");
continue;
};
let since_shortstatehash = services
.rooms
.user
.get_token_shortstatehash(room_id, globalsince)
.await
.ok();
let encrypted_room = services
.rooms
.state_accessor
.state_get(current_shortstatehash, &StateEventType::RoomEncryption, "")
.await
.is_ok();
if let Some(since_shortstatehash) = since_shortstatehash {
// Skip if there are only timeline changes
if since_shortstatehash == current_shortstatehash {
continue;
}
let since_encryption = services
.rooms
.state_accessor
.state_get(since_shortstatehash, &StateEventType::RoomEncryption, "")
.await;
let since_sender_member: Option<RoomMemberEventContent> = services
.rooms
.state_accessor
.state_get_content(since_shortstatehash, &StateEventType::RoomMember, sender_user.as_str())
.ok()
.await;
let joined_since_last_sync =
since_sender_member.map_or(true, |member| member.membership != MembershipState::Join);
let new_encrypted_room = encrypted_room && since_encryption.is_err();
if encrypted_room {
let current_state_ids: HashMap<_, OwnedEventId> = services
.rooms
.state_accessor
.state_full_ids(current_shortstatehash)
.await?;
let since_state_ids = services
.rooms
.state_accessor
.state_full_ids(since_shortstatehash)
.await?;
for (key, id) in current_state_ids {
if since_state_ids.get(&key) != Some(&id) {
let Ok(pdu) = services.rooms.timeline.get_pdu(&id).await else {
error!("Pdu in state not found: {id}");
continue;
};
if pdu.kind == RoomMember {
if let Some(state_key) = &pdu.state_key {
let user_id = UserId::parse(state_key.clone())
.map_err(|_| Error::bad_database("Invalid UserId in member PDU."))?;
if user_id == *sender_user {
continue;
}
let content: RoomMemberEventContent = pdu.get_content()?;
match content.membership {
MembershipState::Join => {
// A new user joined an encrypted room
if !share_encrypted_room(&services, sender_user, &user_id, Some(room_id))
.await
{
device_list_changes.insert(user_id);
}
},
MembershipState::Leave => {
// Write down users that have left encrypted rooms we are in
left_encrypted_users.insert(user_id);
},
_ => {},
}
}
}
}
}
if joined_since_last_sync || new_encrypted_room {
// If the user is in a new encrypted room, give them all joined users
device_list_changes.extend(
services
.rooms
.state_cache
.room_members(room_id)
// Don't send key updates from the sender to the sender
.ready_filter(|user_id| sender_user != user_id)
// Only send keys if the sender doesn't share an encrypted room with the target
// already
.filter_map(|user_id| {
share_encrypted_room(&services, sender_user, user_id, Some(room_id))
.map(|res| res.or_some(user_id.to_owned()))
})
.collect::<Vec<_>>()
.await,
);
}
}
}
// Look for device list updates in this room
device_list_changes.extend(
services
.users
.room_keys_changed(room_id, globalsince, None)
.map(|(user_id, _)| user_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
}
for user_id in left_encrypted_users {
let dont_share_encrypted_room = !share_encrypted_room(&services, sender_user, &user_id, None).await;
// If the user doesn't share an encrypted room with the target anymore, we need
// to tell them
if dont_share_encrypted_room {
device_list_left.insert(user_id);
}
}
}
let mut lists = BTreeMap::new();
let mut todo_rooms = BTreeMap::new(); // and required state
for (list_id, list) in &body.lists {
let active_rooms = match list.filters.clone().and_then(|f| f.is_invite) {
Some(true) => &all_invited_rooms,
Some(false) => &all_joined_rooms,
None => &all_rooms,
};
let active_rooms = match list.filters.clone().map(|f| f.not_room_types) {
Some(filter) if filter.is_empty() => active_rooms.clone(),
Some(value) => filter_rooms(&services, active_rooms, &value, true).await,
None => active_rooms.clone(),
};
let active_rooms = match list.filters.clone().map(|f| f.room_types) {
Some(filter) if filter.is_empty() => active_rooms.clone(),
Some(value) => filter_rooms(&services, &active_rooms, &value, false).await,
None => active_rooms,
};
let mut new_known_rooms = BTreeSet::new();
let ranges = list.ranges.clone();
lists.insert(
list_id.clone(),
sync_events::v4::SyncList {
ops: ranges
.into_iter()
.map(|mut r| {
r.0 = r.0.clamp(
uint!(0),
UInt::try_from(active_rooms.len().saturating_sub(1)).unwrap_or(UInt::MAX),
);
r.1 =
r.1.clamp(r.0, UInt::try_from(active_rooms.len().saturating_sub(1)).unwrap_or(UInt::MAX));
let room_ids = if !active_rooms.is_empty() {
active_rooms[usize_from_ruma(r.0)..=usize_from_ruma(r.1)].to_vec()
} else {
Vec::new()
};
new_known_rooms.extend(room_ids.iter().cloned());
for room_id in &room_ids {
let todo_room =
todo_rooms
.entry(room_id.clone())
.or_insert((BTreeSet::new(), 0_usize, u64::MAX));
let limit: usize = list
.room_details
.timeline_limit
.map(u64::from)
.map_or(10, usize_from_u64_truncated)
.min(100);
todo_room
.0
.extend(list.room_details.required_state.iter().cloned());
todo_room.1 = todo_room.1.max(limit);
// 0 means unknown because it got out of date
todo_room.2 = todo_room.2.min(
known_rooms
.get(list_id.as_str())
.and_then(|k| k.get(room_id))
.copied()
.unwrap_or(0),
);
}
sync_events::v4::SyncOp {
op: SlidingOp::Sync,
range: Some(r),
index: None,
room_ids,
room_id: None,
}
})
.collect(),
count: ruma_from_usize(active_rooms.len()),
},
);
if let Some(conn_id) = &body.conn_id {
services.sync.update_sync_known_rooms(
sender_user.clone(),
sender_device.clone(),
conn_id.clone(),
list_id.clone(),
new_known_rooms,
globalsince,
);
}
}
let mut known_subscription_rooms = BTreeSet::new();
for (room_id, room) in &body.room_subscriptions {
if !services.rooms.metadata.exists(room_id).await {
continue;
}
let todo_room = todo_rooms
.entry(room_id.clone())
.or_insert((BTreeSet::new(), 0_usize, u64::MAX));
let limit: usize = room
.timeline_limit
.map(u64::from)
.map_or(10, usize_from_u64_truncated)
.min(100);
todo_room.0.extend(room.required_state.iter().cloned());
todo_room.1 = todo_room.1.max(limit);
// 0 means unknown because it got out of date
todo_room.2 = todo_room.2.min(
known_rooms
.get("subscriptions")
.and_then(|k| k.get(room_id))
.copied()
.unwrap_or(0),
);
known_subscription_rooms.insert(room_id.clone());
}
for r in body.unsubscribe_rooms {
known_subscription_rooms.remove(&r);
body.room_subscriptions.remove(&r);
}
if let Some(conn_id) = &body.conn_id {
services.sync.update_sync_known_rooms(
sender_user.clone(),
sender_device.clone(),
conn_id.clone(),
"subscriptions".to_owned(),
known_subscription_rooms,
globalsince,
);
}
if let Some(conn_id) = &body.conn_id {
services.sync.update_sync_subscriptions(
sender_user.clone(),
sender_device.clone(),
conn_id.clone(),
body.room_subscriptions,
);
}
let mut rooms = BTreeMap::new();
for (room_id, (required_state_request, timeline_limit, roomsince)) in &todo_rooms {
let roomsincecount = PduCount::Normal(*roomsince);
let mut timestamp: Option<_> = None;
let mut invite_state = None;
let (timeline_pdus, limited);
if all_invited_rooms.contains(room_id) {
// TODO: figure out a timestamp we can use for remote invites
invite_state = services
.rooms
.state_cache
.invite_state(sender_user, room_id)
.await
.ok();
(timeline_pdus, limited) = (Vec::new(), true);
} else {
(timeline_pdus, limited) =
match load_timeline(&services, sender_user, room_id, roomsincecount, None, *timeline_limit).await {
Ok(value) => value,
Err(err) => {
warn!("Encountered missing timeline in {}, error {}", room_id, err);
continue;
},
};
}
account_data.rooms.insert(
room_id.clone(),
services
.account_data
.changes_since(Some(room_id), sender_user, *roomsince)
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
.collect()
.await,
);
let vector: Vec<_> = services
.rooms
.read_receipt
.readreceipts_since(room_id, *roomsince)
.filter_map(|(read_user, ts, v)| async move {
services
.users
.user_is_ignored(read_user, sender_user)
.await
.or_some((read_user.to_owned(), ts, v))
})
.collect()
.await;
let receipt_size = vector.len();
receipts
.rooms
.insert(room_id.clone(), pack_receipts(Box::new(vector.into_iter())));
if roomsince != &0
&& timeline_pdus.is_empty()
&& account_data.rooms.get(room_id).is_some_and(Vec::is_empty)
&& receipt_size == 0
{
continue;
}
let prev_batch = timeline_pdus
.first()
.map_or(Ok::<_, Error>(None), |(pdu_count, _)| {
Ok(Some(match pdu_count {
PduCount::Backfilled(_) => {
error!("timeline in backfill state?!");
"0".to_owned()
},
PduCount::Normal(c) => c.to_string(),
}))
})?
.or_else(|| {
if roomsince != &0 {
Some(roomsince.to_string())
} else {
None
}
});
let room_events: Vec<_> = timeline_pdus
.iter()
.stream()
.filter_map(|item| ignored_filter(&services, item.clone(), sender_user))
.map(|(_, pdu)| pdu.to_sync_room_event())
.collect()
.await;
for (_, pdu) in timeline_pdus {
let ts = MilliSecondsSinceUnixEpoch(pdu.origin_server_ts);
if DEFAULT_BUMP_TYPES.contains(pdu.event_type()) && timestamp.is_none_or(|time| time <= ts) {
timestamp = Some(ts);
}
}
let required_state = required_state_request
.iter()
.stream()
.filter_map(|state| async move {
services
.rooms
.state_accessor
.room_state_get(room_id, &state.0, &state.1)
.await
.map(|s| s.to_sync_state_event())
.ok()
})
.collect()
.await;
// Heroes
let heroes: Vec<_> = services
.rooms
.state_cache
.room_members(room_id)
.ready_filter(|member| member != sender_user)
.filter_map(|user_id| {
services
.rooms
.state_accessor
.get_member(room_id, user_id)
.map_ok(|memberevent| SlidingSyncRoomHero {
user_id: user_id.into(),
name: memberevent.displayname,
avatar: memberevent.avatar_url,
})
.ok()
})
.take(5)
.collect()
.await;
let name = match heroes.len().cmp(&(1_usize)) {
Ordering::Greater => {
let firsts = heroes[1..]
.iter()
.map(|h| h.name.clone().unwrap_or_else(|| h.user_id.to_string()))
.collect::<Vec<_>>()
.join(", ");
let last = heroes[0]
.name
.clone()
.unwrap_or_else(|| heroes[0].user_id.to_string());
Some(format!("{firsts} and {last}"))
},
Ordering::Equal => Some(
heroes[0]
.name
.clone()
.unwrap_or_else(|| heroes[0].user_id.to_string()),
),
Ordering::Less => None,
};
let heroes_avatar = if heroes.len() == 1 {
heroes[0].avatar.clone()
} else {
None
};
rooms.insert(
room_id.clone(),
sync_events::v4::SlidingSyncRoom {
name: services
.rooms
.state_accessor
.get_name(room_id)
.await
.ok()
.or(name),
avatar: if let Some(heroes_avatar) = heroes_avatar {
ruma::JsOption::Some(heroes_avatar)
} else {
match services.rooms.state_accessor.get_avatar(room_id).await {
ruma::JsOption::Some(avatar) => ruma::JsOption::from_option(avatar.url),
ruma::JsOption::Null => ruma::JsOption::Null,
ruma::JsOption::Undefined => ruma::JsOption::Undefined,
}
},
initial: Some(roomsince == &0),
is_dm: None,
invite_state,
unread_notifications: UnreadNotificationsCount {
highlight_count: Some(
services
.rooms
.user
.highlight_count(sender_user, room_id)
.await
.try_into()
.expect("notification count can't go that high"),
),
notification_count: Some(
services
.rooms
.user
.notification_count(sender_user, room_id)
.await
.try_into()
.expect("notification count can't go that high"),
),
},
timeline: room_events,
required_state,
prev_batch,
limited,
joined_count: Some(
services
.rooms
.state_cache
.room_joined_count(room_id)
.await
.unwrap_or(0)
.try_into()
.unwrap_or_else(|_| uint!(0)),
),
invited_count: Some(
services
.rooms
.state_cache
.room_invited_count(room_id)
.await
.unwrap_or(0)
.try_into()
.unwrap_or_else(|_| uint!(0)),
),
num_live: None, // Count events in timeline greater than global sync counter
timestamp,
heroes: Some(heroes),
},
);
}
if rooms
.iter()
.all(|(_, r)| r.timeline.is_empty() && r.required_state.is_empty())
{
// Hang a few seconds so requests are not spammed
// Stop hanging if new info arrives
let default = Duration::from_secs(30);
let duration = cmp::min(body.timeout.unwrap_or(default), default);
_ = tokio::time::timeout(duration, watcher).await;
}
Ok(sync_events::v4::Response {
initial: globalsince == 0,
txn_id: body.txn_id.clone(),
pos: next_batch.to_string(),
lists,
rooms,
extensions: sync_events::v4::Extensions {
to_device: if body.extensions.to_device.enabled.unwrap_or(false) {
Some(sync_events::v4::ToDevice {
events: services
.users
.get_to_device_events(sender_user, &sender_device)
.collect()
.await,
next_batch: next_batch.to_string(),
})
} else {
None
},
e2ee: sync_events::v4::E2EE {
device_lists: DeviceLists {
changed: device_list_changes.into_iter().collect(),
left: device_list_left.into_iter().collect(),
},
device_one_time_keys_count: services
.users
.count_one_time_keys(sender_user, &sender_device)
.await,
// Fallback keys are not yet supported
device_unused_fallback_key_types: None,
},
account_data,
receipts,
typing: sync_events::v4::Typing {
rooms: BTreeMap::new(),
},
},
delta_token: None,
})
}
async fn filter_rooms(
services: &Services, rooms: &[OwnedRoomId], filter: &[RoomTypeFilter], negate: bool,
) -> Vec<OwnedRoomId> {
rooms
.iter()
.stream()
.filter_map(|r| async move {
let room_type = services.rooms.state_accessor.get_room_type(r).await;
if room_type.as_ref().is_err_and(|e| !e.is_not_found()) {
return None;
}
let room_type_filter = RoomTypeFilter::from(room_type.ok());
let include = if negate {
!filter.contains(&room_type_filter)
} else {
filter.is_empty() || filter.contains(&room_type_filter)
};
include.then_some(r.to_owned())
})
.collect()
.await
}
+43 -52
View File
@@ -9,7 +9,7 @@ use ruma::{
},
};
use crate::{Error, Result, Ruma};
use crate::{Result, Ruma};
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}`
///
@@ -21,32 +21,30 @@ pub(crate) async fn update_tag_route(
) -> Result<create_tag::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event = services
let mut tags_event = services
.account_data
.get(Some(&body.room_id), sender_user, RoomAccountDataEventType::Tag)?;
let mut tags_event = event.map_or_else(
|| {
Ok(TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
})
},
|e| serde_json::from_str(e.get()).map_err(|_| Error::bad_database("Invalid account data event in db.")),
)?;
.get_room(&body.room_id, sender_user, RoomAccountDataEventType::Tag)
.await
.unwrap_or(TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
});
tags_event
.content
.tags
.insert(body.tag.clone().into(), body.tag_info.clone());
services.account_data.update(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)?;
services
.account_data
.update(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)
.await?;
Ok(create_tag::v3::Response {})
}
@@ -61,29 +59,27 @@ pub(crate) async fn delete_tag_route(
) -> Result<delete_tag::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event = services
let mut tags_event = services
.account_data
.get(Some(&body.room_id), sender_user, RoomAccountDataEventType::Tag)?;
let mut tags_event = event.map_or_else(
|| {
Ok(TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
})
},
|e| serde_json::from_str(e.get()).map_err(|_| Error::bad_database("Invalid account data event in db.")),
)?;
.get_room(&body.room_id, sender_user, RoomAccountDataEventType::Tag)
.await
.unwrap_or(TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
});
tags_event.content.tags.remove(&body.tag.clone().into());
services.account_data.update(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)?;
services
.account_data
.update(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"),
)
.await?;
Ok(delete_tag::v3::Response {})
}
@@ -98,20 +94,15 @@ pub(crate) async fn get_tags_route(
) -> Result<get_tags::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event = services
let tags_event = services
.account_data
.get(Some(&body.room_id), sender_user, RoomAccountDataEventType::Tag)?;
let tags_event = event.map_or_else(
|| {
Ok(TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
})
},
|e| serde_json::from_str(e.get()).map_err(|_| Error::bad_database("Invalid account data event in db.")),
)?;
.get_room(&body.room_id, sender_user, RoomAccountDataEventType::Tag)
.await
.unwrap_or(TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
},
});
Ok(get_tags::v3::Response {
tags: tags_event.content.tags,

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