Compare commits

...

45 commits

Author SHA1 Message Date
Jacob Taylor
676bfffd1d more funny settings (part 3 of 12)
Some checks failed
Checks / Prefligit / prefligit (push) Failing after 2s
Release Docker Image / define-variables (push) Failing after 3s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 2s
Checks / Rust / Clippy (push) Failing after 38s
Checks / Rust / Cargo Test (push) Failing after 40s
2025-07-03 13:14:22 -07:00
Jacob Taylor
2b5bbbb638 sender_workers scaling. this time, with feeling! 2025-07-03 13:14:22 -07:00
Jacob Taylor
2e4d4d09c8 vehicle loan documentation now available at window 7
also print event id
2025-07-03 13:14:22 -07:00
Jacob Taylor
1186eace4d lock the getter instead ??? c/o M 2025-07-03 13:14:22 -07:00
Jacob Taylor
9093ee2485 make fetching key room events less smart 2025-07-03 13:14:22 -07:00
Jacob Taylor
7923e05982 change rocksdb stats level to 3
scale rocksdb background jobs and subcompactions

change rocksdb default error level to info from error

delete unused num_threads function

fix warns from cargo
2025-07-03 13:14:22 -07:00
nexy7574
879a59a1e4 modify more log strings so they're more useful than not 2025-07-03 13:14:22 -07:00
nexy7574
864704a536 When in doubt, log all the things 2025-07-03 13:14:22 -07:00
nexy7574
17fb69e3c2 log which room struggled to get mainline depth 2025-07-03 13:13:32 -07:00
nexy7574
feeb418e81 more logs 2025-07-03 13:13:32 -07:00
nexy7574
6e7390a9b7 Unsafe, untested, and potentially overeager PDU sanity checks 2025-07-03 13:13:32 -07:00
nexy7574
ddc0b9fb1d Fix room ID check 2025-07-03 13:12:43 -07:00
nexy7574
64b89a4365 Kick up a fuss when m.room.create is unfindable 2025-07-03 13:12:43 -07:00
nexy7574
8ed76fa38c Note about ruma#2064 in TODO 2025-07-03 13:12:43 -07:00
nexy7574
93a6442315 fix an auth rule not applying correctly 2025-07-03 13:12:43 -07:00
Jacob Taylor
1383fd198a upgrade some settings to enable 5g in continuwuity
enable converged 6g at the edge in continuwuity

better stateinfo_cache_capacity default

better roomid_spacehierarchy_cache_capacity

make sender workers default better and clamp value to core count

update sender workers documentation

add more parallelism_scaled and make them public

update 1 document
2025-07-03 13:12:43 -07:00
Jacob Taylor
6262c61b0a bump the number of allowed immutable memtables by 1, to allow for greater flood protection
this should probably not be applied if you have rocksdb_atomic_flush = false (the default)
2025-07-03 13:12:43 -07:00
Jacob Taylor
c4a80540c0 probably incorrectly delete support for non-standardized matrix srv record 2025-07-03 13:12:43 -07:00
Jade Ellis
293e7243b3
style: Fix formatting/clippy issues
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 3s
Release Docker Image / define-variables (push) Failing after 12s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 6s
Checks / Rust / Clippy (push) Failing after 20s
Checks / Rust / Cargo Test (push) Failing after 16s
2025-07-02 19:32:50 +01:00
Jason Volk
143cb55ac8
Fix clippy::unnecessary-unwrap.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 19:29:32 +01:00
Jason Volk
3c7c641d2d
Add revoke_admin to service.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 19:29:32 +01:00
Jason Volk
36e81ba185
Split state_cache service.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 19:29:31 +01:00
Jason Volk
56420a67ca
Outdent state_compressor service.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 19:29:31 +01:00
Jason Volk
c5c309ec43
Split timeline service.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 19:29:28 +01:00
Jason Volk
c06aa49a90
Fix regression 75aadd5c6a
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 19:17:09 +01:00
Jason Volk
364293608d
Post-formatting aesthetic and spacing corrections
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 19:17:06 +01:00
Jason Volk
af4f66c768
Cleanup/improve other async queries in some client handlers.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 19:12:51 +01:00
Jason Volk
116f85360f
Toward abstracting Pdu into trait Event.
Co-authored-by: Jade Ellis <jade@ellis.link>
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 19:03:26 +01:00
Jason Volk
3d0360bcd6
Dedup and parallelize current key backup count and etag fetching.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 19:03:26 +01:00
Jason Volk
667afedd24
Macroize various remaining Error constructions.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 19:03:24 +01:00
Jason Volk
21bbee8e3c
Simplify api to send notices to admin room
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 18:58:18 +01:00
Jason Volk
732a77f3a8
Use integrated error instead of panic on some legacy codepaths
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 18:58:18 +01:00
Jason Volk
f3dd90df39
Mitigate large futures
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 18:58:18 +01:00
Jason Volk
2051c22a28
Support optional device_id's in lazy-loading context.
Co-authored-by: Jade Ellis <jade@ellis.link>
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 18:58:17 +01:00
Jason Volk
49f7a2487f
Modernize various sender_user/sender_device lets.
Signed-off-by: Jason Volk <jason@zemos.net>
2025-07-02 18:58:14 +01:00
nexy7574
d6aa03ea73 style: Remove extraneous import
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 5s
Release Docker Image / define-variables (push) Failing after 4s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 4s
Checks / Rust / Clippy (push) Failing after 16s
Checks / Rust / Cargo Test (push) Failing after 17s
2025-07-02 00:53:40 +00:00
nexy7574
8e0852e5b5 docs: Add suggestion about auto join room
Adds suggestion to suspend_on_register doc that admins
should add a room that contains information to their
auto_join_rooms as to not confuse new users who may be
lost at the fact they can't join any rooms or send any
messages.
2025-07-02 00:53:40 +00:00
nexy7574
6e60918584 feat: Suspend new users on registration 2025-07-02 00:53:40 +00:00
nexy7574
68afb07c27
feat: Stabilise room summary API (MSC3266)
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 1s
Release Docker Image / define-variables (push) Failing after 1s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 1s
Checks / Rust / Clippy (push) Failing after 27s
Checks / Rust / Cargo Test (push) Failing after 27s
# Conflicts:
#	Cargo.lock
#	Cargo.toml
2025-07-02 00:48:16 +01:00
nexy7574
b44791799c
fix: Room bans preventing federated leaves
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 2s
Release Docker Image / define-variables (push) Failing after 1s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 1s
Checks / Rust / Clippy (push) Failing after 29s
Checks / Rust / Cargo Test (push) Failing after 28s
Fixes the issue where room bans prevent federating leave
events, resulting in local users being stuck in remote
rooms
2025-07-01 23:14:41 +01:00
nexy7574
4f69da47c6
feat: Advertise support for spec v1.8, 1.12, 1.13, and 1.14
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 4s
Release Docker Image / define-variables (push) Failing after 2s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 2s
Checks / Rust / Clippy (push) Failing after 25s
Checks / Rust / Cargo Test (push) Failing after 23s
2025-07-01 18:38:48 +01:00
nexy7574
24d2a514e2
chore: Resolve linting errors
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 4s
Release Docker Image / define-variables (push) Failing after 2s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 4s
Checks / Rust / Clippy (push) Failing after 23s
Checks / Rust / Cargo Test (push) Failing after 34s
2025-07-01 18:00:28 +01:00
nexy7574
f49c73c031
feat: Forbid suspended users from sending reports
Some checks failed
Checks / Prefligit / prefligit (push) Failing after 4s
Release Docker Image / define-variables (push) Failing after 2s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 4s
Checks / Rust / Clippy (push) Failing after 14s
Checks / Rust / Cargo Test (push) Failing after 16s
2025-07-01 15:44:04 +01:00
nexy7574
59912709aa
feat: Send intentional mentions in report messages 2025-07-01 15:42:38 +01:00
nexy7574
97e5cc4e2d
feat: Implement user reporting 2025-07-01 01:55:13 +01:00
142 changed files with 7470 additions and 3247 deletions

22
Cargo.lock generated
View file

@ -3798,7 +3798,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma" name = "ruma"
version = "0.10.1" version = "0.10.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b948b40417a65ab0282ae47cc50035dd455e02#a4b948b40417a65ab0282ae47cc50035dd455e02"
dependencies = [ dependencies = [
"assign", "assign",
"js_int", "js_int",
@ -3818,7 +3818,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-appservice-api" name = "ruma-appservice-api"
version = "0.10.0" version = "0.10.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b948b40417a65ab0282ae47cc50035dd455e02#a4b948b40417a65ab0282ae47cc50035dd455e02"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3830,7 +3830,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-client-api" name = "ruma-client-api"
version = "0.18.0" version = "0.18.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b948b40417a65ab0282ae47cc50035dd455e02#a4b948b40417a65ab0282ae47cc50035dd455e02"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"assign", "assign",
@ -3853,7 +3853,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-common" name = "ruma-common"
version = "0.13.0" version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b948b40417a65ab0282ae47cc50035dd455e02#a4b948b40417a65ab0282ae47cc50035dd455e02"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"base64 0.22.1", "base64 0.22.1",
@ -3885,7 +3885,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-events" name = "ruma-events"
version = "0.28.1" version = "0.28.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b948b40417a65ab0282ae47cc50035dd455e02#a4b948b40417a65ab0282ae47cc50035dd455e02"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"indexmap 2.9.0", "indexmap 2.9.0",
@ -3910,7 +3910,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-federation-api" name = "ruma-federation-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b948b40417a65ab0282ae47cc50035dd455e02#a4b948b40417a65ab0282ae47cc50035dd455e02"
dependencies = [ dependencies = [
"bytes", "bytes",
"headers", "headers",
@ -3932,7 +3932,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identifiers-validation" name = "ruma-identifiers-validation"
version = "0.9.5" version = "0.9.5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b948b40417a65ab0282ae47cc50035dd455e02#a4b948b40417a65ab0282ae47cc50035dd455e02"
dependencies = [ dependencies = [
"js_int", "js_int",
"thiserror 2.0.12", "thiserror 2.0.12",
@ -3941,7 +3941,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identity-service-api" name = "ruma-identity-service-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b948b40417a65ab0282ae47cc50035dd455e02#a4b948b40417a65ab0282ae47cc50035dd455e02"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3951,7 +3951,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-macros" name = "ruma-macros"
version = "0.13.0" version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b948b40417a65ab0282ae47cc50035dd455e02#a4b948b40417a65ab0282ae47cc50035dd455e02"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"proc-macro-crate", "proc-macro-crate",
@ -3966,7 +3966,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-push-gateway-api" name = "ruma-push-gateway-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b948b40417a65ab0282ae47cc50035dd455e02#a4b948b40417a65ab0282ae47cc50035dd455e02"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3978,7 +3978,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-signatures" name = "ruma-signatures"
version = "0.15.0" version = "0.15.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=a4b948b40417a65ab0282ae47cc50035dd455e02#a4b948b40417a65ab0282ae47cc50035dd455e02"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"ed25519-dalek", "ed25519-dalek",

View file

@ -350,7 +350,7 @@ version = "0.1.2"
[workspace.dependencies.ruma] [workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma" git = "https://forgejo.ellis.link/continuwuation/ruwuma"
#branch = "conduwuit-changes" #branch = "conduwuit-changes"
rev = "9b65f83981f6f53d185ce77da37aaef9dfd764a9" rev = "a4b948b40417a65ab0282ae47cc50035dd455e02"
features = [ features = [
"compat", "compat",
"rand", "rand",

View file

@ -1,2 +1,3 @@
style = "conventional" style = "conventional"
subject_length = 72
allowed_types = ["ci", "build", "fix", "feat", "chore", "docs", "style", "refactor", "perf", "test"] allowed_types = ["ci", "build", "fix", "feat", "chore", "docs", "style", "refactor", "perf", "test"]

View file

@ -398,6 +398,17 @@
# #
#allow_registration = false #allow_registration = false
# If registration is enabled, and this setting is true, new users
# registered after the first admin user will be automatically suspended
# and will require an admin to run `!admin users unsuspend <user_id>`.
#
# Suspended users are still able to read messages, make profile updates,
# leave rooms, and deactivate their account, however cannot send messages,
# invites, or create/join or otherwise modify rooms.
# They are effectively read-only.
#
#suspend_on_register = false
# Enabling this setting opens registration to anyone without restrictions. # Enabling this setting opens registration to anyone without restrictions.
# This makes your server vulnerable to abuse # This makes your server vulnerable to abuse
# #
@ -990,7 +1001,7 @@
# 3 to 5 = Statistics with possible performance impact. # 3 to 5 = Statistics with possible performance impact.
# 6 = All statistics. # 6 = All statistics.
# #
#rocksdb_stats_level = 1 #rocksdb_stats_level = 3
# This is a password that can be configured that will let you login to the # This is a password that can be configured that will let you login to the
# server bot account (currently `@conduit`) for emergency troubleshooting # server bot account (currently `@conduit`) for emergency troubleshooting
@ -1590,11 +1601,9 @@
#stream_amplification = 1024 #stream_amplification = 1024
# Number of sender task workers; determines sender parallelism. Default is # Number of sender task workers; determines sender parallelism. Default is
# '0' which means the value is determined internally, likely matching the # number of CPU cores. Override by setting a different value.
# number of tokio worker-threads or number of cores, etc. Override by
# setting a non-zero value.
# #
#sender_workers = 0 #sender_workers = 4
# Enables listener sockets; can be set to false to disable listening. This # Enables listener sockets; can be set to false to disable listening. This
# option is intended for developer/diagnostic purposes only. # option is intended for developer/diagnostic purposes only.

View file

@ -7,7 +7,10 @@ use std::{
use conduwuit::{ use conduwuit::{
Err, Result, debug_error, err, info, Err, Result, debug_error, err, info,
matrix::pdu::{PduEvent, PduId, RawPduId}, matrix::{
Event,
pdu::{PduEvent, PduId, RawPduId},
},
trace, utils, trace, utils,
utils::{ utils::{
stream::{IterStream, ReadyExt}, stream::{IterStream, ReadyExt},
@ -19,7 +22,7 @@ use futures::{FutureExt, StreamExt, TryStreamExt};
use ruma::{ use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId, OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId,
api::federation::event::get_room_state, api::federation::event::get_room_state, events::AnyStateEvent, serde::Raw,
}; };
use service::rooms::{ use service::rooms::{
short::{ShortEventId, ShortRoomId}, short::{ShortEventId, ShortRoomId},
@ -296,12 +299,12 @@ pub(super) async fn get_remote_pdu(
#[admin_command] #[admin_command]
pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result { pub(super) async fn get_room_state(&self, room: OwnedRoomOrAliasId) -> Result {
let room_id = self.services.rooms.alias.resolve(&room).await?; let room_id = self.services.rooms.alias.resolve(&room).await?;
let room_state: Vec<_> = self let room_state: Vec<Raw<AnyStateEvent>> = self
.services .services
.rooms .rooms
.state_accessor .state_accessor
.room_state_full_pdus(&room_id) .room_state_full_pdus(&room_id)
.map_ok(PduEvent::into_state_event) .map_ok(Event::into_format)
.try_collect() .try_collect()
.await?; .await?;
@ -409,7 +412,9 @@ pub(super) async fn change_log_level(&self, filter: Option<String>, reset: bool)
.reload .reload
.reload(&new_filter_layer, Some(handles)) .reload(&new_filter_layer, Some(handles))
{ {
| Ok(()) => return self.write_str("Successfully changed log level").await, | Ok(()) => {
return self.write_str("Successfully changed log level").await;
},
| Err(e) => { | Err(e) => {
return Err!("Failed to modify and reload the global tracing log level: {e}"); return Err!("Failed to modify and reload the global tracing log level: {e}");
}, },
@ -553,8 +558,8 @@ pub(super) async fn force_set_room_state_from_server(
.latest_pdu_in_room(&room_id) .latest_pdu_in_room(&room_id)
.await .await
.map_err(|_| err!(Database("Failed to find the latest PDU in database")))? .map_err(|_| err!(Database("Failed to find the latest PDU in database")))?
.event_id .event_id()
.clone(), .to_owned(),
}; };
let room_version = self.services.rooms.state.get_room_version(&room_id).await?; let room_version = self.services.rooms.state.get_room_version(&room_id).await?;

View file

@ -94,8 +94,7 @@ async fn process_command(services: Arc<Services>, input: &CommandInput) -> Proce
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
fn handle_panic(error: &Error, command: &CommandInput) -> ProcessorResult { fn handle_panic(error: &Error, command: &CommandInput) -> ProcessorResult {
let link = let link = "Please submit a [bug report](https://forgejo.ellis.link/continuwuation/continuwuity/issues/new). 🥺";
"Please submit a [bug report](https://forgejo.ellis.link/continuwuation/continuwuity/issues/new). 🥺";
let msg = format!("Panic occurred while processing command:\n```\n{error:#?}\n```\n{link}"); let msg = format!("Panic occurred while processing command:\n```\n{error:#?}\n```\n{link}");
let content = RoomMessageEventContent::notice_markdown(msg); let content = RoomMessageEventContent::notice_markdown(msg);
error!("Panic while processing command: {error:?}"); error!("Panic while processing command: {error:?}");

View file

@ -1,11 +1,11 @@
use api::client::leave_room; use api::client::leave_room;
use clap::Subcommand; use clap::Subcommand;
use conduwuit::{ use conduwuit::{
Err, Result, debug, Err, Result, debug, info,
utils::{IterStream, ReadyExt}, utils::{IterStream, ReadyExt},
warn, warn,
}; };
use futures::StreamExt; use futures::{FutureExt, StreamExt};
use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId}; use ruma::{OwnedRoomId, OwnedRoomOrAliasId, RoomAliasId, RoomId, RoomOrAliasId};
use crate::{admin_command, admin_command_dispatch, get_room_info}; use crate::{admin_command, admin_command_dispatch, get_room_info};
@ -70,7 +70,6 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
}; };
debug!("Room specified is a room ID, banning room ID"); debug!("Room specified is a room ID, banning room ID");
self.services.rooms.metadata.ban_room(room_id, true);
room_id.to_owned() room_id.to_owned()
} else if room.is_room_alias_id() { } else if room.is_room_alias_id() {
@ -90,47 +89,25 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
locally, if not using get_alias_helper to fetch room ID remotely" locally, if not using get_alias_helper to fetch room ID remotely"
); );
let room_id = match self match self
.services .services
.rooms .rooms
.alias .alias
.resolve_local_alias(room_alias) .resolve_alias(room_alias, None)
.await .await
{ {
| Ok(room_id) => room_id, | Ok((room_id, servers)) => {
| _ => {
debug!( debug!(
"We don't have this room alias to a room ID locally, attempting to fetch \ ?room_id,
room ID over federation" ?servers,
"Got federation response fetching room ID for room {room}"
); );
room_id
match self
.services
.rooms
.alias
.resolve_alias(room_alias, None)
.await
{
| Ok((room_id, servers)) => {
debug!(
?room_id,
?servers,
"Got federation response fetching room ID for {room_id}"
);
room_id
},
| Err(e) => {
return Err!(
"Failed to resolve room alias {room_alias} to a room ID: {e}"
);
},
}
}, },
}; | Err(e) => {
return Err!("Failed to resolve room alias {room} to a room ID: {e}");
self.services.rooms.metadata.ban_room(&room_id, true); },
}
room_id
} else { } else {
return Err!( return Err!(
"Room specified is not a room ID or room alias. Please note that this requires a \ "Room specified is not a room ID or room alias. Please note that this requires a \
@ -139,7 +116,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
); );
}; };
debug!("Making all users leave the room {room_id} and forgetting it"); info!("Making all users leave the room {room_id} and forgetting it");
let mut users = self let mut users = self
.services .services
.rooms .rooms
@ -150,12 +127,15 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
.boxed(); .boxed();
while let Some(ref user_id) = users.next().await { while let Some(ref user_id) = users.next().await {
debug!( info!(
"Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \ "Attempting leave for user {user_id} in room {room_id} (ignoring all errors, \
evicting admins too)", evicting admins too)",
); );
if let Err(e) = leave_room(self.services, user_id, &room_id, None).await { if let Err(e) = leave_room(self.services, user_id, &room_id, None)
.boxed()
.await
{
warn!("Failed to leave room: {e}"); warn!("Failed to leave room: {e}");
} }
@ -177,10 +157,9 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
}) })
.await; .await;
// unpublish from room directory self.services.rooms.directory.set_not_public(&room_id); // remove from the room directory
self.services.rooms.directory.set_not_public(&room_id); self.services.rooms.metadata.ban_room(&room_id, true); // prevent further joins
self.services.rooms.metadata.disable_room(&room_id, true); // disable federation
self.services.rooms.metadata.disable_room(&room_id, true);
self.write_str( self.write_str(
"Room banned, removed all our local users, and disabled incoming federation with room.", "Room banned, removed all our local users, and disabled incoming federation with room.",
@ -302,8 +281,6 @@ async fn ban_list_of_rooms(&self) -> Result {
} }
for room_id in room_ids { for room_id in room_ids {
self.services.rooms.metadata.ban_room(&room_id, true);
debug!("Banned {room_id} successfully"); debug!("Banned {room_id} successfully");
room_ban_count = room_ban_count.saturating_add(1); room_ban_count = room_ban_count.saturating_add(1);
@ -323,7 +300,10 @@ async fn ban_list_of_rooms(&self) -> Result {
evicting admins too)", evicting admins too)",
); );
if let Err(e) = leave_room(self.services, user_id, &room_id, None).await { if let Err(e) = leave_room(self.services, user_id, &room_id, None)
.boxed()
.await
{
warn!("Failed to leave room: {e}"); warn!("Failed to leave room: {e}");
} }
@ -346,9 +326,9 @@ async fn ban_list_of_rooms(&self) -> Result {
}) })
.await; .await;
self.services.rooms.metadata.ban_room(&room_id, true);
// unpublish from room directory, ignore errors // unpublish from room directory, ignore errors
self.services.rooms.directory.set_not_public(&room_id); self.services.rooms.directory.set_not_public(&room_id);
self.services.rooms.metadata.disable_room(&room_id, true); self.services.rooms.metadata.disable_room(&room_id, true);
} }

View file

@ -1,14 +1,16 @@
use std::{collections::BTreeMap, fmt::Write as _}; use std::{collections::BTreeMap, fmt::Write as _};
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room}; use api::client::{
full_user_deactivate, join_room_by_id_helper, leave_all_rooms, leave_room, update_avatar_url,
update_displayname,
};
use conduwuit::{ use conduwuit::{
Err, Result, debug, debug_warn, error, info, is_equal_to, Err, Result, debug, debug_warn, error, info, is_equal_to,
matrix::pdu::PduBuilder, matrix::{Event, pdu::PduBuilder},
utils::{self, ReadyExt}, utils::{self, ReadyExt},
warn, warn,
}; };
use conduwuit_api::client::{leave_all_rooms, update_avatar_url, update_displayname}; use futures::{FutureExt, StreamExt};
use futures::StreamExt;
use ruma::{ use ruma::{
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
events::{ events::{
@ -284,8 +286,9 @@ pub(super) async fn reset_password(&self, username: String, password: Option<Str
.set_password(&user_id, Some(new_password.as_str())) .set_password(&user_id, Some(new_password.as_str()))
{ {
| Err(e) => return Err!("Couldn't reset the password for user {user_id}: {e}"), | Err(e) => return Err!("Couldn't reset the password for user {user_id}: {e}"),
| Ok(()) => | Ok(()) => {
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`"), write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
},
} }
.await .await
} }
@ -696,7 +699,9 @@ pub(super) async fn force_leave_room(
return Err!("{user_id} is not joined in the room"); return Err!("{user_id} is not joined in the room");
} }
leave_room(self.services, &user_id, &room_id, None).await?; leave_room(self.services, &user_id, &room_id, None)
.boxed()
.await?;
self.write_str(&format!("{user_id} has left {room_id}.",)) self.write_str(&format!("{user_id} has left {room_id}.",))
.await .await
@ -733,7 +738,7 @@ pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAli
.state_accessor .state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "") .room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await .await
.is_ok_and(|event| event.sender == user_id); .is_ok_and(|event| event.sender() == user_id);
if !user_can_demote_self { if !user_can_demote_self {
return Err!("User is not allowed to modify their own power levels in the room.",); return Err!("User is not allowed to modify their own power levels in the room.",);
@ -884,10 +889,7 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
return Err!("Event is already redacted."); return Err!("Event is already redacted.");
} }
let room_id = event.room_id; if !self.services.globals.user_is_local(event.sender()) {
let sender_user = event.sender;
if !self.services.globals.user_is_local(&sender_user) {
return Err!("This command only works on local users."); return Err!("This command only works on local users.");
} }
@ -897,21 +899,21 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
); );
let redaction_event_id = { let redaction_event_id = {
let state_lock = self.services.rooms.state.mutex.lock(&room_id).await; let state_lock = self.services.rooms.state.mutex.lock(event.room_id()).await;
self.services self.services
.rooms .rooms
.timeline .timeline
.build_and_append_pdu( .build_and_append_pdu(
PduBuilder { PduBuilder {
redacts: Some(event.event_id.clone()), redacts: Some(event.event_id().to_owned()),
..PduBuilder::timeline(&RoomRedactionEventContent { ..PduBuilder::timeline(&RoomRedactionEventContent {
redacts: Some(event.event_id.clone()), redacts: Some(event.event_id().to_owned()),
reason: Some(reason), reason: Some(reason),
}) })
}, },
&sender_user, event.sender(),
&room_id, event.room_id(),
&state_lock, &state_lock,
) )
.await? .await?

View file

@ -3,10 +3,9 @@ use std::fmt::Write;
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Error, Result, debug_info, err, error, info, is_equal_to, Err, Error, Event, Result, debug_info, err, error, info, is_equal_to,
matrix::pdu::PduBuilder, matrix::pdu::PduBuilder,
utils, utils::{self, ReadyExt, stream::BroadbandExt},
utils::{ReadyExt, stream::BroadbandExt},
warn, warn,
}; };
use conduwuit_service::Services; use conduwuit_service::Services;
@ -151,16 +150,32 @@ pub(crate) async fn register_route(
if !services.config.allow_registration && body.appservice_info.is_none() { if !services.config.allow_registration && body.appservice_info.is_none() {
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) { match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
| (Some(username), Some(device_display_name)) => { | (Some(username), Some(device_display_name)) => {
info!(%is_guest, user = %username, device_name = %device_display_name, "Rejecting registration attempt as registration is disabled"); info!(
%is_guest,
user = %username,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
}, },
| (Some(username), _) => { | (Some(username), _) => {
info!(%is_guest, user = %username, "Rejecting registration attempt as registration is disabled"); info!(
%is_guest,
user = %username,
"Rejecting registration attempt as registration is disabled"
);
}, },
| (_, Some(device_display_name)) => { | (_, Some(device_display_name)) => {
info!(%is_guest, device_name = %device_display_name, "Rejecting registration attempt as registration is disabled"); info!(
%is_guest,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
}, },
| (None, _) => { | (None, _) => {
info!(%is_guest, "Rejecting registration attempt as registration is disabled"); info!(
%is_guest,
"Rejecting registration attempt as registration is disabled"
);
}, },
} }
@ -351,8 +366,7 @@ pub(crate) async fn register_route(
if !services.globals.new_user_displayname_suffix().is_empty() if !services.globals.new_user_displayname_suffix().is_empty()
&& body.appservice_info.is_none() && body.appservice_info.is_none()
{ {
write!(displayname, " {}", services.server.config.new_user_displayname_suffix) write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
.expect("should be able to write to string buffer");
} }
services services
@ -370,8 +384,7 @@ pub(crate) async fn register_route(
content: ruma::events::push_rules::PushRulesEventContent { content: ruma::events::push_rules::PushRulesEventContent {
global: push::Ruleset::server_default(&user_id), global: push::Ruleset::server_default(&user_id),
}, },
}) })?,
.expect("to json always works"),
) )
.await?; .await?;
@ -416,32 +429,21 @@ pub(crate) async fn register_route(
// log in conduit admin channel if a non-guest user registered // log in conduit admin channel if a non-guest user registered
if body.appservice_info.is_none() && !is_guest { if body.appservice_info.is_none() && !is_guest {
if !device_display_name.is_empty() { if !device_display_name.is_empty() {
info!( let notice = format!(
"New user \"{user_id}\" registered on this server with device display name: \ "New user \"{user_id}\" registered on this server from IP {client} and device \
\"{device_display_name}\"" display name \"{device_display_name}\""
); );
info!("{notice}");
if services.server.config.admin_room_notices { if services.server.config.admin_room_notices {
services services.admin.notice(&notice).await;
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"New user \"{user_id}\" registered on this server from IP {client} and \
device display name \"{device_display_name}\""
)))
.await
.ok();
} }
} else { } else {
info!("New user \"{user_id}\" registered on this server."); let notice = format!("New user \"{user_id}\" registered on this server.");
info!("{notice}");
if services.server.config.admin_room_notices { if services.server.config.admin_room_notices {
services services.admin.notice(&notice).await;
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"New user \"{user_id}\" registered on this server from IP {client}"
)))
.await
.ok();
} }
} }
} }
@ -454,24 +456,22 @@ pub(crate) async fn register_route(
if services.server.config.admin_room_notices { if services.server.config.admin_room_notices {
services services
.admin .admin
.send_message(RoomMessageEventContent::notice_plain(format!( .notice(&format!(
"Guest user \"{user_id}\" with device display name \ "Guest user \"{user_id}\" with device display name \
\"{device_display_name}\" registered on this server from IP {client}" \"{device_display_name}\" registered on this server from IP {client}"
))) ))
.await .await;
.ok();
} }
} else { } else {
#[allow(clippy::collapsible_else_if)] #[allow(clippy::collapsible_else_if)]
if services.server.config.admin_room_notices { if services.server.config.admin_room_notices {
services services
.admin .admin
.send_message(RoomMessageEventContent::notice_plain(format!( .notice(&format!(
"Guest user \"{user_id}\" with no device display name registered on \ "Guest user \"{user_id}\" with no device display name registered on \
this server from IP {client}", this server from IP {client}",
))) ))
.await .await;
.ok();
} }
} }
} }
@ -490,6 +490,25 @@ pub(crate) async fn register_route(
{ {
services.admin.make_user_admin(&user_id).await?; services.admin.make_user_admin(&user_id).await?;
warn!("Granting {user_id} admin privileges as the first user"); warn!("Granting {user_id} admin privileges as the first user");
} else if services.config.suspend_on_register {
// This is not an admin, suspend them.
// Note that we can still do auto joins for suspended users
services
.users
.suspend_account(&user_id, &services.globals.server_user)
.await;
// And send an @room notice to the admin room, to prompt admins to review the
// new user and ideally unsuspend them if deemed appropriate.
if services.server.config.admin_room_notices {
services
.admin
.send_loud_message(RoomMessageEventContent::text_plain(format!(
"User {user_id} has been suspended as they are not the first user \
on this server. Please review and unsuspend them if appropriate."
)))
.await
.ok();
}
} }
} }
} }
@ -584,7 +603,6 @@ pub(crate) async fn change_password_route(
.sender_user .sender_user
.as_ref() .as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?; .ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let sender_device = body.sender_device();
let mut uiaainfo = UiaaInfo { let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }], flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
@ -598,7 +616,7 @@ pub(crate) async fn change_password_route(
| Some(auth) => { | Some(auth) => {
let (worked, uiaainfo) = services let (worked, uiaainfo) = services
.uiaa .uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo) .try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
.await?; .await?;
if !worked { if !worked {
@ -612,7 +630,7 @@ pub(crate) async fn change_password_route(
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services services
.uiaa .uiaa
.create(sender_user, sender_device, &uiaainfo, json); .create(sender_user, body.sender_device(), &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
}, },
@ -631,7 +649,7 @@ pub(crate) async fn change_password_route(
services services
.users .users
.all_device_ids(sender_user) .all_device_ids(sender_user)
.ready_filter(|id| *id != sender_device) .ready_filter(|id| *id != body.sender_device())
.for_each(|id| services.users.remove_device(sender_user, id)) .for_each(|id| services.users.remove_device(sender_user, id))
.await; .await;
@ -640,17 +658,17 @@ pub(crate) async fn change_password_route(
.pusher .pusher
.get_pushkeys(sender_user) .get_pushkeys(sender_user)
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
.broad_filter_map(|pushkey| async move { .broad_filter_map(async |pushkey| {
services services
.pusher .pusher
.get_pusher_device(&pushkey) .get_pusher_device(&pushkey)
.await .await
.ok() .ok()
.filter(|pusher_device| pusher_device != sender_device) .filter(|pusher_device| pusher_device != body.sender_device())
.is_some() .is_some()
.then_some(pushkey) .then_some(pushkey)
}) })
.for_each(|pushkey| async move { .for_each(async |pushkey| {
services.pusher.delete_pusher(sender_user, &pushkey).await; services.pusher.delete_pusher(sender_user, &pushkey).await;
}) })
.await; .await;
@ -661,11 +679,8 @@ pub(crate) async fn change_password_route(
if services.server.config.admin_room_notices { if services.server.config.admin_room_notices {
services services
.admin .admin
.send_message(RoomMessageEventContent::notice_plain(format!( .notice(&format!("User {sender_user} changed their password."))
"User {sender_user} changed their password." .await;
)))
.await
.ok();
} }
Ok(change_password::v3::Response {}) Ok(change_password::v3::Response {})
@ -680,13 +695,10 @@ pub(crate) async fn whoami_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<whoami::v3::Request>, body: Ruma<whoami::v3::Request>,
) -> Result<whoami::v3::Response> { ) -> Result<whoami::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let device_id = body.sender_device.clone();
Ok(whoami::v3::Response { Ok(whoami::v3::Response {
user_id: sender_user.clone(), user_id: body.sender_user().to_owned(),
device_id, device_id: body.sender_device.clone(),
is_guest: services.users.is_deactivated(sender_user).await? is_guest: services.users.is_deactivated(body.sender_user()).await?
&& body.appservice_info.is_none(), && body.appservice_info.is_none(),
}) })
} }
@ -714,7 +726,6 @@ pub(crate) async fn deactivate_route(
.sender_user .sender_user
.as_ref() .as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?; .ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let sender_device = body.sender_device();
let mut uiaainfo = UiaaInfo { let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }], flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
@ -728,7 +739,7 @@ pub(crate) async fn deactivate_route(
| Some(auth) => { | Some(auth) => {
let (worked, uiaainfo) = services let (worked, uiaainfo) = services
.uiaa .uiaa
.try_auth(sender_user, sender_device, auth, &uiaainfo) .try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
.await?; .await?;
if !worked { if !worked {
@ -741,7 +752,7 @@ pub(crate) async fn deactivate_route(
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services services
.uiaa .uiaa
.create(sender_user, sender_device, &uiaainfo, json); .create(sender_user, body.sender_device(), &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
}, },
@ -763,18 +774,17 @@ pub(crate) async fn deactivate_route(
super::update_displayname(&services, sender_user, None, &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; super::update_avatar_url(&services, sender_user, None, None, &all_joined_rooms).await;
full_user_deactivate(&services, sender_user, &all_joined_rooms).await?; full_user_deactivate(&services, sender_user, &all_joined_rooms)
.boxed()
.await?;
info!("User {sender_user} deactivated their account."); info!("User {sender_user} deactivated their account.");
if services.server.config.admin_room_notices { if services.server.config.admin_room_notices {
services services
.admin .admin
.send_message(RoomMessageEventContent::notice_plain(format!( .notice(&format!("User {sender_user} deactivated their account."))
"User {sender_user} deactivated their account." .await;
)))
.await
.ok();
} }
Ok(deactivate::v3::Response { Ok(deactivate::v3::Response {
@ -851,6 +861,7 @@ pub async fn full_user_deactivate(
all_joined_rooms: &[OwnedRoomId], all_joined_rooms: &[OwnedRoomId],
) -> Result<()> { ) -> Result<()> {
services.users.deactivate_account(user_id).await.ok(); services.users.deactivate_account(user_id).await.ok();
super::update_displayname(services, user_id, None, all_joined_rooms).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_avatar_url(services, user_id, None, None, all_joined_rooms).await;
@ -887,7 +898,7 @@ pub async fn full_user_deactivate(
.state_accessor .state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "") .room_state_get(room_id, &StateEventType::RoomCreate, "")
.await .await
.is_ok_and(|event| event.sender == user_id); .is_ok_and(|event| event.sender() == user_id);
if user_can_demote_self { if user_can_demote_self {
let mut power_levels_content = room_power_levels.unwrap_or_default(); let mut power_levels_content = room_power_levels.unwrap_or_default();
@ -915,7 +926,7 @@ pub async fn full_user_deactivate(
} }
} }
super::leave_all_rooms(services, user_id).await; super::leave_all_rooms(services, user_id).boxed().await;
Ok(()) Ok(())
} }

View file

@ -17,7 +17,7 @@ pub(crate) async fn create_alias_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<create_alias::v3::Request>, body: Ruma<create_alias::v3::Request>,
) -> Result<create_alias::v3::Response> { ) -> Result<create_alias::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
@ -65,7 +65,7 @@ pub(crate) async fn delete_alias_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_alias::v3::Request>, body: Ruma<delete_alias::v3::Request>,
) -> Result<delete_alias::v3::Response> { ) -> Result<delete_alias::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }

View file

@ -2,8 +2,10 @@ use std::cmp::Ordering;
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Result, err}; use conduwuit::{Err, Result, err};
use conduwuit_service::Services;
use futures::{FutureExt, future::try_join};
use ruma::{ use ruma::{
UInt, UInt, UserId,
api::client::backup::{ api::client::backup::{
add_backup_keys, add_backup_keys_for_room, add_backup_keys_for_session, add_backup_keys, add_backup_keys_for_room, add_backup_keys_for_session,
create_backup_version, delete_backup_keys, delete_backup_keys_for_room, create_backup_version, delete_backup_keys, delete_backup_keys_for_room,
@ -58,21 +60,9 @@ pub(crate) async fn get_latest_backup_info_route(
.await .await
.map_err(|_| err!(Request(NotFound("Key backup does not exist."))))?; .map_err(|_| err!(Request(NotFound("Key backup does not exist."))))?;
Ok(get_latest_backup_info::v3::Response { let (count, etag) = get_count_etag(&services, body.sender_user(), &version).await?;
algorithm,
count: (UInt::try_from( Ok(get_latest_backup_info::v3::Response { algorithm, count, etag, version })
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,
})
} }
/// # `GET /_matrix/client/v3/room_keys/version/{version}` /// # `GET /_matrix/client/v3/room_keys/version/{version}`
@ -90,17 +80,12 @@ pub(crate) async fn get_backup_info_route(
err!(Request(NotFound("Key backup does not exist at version {:?}", body.version))) err!(Request(NotFound("Key backup does not exist at version {:?}", body.version)))
})?; })?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(get_backup_info::v3::Response { Ok(get_backup_info::v3::Response {
algorithm, algorithm,
count: services count,
.key_backups etag,
.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(), version: body.version.clone(),
}) })
} }
@ -155,17 +140,9 @@ pub(crate) async fn add_backup_keys_route(
} }
} }
Ok(add_backup_keys::v3::Response { let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
count: services
.key_backups Ok(add_backup_keys::v3::Response { count, etag })
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
} }
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}` /// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}`
@ -198,17 +175,9 @@ pub(crate) async fn add_backup_keys_for_room_route(
.await?; .await?;
} }
Ok(add_backup_keys_for_room::v3::Response { let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
count: services
.key_backups Ok(add_backup_keys_for_room::v3::Response { count, etag })
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
} }
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}` /// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
@ -306,17 +275,9 @@ pub(crate) async fn add_backup_keys_for_session_route(
.await?; .await?;
} }
Ok(add_backup_keys_for_session::v3::Response { let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
count: services
.key_backups Ok(add_backup_keys_for_session::v3::Response { count, etag })
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
} }
/// # `GET /_matrix/client/r0/room_keys/keys` /// # `GET /_matrix/client/r0/room_keys/keys`
@ -379,17 +340,9 @@ pub(crate) async fn delete_backup_keys_route(
.delete_all_keys(body.sender_user(), &body.version) .delete_all_keys(body.sender_user(), &body.version)
.await; .await;
Ok(delete_backup_keys::v3::Response { let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
count: services
.key_backups Ok(delete_backup_keys::v3::Response { count, etag })
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
} }
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}` /// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}`
@ -404,17 +357,9 @@ pub(crate) async fn delete_backup_keys_for_room_route(
.delete_room_keys(body.sender_user(), &body.version, &body.room_id) .delete_room_keys(body.sender_user(), &body.version, &body.room_id)
.await; .await;
Ok(delete_backup_keys_for_room::v3::Response { let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
count: services
.key_backups Ok(delete_backup_keys_for_room::v3::Response { count, etag })
.count_keys(body.sender_user(), &body.version)
.await
.try_into()?,
etag: services
.key_backups
.get_etag(body.sender_user(), &body.version)
.await,
})
} }
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}` /// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
@ -429,15 +374,22 @@ pub(crate) async fn delete_backup_keys_for_session_route(
.delete_room_key(body.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; .await;
Ok(delete_backup_keys_for_session::v3::Response { let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
count: services
.key_backups Ok(delete_backup_keys_for_session::v3::Response { count, etag })
.count_keys(body.sender_user(), &body.version) }
.await
.try_into()?, async fn get_count_etag(
etag: services services: &Services,
.key_backups sender_user: &UserId,
.get_etag(body.sender_user(), &body.version) version: &str,
.await, ) -> Result<(UInt, String)> {
}) let count = services
.key_backups
.count_keys(sender_user, version)
.map(TryInto::try_into);
let etag = services.key_backups.get_etag(sender_user, version).map(Ok);
Ok(try_join(count, etag).await?)
} }

View file

@ -26,8 +26,8 @@ pub(crate) async fn get_capabilities_route(
let mut capabilities = Capabilities::default(); let mut capabilities = Capabilities::default();
capabilities.room_versions = RoomVersionsCapability { capabilities.room_versions = RoomVersionsCapability {
default: services.server.config.default_room_version.clone(),
available, available,
default: services.server.config.default_room_version.clone(),
}; };
// we do not implement 3PID stuff // we do not implement 3PID stuff
@ -38,16 +38,12 @@ pub(crate) async fn get_capabilities_route(
}; };
// MSC4133 capability // MSC4133 capability
capabilities capabilities.set("uk.tcpip.msc4133.profile_fields", json!({"enabled": true}))?;
.set("uk.tcpip.msc4133.profile_fields", json!({"enabled": true}))
.expect("this is valid JSON we created");
capabilities capabilities.set(
.set( "org.matrix.msc4267.forget_forced_upon_leave",
"org.matrix.msc4267.forget_forced_upon_leave", json!({"enabled": services.config.forget_forced_upon_leave}),
json!({"enabled": services.config.forget_forced_upon_leave}), )?;
)
.expect("valid JSON we created");
Ok(get_capabilities::v3::Response { capabilities }) Ok(get_capabilities::v3::Response { capabilities })
} }

View file

@ -1,8 +1,6 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Result, at, debug_warn, err, Err, Event, Result, at, debug_warn, err, ref_at,
matrix::pdu::PduEvent,
ref_at,
utils::{ utils::{
IterStream, IterStream,
future::TryExtExt, future::TryExtExt,
@ -111,7 +109,7 @@ pub(crate) async fn get_context_route(
let lazy_loading_context = lazy_loading::Context { let lazy_loading_context = lazy_loading::Context {
user_id: sender_user, user_id: sender_user,
device_id: sender_device, device_id: Some(sender_device),
room_id, room_id,
token: Some(base_count.into_unsigned()), token: Some(base_count.into_unsigned()),
options: Some(&filter.lazy_load_options), options: Some(&filter.lazy_load_options),
@ -179,12 +177,12 @@ pub(crate) async fn get_context_route(
.broad_filter_map(|event_id: &OwnedEventId| { .broad_filter_map(|event_id: &OwnedEventId| {
services.rooms.timeline.get_pdu(event_id.as_ref()).ok() services.rooms.timeline.get_pdu(event_id.as_ref()).ok()
}) })
.map(PduEvent::into_state_event) .map(Event::into_format)
.collect() .collect()
.await; .await;
Ok(get_context::v3::Response { Ok(get_context::v3::Response {
event: base_event.map(at!(1)).map(PduEvent::into_room_event), event: base_event.map(at!(1)).map(Event::into_format),
start: events_before start: events_before
.last() .last()
@ -203,13 +201,13 @@ pub(crate) async fn get_context_route(
events_before: events_before events_before: events_before
.into_iter() .into_iter()
.map(at!(1)) .map(at!(1))
.map(PduEvent::into_room_event) .map(Event::into_format)
.collect(), .collect(),
events_after: events_after events_after: events_after
.into_iter() .into_iter()
.map(at!(1)) .map(at!(1))
.map(PduEvent::into_room_event) .map(Event::into_format)
.collect(), .collect(),
state, state,

View file

@ -21,11 +21,9 @@ pub(crate) async fn get_devices_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_devices::v3::Request>, body: Ruma<get_devices::v3::Request>,
) -> Result<get_devices::v3::Response> { ) -> Result<get_devices::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let devices: Vec<device::Device> = services let devices: Vec<device::Device> = services
.users .users
.all_devices_metadata(sender_user) .all_devices_metadata(body.sender_user())
.collect() .collect()
.await; .await;
@ -39,11 +37,9 @@ pub(crate) async fn get_device_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_device::v3::Request>, body: Ruma<get_device::v3::Request>,
) -> Result<get_device::v3::Response> { ) -> Result<get_device::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let device = services let device = services
.users .users
.get_device_metadata(sender_user, &body.body.device_id) .get_device_metadata(body.sender_user(), &body.body.device_id)
.await .await
.map_err(|_| err!(Request(NotFound("Device not found."))))?; .map_err(|_| err!(Request(NotFound("Device not found."))))?;

View file

@ -1,7 +1,7 @@
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Result, err, info, Err, Event, Result, err, info,
utils::{ utils::{
TryFutureExtExt, TryFutureExtExt,
math::Expected, math::Expected,
@ -352,7 +352,7 @@ async fn user_can_publish_room(
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "") .room_state_get(room_id, &StateEventType::RoomPowerLevels, "")
.await .await
{ {
| Ok(event) => serde_json::from_str(event.content.get()) | Ok(event) => serde_json::from_str(event.content().get())
.map_err(|_| err!(Database("Invalid event content for m.room.power_levels"))) .map_err(|_| err!(Database("Invalid event content for m.room.power_levels")))
.map(|content: RoomPowerLevelsEventContent| { .map(|content: RoomPowerLevelsEventContent| {
RoomPowerLevels::from(content) RoomPowerLevels::from(content)
@ -365,7 +365,7 @@ async fn user_can_publish_room(
.room_state_get(room_id, &StateEventType::RoomCreate, "") .room_state_get(room_id, &StateEventType::RoomCreate, "")
.await .await
{ {
| Ok(event) => Ok(event.sender == user_id), | Ok(event) => Ok(event.sender() == user_id),
| _ => Err!(Request(Forbidden("User is not allowed to publish this room"))), | _ => Err!(Request(Forbidden("User is not allowed to publish this room"))),
} }
}, },

View file

@ -13,11 +13,9 @@ pub(crate) async fn get_filter_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_filter::v3::Request>, body: Ruma<get_filter::v3::Request>,
) -> Result<get_filter::v3::Response> { ) -> Result<get_filter::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services services
.users .users
.get_filter(sender_user, &body.filter_id) .get_filter(body.sender_user(), &body.filter_id)
.await .await
.map(get_filter::v3::Response::new) .map(get_filter::v3::Response::new)
.map_err(|_| err!(Request(NotFound("Filter not found.")))) .map_err(|_| err!(Request(NotFound("Filter not found."))))
@ -30,9 +28,9 @@ pub(crate) async fn create_filter_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<create_filter::v3::Request>, body: Ruma<create_filter::v3::Request>,
) -> Result<create_filter::v3::Response> { ) -> Result<create_filter::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let filter_id = services
.users
let filter_id = services.users.create_filter(sender_user, &body.filter); .create_filter(body.sender_user(), &body.filter);
Ok(create_filter::v3::Response::new(filter_id)) Ok(create_filter::v3::Response::new(filter_id))
} }

View file

@ -126,7 +126,7 @@ pub(crate) async fn get_keys_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_keys::v3::Request>, body: Ruma<get_keys::v3::Request>,
) -> Result<get_keys::v3::Response> { ) -> Result<get_keys::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
get_keys_helper( get_keys_helper(
&services, &services,
@ -157,8 +157,7 @@ pub(crate) async fn upload_signing_keys_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<upload_signing_keys::v3::Request>, body: Ruma<upload_signing_keys::v3::Request>,
) -> Result<upload_signing_keys::v3::Response> { ) -> Result<upload_signing_keys::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let (sender_user, sender_device) = body.sender();
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
// UIAA // UIAA
let mut uiaainfo = UiaaInfo { let mut uiaainfo = UiaaInfo {
@ -203,12 +202,12 @@ pub(crate) async fn upload_signing_keys_route(
} }
// Success! // Success!
}, },
| _ => match body.json_body { | _ => match body.json_body.as_ref() {
| Some(json) => { | Some(json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH)); uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services services
.uiaa .uiaa
.create(sender_user, sender_device, &uiaainfo, &json); .create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo)); return Err(Error::Uiaa(uiaainfo));
}, },
@ -373,7 +372,7 @@ pub(crate) async fn get_key_changes_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_key_changes::v3::Request>, body: Ruma<get_key_changes::v3::Request>,
) -> Result<get_key_changes::v3::Response> { ) -> Result<get_key_changes::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let mut device_list_updates = HashSet::new(); let mut device_list_updates = HashSet::new();

View file

@ -51,7 +51,7 @@ pub(crate) async fn create_content_route(
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
body: Ruma<create_content::v3::Request>, body: Ruma<create_content::v3::Request>,
) -> Result<create_content::v3::Response> { ) -> Result<create_content::v3::Response> {
let user = body.sender_user.as_ref().expect("user is authenticated"); let user = body.sender_user();
if services.users.is_suspended(user).await? { if services.users.is_suspended(user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
@ -97,7 +97,7 @@ pub(crate) async fn get_content_thumbnail_route(
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_thumbnail::v1::Request>, body: Ruma<get_content_thumbnail::v1::Request>,
) -> Result<get_content_thumbnail::v1::Response> { ) -> Result<get_content_thumbnail::v1::Response> {
let user = body.sender_user.as_ref().expect("user is authenticated"); let user = body.sender_user();
let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?; let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
let mxc = Mxc { let mxc = Mxc {
@ -134,7 +134,7 @@ pub(crate) async fn get_content_route(
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content::v1::Request>, body: Ruma<get_content::v1::Request>,
) -> Result<get_content::v1::Response> { ) -> Result<get_content::v1::Response> {
let user = body.sender_user.as_ref().expect("user is authenticated"); let user = body.sender_user();
let mxc = Mxc { let mxc = Mxc {
server_name: &body.server_name, server_name: &body.server_name,
@ -170,7 +170,7 @@ pub(crate) async fn get_content_as_filename_route(
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_content_as_filename::v1::Request>, body: Ruma<get_content_as_filename::v1::Request>,
) -> Result<get_content_as_filename::v1::Response> { ) -> Result<get_content_as_filename::v1::Response> {
let user = body.sender_user.as_ref().expect("user is authenticated"); let user = body.sender_user();
let mxc = Mxc { let mxc = Mxc {
server_name: &body.server_name, server_name: &body.server_name,
@ -206,7 +206,7 @@ pub(crate) async fn get_media_preview_route(
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_media_preview::v1::Request>, body: Ruma<get_media_preview::v1::Request>,
) -> Result<get_media_preview::v1::Response> { ) -> Result<get_media_preview::v1::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let url = &body.url; let url = &body.url;
let url = Url::parse(&body.url).map_err(|e| { let url = Url::parse(&body.url).map_err(|e| {

View file

@ -55,7 +55,7 @@ pub(crate) async fn get_media_preview_legacy_route(
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
body: Ruma<get_media_preview::v3::Request>, body: Ruma<get_media_preview::v3::Request>,
) -> Result<get_media_preview::v3::Response> { ) -> Result<get_media_preview::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let url = &body.url; let url = &body.url;
let url = Url::parse(&body.url).map_err(|e| { let url = Url::parse(&body.url).map_err(|e| {

View file

@ -1276,6 +1276,7 @@ async fn join_room_by_id_helper_remote(
services.rooms.timeline.get_pdu(event_id).await.ok() services.rooms.timeline.get_pdu(event_id).await.ok()
}; };
debug!("running stateres check on send_join parsed PDU");
let auth_check = state_res::event_auth::auth_check( let auth_check = state_res::event_auth::auth_check(
&state_res::RoomVersion::new(&room_version_id)?, &state_res::RoomVersion::new(&room_version_id)?,
&parsed_join_pdu, &parsed_join_pdu,

View file

@ -0,0 +1,60 @@
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::membership::ban_user,
events::room::member::{MembershipState, RoomMemberEventContent},
};
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/ban`
///
/// Tries to send a ban event into the room.
pub(crate) async fn ban_user_route(
State(services): State<crate::State>,
body: Ruma<ban_user::v3::Request>,
) -> Result<ban_user::v3::Response> {
let sender_user = body.sender_user();
if sender_user == body.user_id {
return Err!(Request(Forbidden("You cannot ban yourself.")));
}
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let current_member_content = services
.rooms
.state_accessor
.get_member(&body.room_id, &body.user_id)
.await
.unwrap_or_else(|_| RoomMemberEventContent::new(MembershipState::Ban));
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Ban,
reason: body.reason.clone(),
displayname: None, // display name may be offensive
avatar_url: None, // avatar may be offensive
is_direct: None,
join_authorized_via_users_server: None,
third_party_invite: None,
redact_events: body.redact_events,
..current_member_content
}),
sender_user,
&body.room_id,
&state_lock,
)
.await?;
drop(state_lock);
Ok(ban_user::v3::Response::new())
}

View file

@ -0,0 +1,52 @@
use axum::extract::State;
use conduwuit::{Err, Result, is_matching, result::NotFound, utils::FutureBoolExt};
use futures::pin_mut;
use ruma::{api::client::membership::forget_room, events::room::member::MembershipState};
use crate::Ruma;
/// # `POST /_matrix/client/v3/rooms/{roomId}/forget`
///
/// Forgets about a room.
///
/// - If the sender user currently left the room: Stops sender user from
/// receiving information about the room
///
/// Note: Other devices of the user have no way of knowing the room was
/// forgotten, so this has to be called from every device
pub(crate) async fn forget_room_route(
State(services): State<crate::State>,
body: Ruma<forget_room::v3::Request>,
) -> Result<forget_room::v3::Response> {
let user_id = body.sender_user();
let room_id = &body.room_id;
let joined = services.rooms.state_cache.is_joined(user_id, room_id);
let knocked = services.rooms.state_cache.is_knocked(user_id, room_id);
let invited = services.rooms.state_cache.is_invited(user_id, room_id);
pin_mut!(joined, knocked, invited);
if joined.or(knocked).or(invited).await {
return Err!(Request(Unknown("You must leave the room before forgetting it")));
}
let membership = services
.rooms
.state_accessor
.get_member(room_id, user_id)
.await;
if membership.is_not_found() {
return Err!(Request(Unknown("No membership event was found, room was never joined")));
}
let non_membership = membership
.map(|member| member.membership)
.is_ok_and(is_matching!(MembershipState::Leave | MembershipState::Ban));
if non_membership || services.rooms.state_cache.is_left(user_id, room_id).await {
services.rooms.state_cache.forget(room_id, user_id);
}
Ok(forget_room::v3::Response::new())
}

View file

@ -0,0 +1,238 @@
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
};
use futures::{FutureExt, join};
use ruma::{
OwnedServerName, RoomId, UserId,
api::{client::membership::invite_user, federation::membership::create_invite},
events::room::member::{MembershipState, RoomMemberEventContent},
};
use service::Services;
use super::banned_room_check;
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/invite`
///
/// Tries to send an invite event into the room.
#[tracing::instrument(skip_all, fields(%client), name = "invite")]
pub(crate) async fn invite_user_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<invite_user::v3::Request>,
) -> Result<invite_user::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
debug_error!(
"User {sender_user} is not an admin and attempted to send an invite to room {}",
&body.room_id
);
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
banned_room_check(
&services,
sender_user,
Some(&body.room_id),
body.room_id.server_name(),
client,
)
.await?;
match &body.recipient {
| invite_user::v3::InvitationRecipient::UserId { user_id } => {
let sender_ignored_recipient = services.users.user_is_ignored(sender_user, user_id);
let recipient_ignored_by_sender =
services.users.user_is_ignored(user_id, sender_user);
let (sender_ignored_recipient, recipient_ignored_by_sender) =
join!(sender_ignored_recipient, recipient_ignored_by_sender);
if sender_ignored_recipient {
return Ok(invite_user::v3::Response {});
}
if let Ok(target_user_membership) = services
.rooms
.state_accessor
.get_member(&body.room_id, user_id)
.await
{
if target_user_membership.membership == MembershipState::Ban {
return Err!(Request(Forbidden("User is banned from this room.")));
}
}
if recipient_ignored_by_sender {
// silently drop the invite to the recipient if they've been ignored by the
// sender, pretend it worked
return Ok(invite_user::v3::Response {});
}
invite_helper(
&services,
sender_user,
user_id,
&body.room_id,
body.reason.clone(),
false,
)
.boxed()
.await?;
Ok(invite_user::v3::Response {})
},
| _ => {
Err!(Request(NotFound("User not found.")))
},
}
}
pub(crate) async fn invite_helper(
services: &Services,
sender_user: &UserId,
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
is_direct: bool,
) -> Result {
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
info!(
"User {sender_user} is not an admin and attempted to send an invite to room \
{room_id}"
);
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
if !services.globals.user_is_local(user_id) {
let (pdu, pdu_json, invite_room_state) = {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent {
avatar_url: services.users.avatar_url(user_id).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
};
let (pdu, pdu_json) = services
.rooms
.timeline
.create_hash_and_sign_event(
PduBuilder::state(user_id.to_string(), &content),
sender_user,
room_id,
&state_lock,
)
.await?;
let invite_room_state = services.rooms.state.summary_stripped(&pdu).await;
drop(state_lock);
(pdu, pdu_json, invite_room_state)
};
let room_version_id = services.rooms.state.get_room_version(room_id).await?;
let response = services
.sending
.send_federation_request(user_id.server_name(), create_invite::v2::Request {
room_id: room_id.to_owned(),
event_id: (*pdu.event_id).to_owned(),
room_version: room_version_id.clone(),
event: services
.sending
.convert_to_outgoing_federation_event(pdu_json.clone())
.await,
invite_room_state,
via: services
.rooms
.state_cache
.servers_route_via(room_id)
.await
.ok(),
})
.await?;
// We do not add the event_id field to the pdu here because of signature and
// hashes checks
let (event_id, value) = gen_event_id_canonical_json(&response.event, &room_version_id)
.map_err(|e| {
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
})?;
if pdu.event_id != event_id {
return Err!(Request(BadJson(warn!(
%pdu.event_id, %event_id,
"Server {} sent event with wrong event ID",
user_id.server_name()
))));
}
let origin: OwnedServerName = serde_json::from_value(serde_json::to_value(
value
.get("origin")
.ok_or_else(|| err!(Request(BadJson("Event missing origin field."))))?,
)?)
.map_err(|e| {
err!(Request(BadJson(warn!("Origin field in event is not a valid server name: {e}"))))
})?;
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(&origin, room_id, &event_id, value, true)
.boxed()
.await?
.ok_or_else(|| {
err!(Request(InvalidParam("Could not accept incoming PDU as timeline event.")))
})?;
return services.sending.send_pdu_room(room_id, &pdu_id).await;
}
if !services
.rooms
.state_cache
.is_joined(sender_user, room_id)
.await
{
return Err!(Request(Forbidden(
"You must be joined in the room you are trying to invite from."
)));
}
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let content = RoomMemberEventContent {
displayname: services.users.displayname(user_id).await.ok(),
avatar_url: services.users.avatar_url(user_id).await.ok(),
blurhash: services.users.blurhash(user_id).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
};
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(user_id.to_string(), &content),
sender_user,
room_id,
&state_lock,
)
.await?;
drop(state_lock);
Ok(())
}

View file

@ -0,0 +1,989 @@
use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, debug, debug_info, debug_warn, err, error, info,
matrix::{
StateKey,
event::{gen_event_id, gen_event_id_canonical_json},
pdu::{PduBuilder, PduEvent},
state_res,
},
result::FlatOk,
trace,
utils::{
self, shuffle,
stream::{IterStream, ReadyExt},
},
warn,
};
use futures::{FutureExt, StreamExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
RoomVersionId, UserId,
api::{
client::{
error::ErrorKind,
membership::{ThirdPartySigned, join_room_by_id, join_room_by_id_or_alias},
},
federation::{self},
},
canonical_json::to_canonical_value,
events::{
StateEventType,
room::{
join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
},
};
use service::{
Services,
appservice::RegistrationInfo,
rooms::{
state::RoomMutexGuard,
state_compressor::{CompressedState, HashSetCompressStateEvent},
},
};
use super::banned_room_check;
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
///
/// Tries to join the sender user into a room.
///
/// - If the server knowns about this room: creates the join event and does auth
/// rules locally
/// - If the server does not know about the room: asks other servers over
/// federation
#[tracing::instrument(skip_all, fields(%client), name = "join")]
pub(crate) async fn join_room_by_id_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<join_room_by_id::v3::Request>,
) -> Result<join_room_by_id::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
banned_room_check(
&services,
sender_user,
Some(&body.room_id),
body.room_id.server_name(),
client,
)
.await?;
// There is no body.server_name for /roomId/join
let mut servers: Vec<_> = services
.rooms
.state_cache
.servers_invite_via(&body.room_id)
.map(ToOwned::to_owned)
.collect()
.await;
servers.extend(
services
.rooms
.state_cache
.invite_state(sender_user, &body.room_id)
.await
.unwrap_or_default()
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
if let Some(server) = body.room_id.server_name() {
servers.push(server.into());
}
servers.sort_unstable();
servers.dedup();
shuffle(&mut servers);
join_room_by_id_helper(
&services,
sender_user,
&body.room_id,
body.reason.clone(),
&servers,
body.third_party_signed.as_ref(),
&body.appservice_info,
)
.boxed()
.await
}
/// # `POST /_matrix/client/r0/join/{roomIdOrAlias}`
///
/// Tries to join the sender user into a room.
///
/// - If the server knowns about this room: creates the join event and does auth
/// rules locally
/// - If the server does not know about the room: use the server name query
/// param if specified. if not specified, asks other servers over federation
/// via room alias server name and room ID server name
#[tracing::instrument(skip_all, fields(%client), name = "join")]
pub(crate) async fn join_room_by_id_or_alias_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<join_room_by_id_or_alias::v3::Request>,
) -> Result<join_room_by_id_or_alias::v3::Response> {
let sender_user = body.sender_user();
let appservice_info = &body.appservice_info;
let body = &body.body;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias.clone()) {
| Ok(room_id) => {
banned_room_check(
&services,
sender_user,
Some(&room_id),
room_id.server_name(),
client,
)
.boxed()
.await?;
let mut servers = body.via.clone();
servers.extend(
services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
servers.extend(
services
.rooms
.state_cache
.invite_state(sender_user, &room_id)
.await
.unwrap_or_default()
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
if let Some(server) = room_id.server_name() {
servers.push(server.to_owned());
}
servers.sort_unstable();
servers.dedup();
shuffle(&mut servers);
(servers, room_id)
},
| Err(room_alias) => {
let (room_id, mut servers) = services
.rooms
.alias
.resolve_alias(&room_alias, Some(body.via.clone()))
.await?;
banned_room_check(
&services,
sender_user,
Some(&room_id),
Some(room_alias.server_name()),
client,
)
.await?;
let addl_via_servers = services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned);
let addl_state_servers = services
.rooms
.state_cache
.invite_state(sender_user, &room_id)
.await
.unwrap_or_default();
let mut addl_servers: Vec<_> = addl_state_servers
.iter()
.map(|event| event.get_field("sender"))
.filter_map(FlatOk::flat_ok)
.map(|user: &UserId| user.server_name().to_owned())
.stream()
.chain(addl_via_servers)
.collect()
.await;
addl_servers.sort_unstable();
addl_servers.dedup();
shuffle(&mut addl_servers);
servers.append(&mut addl_servers);
(servers, room_id)
},
};
let join_room_response = join_room_by_id_helper(
&services,
sender_user,
&room_id,
body.reason.clone(),
&servers,
body.third_party_signed.as_ref(),
appservice_info,
)
.boxed()
.await?;
Ok(join_room_by_id_or_alias::v3::Response { room_id: join_room_response.room_id })
}
pub async fn join_room_by_id_helper(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
third_party_signed: Option<&ThirdPartySigned>,
appservice_info: &Option<RegistrationInfo>,
) -> Result<join_room_by_id::v3::Response> {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let user_is_guest = services
.users
.is_deactivated(sender_user)
.await
.unwrap_or(false)
&& appservice_info.is_none();
if user_is_guest && !services.rooms.state_accessor.guest_can_join(room_id).await {
return Err!(Request(Forbidden("Guests are not allowed to join this room")));
}
if services
.rooms
.state_cache
.is_joined(sender_user, room_id)
.await
{
debug_warn!("{sender_user} is already joined in {room_id}");
return Ok(join_room_by_id::v3::Response { room_id: room_id.into() });
}
let server_in_room = services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await;
// Only check our known membership if we're already in the room.
// See: https://forgejo.ellis.link/continuwuation/continuwuity/issues/855
let membership = if server_in_room {
services
.rooms
.state_accessor
.get_member(room_id, sender_user)
.await
} else {
debug!("Ignoring local state for join {room_id}, we aren't in the room yet.");
Ok(RoomMemberEventContent::new(MembershipState::Leave))
};
if let Ok(m) = membership {
if m.membership == MembershipState::Ban {
debug_warn!("{sender_user} is banned from {room_id} but attempted to join");
// TODO: return reason
return Err!(Request(Forbidden("You are banned from the room.")));
}
}
let local_join = server_in_room
|| servers.is_empty()
|| (servers.len() == 1 && services.globals.server_is_ours(&servers[0]));
if local_join {
join_room_by_id_helper_local(
services,
sender_user,
room_id,
reason,
servers,
third_party_signed,
state_lock,
)
.boxed()
.await?;
} else {
// Ask a remote server if we are not participating in this room
join_room_by_id_helper_remote(
services,
sender_user,
room_id,
reason,
servers,
third_party_signed,
state_lock,
)
.boxed()
.await?;
}
Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
}
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_remote")]
async fn join_room_by_id_helper_remote(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
_third_party_signed: Option<&ThirdPartySigned>,
state_lock: RoomMutexGuard,
) -> Result {
info!("Joining {room_id} over federation.");
let (make_join_response, remote_server) =
make_join_request(services, sender_user, room_id, servers).await?;
info!("make_join finished");
let Some(room_version_id) = make_join_response.room_version else {
return Err!(BadServerResponse("Remote room version is not supported by conduwuit"));
};
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut join_event_stub: CanonicalJsonObject =
serde_json::from_str(make_join_response.event.get()).map_err(|e| {
err!(BadServerResponse(warn!(
"Invalid make_join event json received from server: {e:?}"
)))
})?;
let join_authorized_via_users_server = {
use RoomVersionId::*;
if !matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
join_event_stub
.get("content")
.map(|s| {
s.as_object()?
.get("join_authorised_via_users_server")?
.as_str()
})
.and_then(|s| OwnedUserId::try_from(s.unwrap_or_default()).ok())
} else {
None
}
};
join_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
join_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
join_event_stub.insert(
"content".to_owned(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
join_authorized_via_users_server: join_authorized_via_users_server.clone(),
..RoomMemberEventContent::new(MembershipState::Join)
})
.expect("event is valid, we just created it"),
);
// We keep the "event_id" in the pdu only in v1 or
// v2 rooms
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
join_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
// Add event_id back
join_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let mut join_event = join_event_stub;
info!("Asking {remote_server} for send_join in room {room_id}");
let send_join_request = federation::membership::create_join_event::v2::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
omit_members: false,
pdu: services
.sending
.convert_to_outgoing_federation_event(join_event.clone())
.await,
};
let send_join_response = match services
.sending
.send_synapse_request(&remote_server, send_join_request)
.await
{
| Ok(response) => response,
| Err(e) => {
error!("send_join failed: {e}");
return Err(e);
},
};
info!("send_join finished");
if join_authorized_via_users_server.is_some() {
if let Some(signed_raw) = &send_join_response.room_state.event {
debug_info!(
"There is a signed event with join_authorized_via_users_server. This room is \
probably using restricted joins. Adding signature to our event"
);
let (signed_event_id, signed_value) =
gen_event_id_canonical_json(signed_raw, &room_version_id).map_err(|e| {
err!(Request(BadJson(warn!(
"Could not convert event to canonical JSON: {e}"
))))
})?;
if signed_event_id != event_id {
return Err!(Request(BadJson(warn!(
%signed_event_id, %event_id,
"Server {remote_server} sent event with wrong event ID"
))));
}
match signed_value["signatures"]
.as_object()
.ok_or_else(|| {
err!(BadServerResponse(warn!(
"Server {remote_server} sent invalid signatures type"
)))
})
.and_then(|e| {
e.get(remote_server.as_str()).ok_or_else(|| {
err!(BadServerResponse(warn!(
"Server {remote_server} did not send its signature for a restricted \
room"
)))
})
}) {
| Ok(signature) => {
join_event
.get_mut("signatures")
.expect("we created a valid pdu")
.as_object_mut()
.expect("we created a valid pdu")
.insert(remote_server.to_string(), signature.clone());
},
| Err(e) => {
warn!(
"Server {remote_server} sent invalid signature in send_join signatures \
for event {signed_value:?}: {e:?}",
);
},
}
}
}
services
.rooms
.short
.get_or_create_shortroomid(room_id)
.await;
info!("Parsing join event");
let parsed_join_pdu = PduEvent::from_id_val(&event_id, join_event.clone())
.map_err(|e| err!(BadServerResponse("Invalid join event PDU: {e:?}")))?;
info!("Acquiring server signing keys for response events");
let resp_events = &send_join_response.room_state;
let resp_state = &resp_events.state;
let resp_auth = &resp_events.auth_chain;
services
.server_keys
.acquire_events_pubkeys(resp_auth.iter().chain(resp_state.iter()))
.await;
info!("Going through send_join response room_state");
let cork = services.db.cork_and_flush();
let state = send_join_response
.room_state
.state
.iter()
.stream()
.then(|pdu| {
services
.server_keys
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
})
.ready_filter_map(Result::ok)
.fold(HashMap::new(), |mut state, (event_id, value)| async move {
let pdu = match PduEvent::from_id_val(&event_id, value.clone()) {
| Ok(pdu) => pdu,
| Err(e) => {
debug_warn!("Invalid PDU in send_join response: {e:?}: {value:#?}");
return state;
},
};
services.rooms.outlier.add_pdu_outlier(&event_id, &value);
if let Some(state_key) = &pdu.state_key {
let shortstatekey = services
.rooms
.short
.get_or_create_shortstatekey(&pdu.kind.to_string().into(), state_key)
.await;
state.insert(shortstatekey, pdu.event_id.clone());
}
state
})
.await;
drop(cork);
info!("Going through send_join response auth_chain");
let cork = services.db.cork_and_flush();
send_join_response
.room_state
.auth_chain
.iter()
.stream()
.then(|pdu| {
services
.server_keys
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
})
.ready_filter_map(Result::ok)
.ready_for_each(|(event_id, value)| {
services.rooms.outlier.add_pdu_outlier(&event_id, &value);
})
.await;
drop(cork);
debug!("Running send_join auth check");
let fetch_state = &state;
let state_fetch = |k: StateEventType, s: StateKey| async move {
let shortstatekey = services.rooms.short.get_shortstatekey(&k, &s).await.ok()?;
let event_id = fetch_state.get(&shortstatekey)?;
services.rooms.timeline.get_pdu(event_id).await.ok()
};
let auth_check = state_res::event_auth::auth_check(
&state_res::RoomVersion::new(&room_version_id)?,
&parsed_join_pdu,
None, // TODO: third party invite
|k, s| state_fetch(k.clone(), s.into()),
)
.await
.map_err(|e| err!(Request(Forbidden(warn!("Auth check failed: {e:?}")))))?;
if !auth_check {
return Err!(Request(Forbidden("Auth check failed")));
}
info!("Compressing state from send_join");
let compressed: CompressedState = services
.rooms
.state_compressor
.compress_state_events(state.iter().map(|(ssk, eid)| (ssk, eid.borrow())))
.collect()
.await;
debug!("Saving compressed state");
let HashSetCompressStateEvent {
shortstatehash: statehash_before_join,
added,
removed,
} = services
.rooms
.state_compressor
.save_state(room_id, Arc::new(compressed))
.await?;
debug!("Forcing state for new room");
services
.rooms
.state
.force_state(room_id, statehash_before_join, added, removed, &state_lock)
.await?;
info!("Updating joined counts for new room");
services
.rooms
.state_cache
.update_joined_count(room_id)
.await;
// We append to state before appending the pdu, so we don't have a moment in
// time with the pdu without it's state. This is okay because append_pdu can't
// fail.
let statehash_after_join = services
.rooms
.state
.append_to_state(&parsed_join_pdu)
.await?;
info!("Appending new room join event");
services
.rooms
.timeline
.append_pdu(
&parsed_join_pdu,
join_event,
once(parsed_join_pdu.event_id.borrow()),
&state_lock,
)
.await?;
info!("Setting final room state for new room");
// We set the room state after inserting the pdu, so that we never have a moment
// in time where events in the current room state do not exist
services
.rooms
.state
.set_room_state(room_id, statehash_after_join, &state_lock);
Ok(())
}
#[tracing::instrument(skip_all, fields(%sender_user, %room_id), name = "join_local")]
async fn join_room_by_id_helper_local(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
_third_party_signed: Option<&ThirdPartySigned>,
state_lock: RoomMutexGuard,
) -> Result {
debug_info!("We can join locally");
let join_rules_event_content = services
.rooms
.state_accessor
.room_state_get_content::<RoomJoinRulesEventContent>(
room_id,
&StateEventType::RoomJoinRules,
"",
)
.await;
let restriction_rooms = match join_rules_event_content {
| Ok(RoomJoinRulesEventContent {
join_rule: JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted),
}) => restricted
.allow
.into_iter()
.filter_map(|a| match a {
| AllowRule::RoomMembership(r) => Some(r.room_id),
| _ => None,
})
.collect(),
| _ => Vec::new(),
};
let join_authorized_via_users_server: Option<OwnedUserId> = {
if restriction_rooms
.iter()
.stream()
.any(|restriction_room_id| {
services
.rooms
.state_cache
.is_joined(sender_user, restriction_room_id)
})
.await
{
services
.rooms
.state_cache
.local_users_in_room(room_id)
.filter(|user| {
services.rooms.state_accessor.user_can_invite(
room_id,
user,
sender_user,
&state_lock,
)
})
.boxed()
.next()
.await
.map(ToOwned::to_owned)
} else {
None
}
};
let content = RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason: reason.clone(),
join_authorized_via_users_server,
..RoomMemberEventContent::new(MembershipState::Join)
};
// Try normal join first
let Err(error) = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(sender_user.to_string(), &content),
sender_user,
room_id,
&state_lock,
)
.await
else {
return Ok(());
};
if restriction_rooms.is_empty()
&& (servers.is_empty()
|| servers.len() == 1 && services.globals.server_is_ours(&servers[0]))
{
return Err(error);
}
warn!(
"We couldn't do the join locally, maybe federation can help to satisfy the restricted \
join requirements"
);
let Ok((make_join_response, remote_server)) =
make_join_request(services, sender_user, room_id, servers).await
else {
return Err(error);
};
let Some(room_version_id) = make_join_response.room_version else {
return Err!(BadServerResponse("Remote room version is not supported by conduwuit"));
};
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut join_event_stub: CanonicalJsonObject =
serde_json::from_str(make_join_response.event.get()).map_err(|e| {
err!(BadServerResponse("Invalid make_join event json received from server: {e:?}"))
})?;
let join_authorized_via_users_server = join_event_stub
.get("content")
.map(|s| {
s.as_object()?
.get("join_authorised_via_users_server")?
.as_str()
})
.and_then(|s| OwnedUserId::try_from(s.unwrap_or_default()).ok());
join_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
join_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
join_event_stub.insert(
"content".to_owned(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
join_authorized_via_users_server,
..RoomMemberEventContent::new(MembershipState::Join)
})
.expect("event is valid, we just created it"),
);
// We keep the "event_id" in the pdu only in v1 or
// v2 rooms
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
join_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
// Add event_id back
join_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let join_event = join_event_stub;
let send_join_response = services
.sending
.send_synapse_request(
&remote_server,
federation::membership::create_join_event::v2::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
omit_members: false,
pdu: services
.sending
.convert_to_outgoing_federation_event(join_event.clone())
.await,
},
)
.await?;
if let Some(signed_raw) = send_join_response.room_state.event {
let (signed_event_id, signed_value) =
gen_event_id_canonical_json(&signed_raw, &room_version_id).map_err(|e| {
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
})?;
if signed_event_id != event_id {
return Err!(Request(BadJson(
warn!(%signed_event_id, %event_id, "Server {remote_server} sent event with wrong event ID")
)));
}
drop(state_lock);
services
.rooms
.event_handler
.handle_incoming_pdu(&remote_server, room_id, &signed_event_id, signed_value, true)
.boxed()
.await?;
} else {
return Err(error);
}
Ok(())
}
async fn make_join_request(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
servers: &[OwnedServerName],
) -> Result<(federation::membership::prepare_join_event::v1::Response, OwnedServerName)> {
let mut make_join_response_and_server =
Err!(BadServerResponse("No server available to assist in joining."));
let mut make_join_counter: usize = 0;
let mut incompatible_room_version_count: usize = 0;
for remote_server in servers {
if services.globals.server_is_ours(remote_server) {
continue;
}
info!("Asking {remote_server} for make_join ({make_join_counter})");
let make_join_response = services
.sending
.send_federation_request(
remote_server,
federation::membership::prepare_join_event::v1::Request {
room_id: room_id.to_owned(),
user_id: sender_user.to_owned(),
ver: services.server.supported_room_versions().collect(),
},
)
.await;
trace!("make_join response: {:?}", make_join_response);
make_join_counter = make_join_counter.saturating_add(1);
if let Err(ref e) = make_join_response {
if matches!(
e.kind(),
ErrorKind::IncompatibleRoomVersion { .. } | ErrorKind::UnsupportedRoomVersion
) {
incompatible_room_version_count =
incompatible_room_version_count.saturating_add(1);
}
if incompatible_room_version_count > 15 {
info!(
"15 servers have responded with M_INCOMPATIBLE_ROOM_VERSION or \
M_UNSUPPORTED_ROOM_VERSION, assuming that conduwuit does not support the \
room version {room_id}: {e}"
);
make_join_response_and_server =
Err!(BadServerResponse("Room version is not supported by Conduwuit"));
return make_join_response_and_server;
}
if make_join_counter > 40 {
warn!(
"40 servers failed to provide valid make_join response, assuming no server \
can assist in joining."
);
make_join_response_and_server =
Err!(BadServerResponse("No server available to assist in joining."));
return make_join_response_and_server;
}
}
make_join_response_and_server = make_join_response.map(|r| (r, remote_server.clone()));
if make_join_response_and_server.is_ok() {
break;
}
}
make_join_response_and_server
}

View file

@ -0,0 +1,65 @@
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::membership::kick_user,
events::room::member::{MembershipState, RoomMemberEventContent},
};
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/kick`
///
/// Tries to send a kick event into the room.
pub(crate) async fn kick_user_route(
State(services): State<crate::State>,
body: Ruma<kick_user::v3::Request>,
) -> Result<kick_user::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let Ok(event) = services
.rooms
.state_accessor
.get_member(&body.room_id, &body.user_id)
.await
else {
// copy synapse's behaviour of returning 200 without any change to the state
// instead of erroring on left users
return Ok(kick_user::v3::Response::new());
};
if !matches!(
event.membership,
MembershipState::Invite | MembershipState::Knock | MembershipState::Join,
) {
return Err!(Request(Forbidden(
"Cannot kick a user who is not apart of the room (current membership: {})",
event.membership
)));
}
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason: body.reason.clone(),
is_direct: None,
join_authorized_via_users_server: None,
third_party_invite: None,
..event
}),
sender_user,
&body.room_id,
&state_lock,
)
.await?;
drop(state_lock);
Ok(kick_user::v3::Response::new())
}

View file

@ -0,0 +1,770 @@
use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Err, Result, debug, debug_info, debug_warn, err, info,
matrix::{
event::{Event, gen_event_id},
pdu::{PduBuilder, PduEvent},
},
result::FlatOk,
trace,
utils::{self, shuffle, stream::IterStream},
warn,
};
use futures::{FutureExt, StreamExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName, RoomId,
RoomVersionId, UserId,
api::{
client::knock::knock_room,
federation::{self},
},
canonical_json::to_canonical_value,
events::{
StateEventType,
room::{
join_rules::{AllowRule, JoinRule},
member::{MembershipState, RoomMemberEventContent},
},
},
};
use service::{
Services,
rooms::{
state::RoomMutexGuard,
state_compressor::{CompressedState, HashSetCompressStateEvent},
},
};
use super::{banned_room_check, join::join_room_by_id_helper};
use crate::Ruma;
/// # `POST /_matrix/client/*/knock/{roomIdOrAlias}`
///
/// Tries to knock the room to ask permission to join for the sender user.
#[tracing::instrument(skip_all, fields(%client), name = "knock")]
pub(crate) async fn knock_room_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<knock_room::v3::Request>,
) -> Result<knock_room::v3::Response> {
let sender_user = body.sender_user();
let body = &body.body;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias.clone()) {
| Ok(room_id) => {
banned_room_check(
&services,
sender_user,
Some(&room_id),
room_id.server_name(),
client,
)
.await?;
let mut servers = body.via.clone();
servers.extend(
services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
servers.extend(
services
.rooms
.state_cache
.invite_state(sender_user, &room_id)
.await
.unwrap_or_default()
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
if let Some(server) = room_id.server_name() {
servers.push(server.to_owned());
}
servers.sort_unstable();
servers.dedup();
shuffle(&mut servers);
(servers, room_id)
},
| Err(room_alias) => {
let (room_id, mut servers) = services
.rooms
.alias
.resolve_alias(&room_alias, Some(body.via.clone()))
.await?;
banned_room_check(
&services,
sender_user,
Some(&room_id),
Some(room_alias.server_name()),
client,
)
.await?;
let addl_via_servers = services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned);
let addl_state_servers = services
.rooms
.state_cache
.invite_state(sender_user, &room_id)
.await
.unwrap_or_default();
let mut addl_servers: Vec<_> = addl_state_servers
.iter()
.map(|event| event.get_field("sender"))
.filter_map(FlatOk::flat_ok)
.map(|user: &UserId| user.server_name().to_owned())
.stream()
.chain(addl_via_servers)
.collect()
.await;
addl_servers.sort_unstable();
addl_servers.dedup();
shuffle(&mut addl_servers);
servers.append(&mut addl_servers);
(servers, room_id)
},
};
knock_room_by_id_helper(&services, sender_user, &room_id, body.reason.clone(), &servers)
.boxed()
.await
}
async fn knock_room_by_id_helper(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
) -> Result<knock_room::v3::Response> {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
if services
.rooms
.state_cache
.is_invited(sender_user, room_id)
.await
{
debug_warn!("{sender_user} is already invited in {room_id} but attempted to knock");
return Err!(Request(Forbidden(
"You cannot knock on a room you are already invited/accepted to."
)));
}
if services
.rooms
.state_cache
.is_joined(sender_user, room_id)
.await
{
debug_warn!("{sender_user} is already joined in {room_id} but attempted to knock");
return Err!(Request(Forbidden("You cannot knock on a room you are already joined in.")));
}
if services
.rooms
.state_cache
.is_knocked(sender_user, room_id)
.await
{
debug_warn!("{sender_user} is already knocked in {room_id}");
return Ok(knock_room::v3::Response { room_id: room_id.into() });
}
if let Ok(membership) = services
.rooms
.state_accessor
.get_member(room_id, sender_user)
.await
{
if membership.membership == MembershipState::Ban {
debug_warn!("{sender_user} is banned from {room_id} but attempted to knock");
return Err!(Request(Forbidden("You cannot knock on a room you are banned from.")));
}
}
// For knock_restricted rooms, check if the user meets the restricted conditions
// If they do, attempt to join instead of knock
// This is not mentioned in the spec, but should be allowable (we're allowed to
// auto-join invites to knocked rooms)
let join_rule = services.rooms.state_accessor.get_join_rules(room_id).await;
if let JoinRule::KnockRestricted(restricted) = &join_rule {
let restriction_rooms: Vec<_> = restricted
.allow
.iter()
.filter_map(|a| match a {
| AllowRule::RoomMembership(r) => Some(&r.room_id),
| _ => None,
})
.collect();
// Check if the user is in any of the allowed rooms
let mut user_meets_restrictions = false;
for restriction_room_id in &restriction_rooms {
if services
.rooms
.state_cache
.is_joined(sender_user, restriction_room_id)
.await
{
user_meets_restrictions = true;
break;
}
}
// If the user meets the restrictions, try joining instead
if user_meets_restrictions {
debug_info!(
"{sender_user} meets the restricted criteria in knock_restricted room \
{room_id}, attempting to join instead of knock"
);
// For this case, we need to drop the state lock and get a new one in
// join_room_by_id_helper We need to release the lock here and let
// join_room_by_id_helper acquire it again
drop(state_lock);
match join_room_by_id_helper(
services,
sender_user,
room_id,
reason.clone(),
servers,
None,
&None,
)
.await
{
| Ok(_) => return Ok(knock_room::v3::Response::new(room_id.to_owned())),
| Err(e) => {
debug_warn!(
"Failed to convert knock to join for {sender_user} in {room_id}: {e:?}"
);
// Get a new state lock for the remaining knock logic
let new_state_lock = services.rooms.state.mutex.lock(room_id).await;
let server_in_room = services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await;
let local_knock = server_in_room
|| servers.is_empty()
|| (servers.len() == 1 && services.globals.server_is_ours(&servers[0]));
if local_knock {
knock_room_helper_local(
services,
sender_user,
room_id,
reason,
servers,
new_state_lock,
)
.boxed()
.await?;
} else {
knock_room_helper_remote(
services,
sender_user,
room_id,
reason,
servers,
new_state_lock,
)
.boxed()
.await?;
}
return Ok(knock_room::v3::Response::new(room_id.to_owned()));
},
}
}
} else if !matches!(join_rule, JoinRule::Knock | JoinRule::KnockRestricted(_)) {
debug_warn!(
"{sender_user} attempted to knock on room {room_id} but its join rule is \
{join_rule:?}, not knock or knock_restricted"
);
}
let server_in_room = services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await;
let local_knock = server_in_room
|| servers.is_empty()
|| (servers.len() == 1 && services.globals.server_is_ours(&servers[0]));
if local_knock {
knock_room_helper_local(services, sender_user, room_id, reason, servers, state_lock)
.boxed()
.await?;
} else {
knock_room_helper_remote(services, sender_user, room_id, reason, servers, state_lock)
.boxed()
.await?;
}
Ok(knock_room::v3::Response::new(room_id.to_owned()))
}
async fn knock_room_helper_local(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
state_lock: RoomMutexGuard,
) -> Result {
debug_info!("We can knock locally");
let room_version_id = services.rooms.state.get_room_version(room_id).await?;
if matches!(
room_version_id,
RoomVersionId::V1
| RoomVersionId::V2
| RoomVersionId::V3
| RoomVersionId::V4
| RoomVersionId::V5
| RoomVersionId::V6
) {
return Err!(Request(Forbidden("This room does not support knocking.")));
}
let content = RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason: reason.clone(),
..RoomMemberEventContent::new(MembershipState::Knock)
};
// Try normal knock first
let Err(error) = services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(sender_user.to_string(), &content),
sender_user,
room_id,
&state_lock,
)
.await
else {
return Ok(());
};
if servers.is_empty() || (servers.len() == 1 && services.globals.server_is_ours(&servers[0]))
{
return Err(error);
}
warn!("We couldn't do the knock locally, maybe federation can help to satisfy the knock");
let (make_knock_response, remote_server) =
make_knock_request(services, sender_user, room_id, servers).await?;
info!("make_knock finished");
let room_version_id = make_knock_response.room_version;
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut knock_event_stub = serde_json::from_str::<CanonicalJsonObject>(
make_knock_response.event.get(),
)
.map_err(|e| {
err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}"))
})?;
knock_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
knock_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
knock_event_stub.insert(
"content".to_owned(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
..RoomMemberEventContent::new(MembershipState::Knock)
})
.expect("event is valid, we just created it"),
);
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
// Add event_id
knock_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let send_knock_request = federation::knock::send_knock::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
.sending
.convert_to_outgoing_federation_event(knock_event.clone())
.await,
};
let send_knock_response = services
.sending
.send_federation_request(&remote_server, send_knock_request)
.await?;
info!("send_knock finished");
services
.rooms
.short
.get_or_create_shortroomid(room_id)
.await;
info!("Parsing knock event");
let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone())
.map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?;
info!("Updating membership locally to knock state with provided stripped state events");
services
.rooms
.state_cache
.update_membership(
room_id,
sender_user,
parsed_knock_pdu
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
None,
false,
)
.await?;
info!("Appending room knock event locally");
services
.rooms
.timeline
.append_pdu(
&parsed_knock_pdu,
knock_event,
once(parsed_knock_pdu.event_id.borrow()),
&state_lock,
)
.await?;
Ok(())
}
async fn knock_room_helper_remote(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
reason: Option<String>,
servers: &[OwnedServerName],
state_lock: RoomMutexGuard,
) -> Result {
info!("Knocking {room_id} over federation.");
let (make_knock_response, remote_server) =
make_knock_request(services, sender_user, room_id, servers).await?;
info!("make_knock finished");
let room_version_id = make_knock_response.room_version;
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut knock_event_stub: CanonicalJsonObject =
serde_json::from_str(make_knock_response.event.get()).map_err(|e| {
err!(BadServerResponse("Invalid make_knock event json received from server: {e:?}"))
})?;
knock_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
knock_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
knock_event_stub.insert(
"content".to_owned(),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
..RoomMemberEventContent::new(MembershipState::Knock)
})
.expect("event is valid, we just created it"),
);
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
// Add event_id
knock_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let send_knock_request = federation::knock::send_knock::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
.sending
.convert_to_outgoing_federation_event(knock_event.clone())
.await,
};
let send_knock_response = services
.sending
.send_federation_request(&remote_server, send_knock_request)
.await?;
info!("send_knock finished");
services
.rooms
.short
.get_or_create_shortroomid(room_id)
.await;
info!("Parsing knock event");
let parsed_knock_pdu = PduEvent::from_id_val(&event_id, knock_event.clone())
.map_err(|e| err!(BadServerResponse("Invalid knock event PDU: {e:?}")))?;
info!("Going through send_knock response knock state events");
let state = send_knock_response
.knock_room_state
.iter()
.map(|event| serde_json::from_str::<CanonicalJsonObject>(event.clone().into_json().get()))
.filter_map(Result::ok);
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
for event in state {
let Some(state_key) = event.get("state_key") else {
debug_warn!("send_knock stripped state event missing state_key: {event:?}");
continue;
};
let Some(event_type) = event.get("type") else {
debug_warn!("send_knock stripped state event missing event type: {event:?}");
continue;
};
let Ok(state_key) = serde_json::from_value::<String>(state_key.clone().into()) else {
debug_warn!("send_knock stripped state event has invalid state_key: {event:?}");
continue;
};
let Ok(event_type) = serde_json::from_value::<StateEventType>(event_type.clone().into())
else {
debug_warn!("send_knock stripped state event has invalid event type: {event:?}");
continue;
};
let event_id = gen_event_id(&event, &room_version_id)?;
let shortstatekey = services
.rooms
.short
.get_or_create_shortstatekey(&event_type, &state_key)
.await;
services.rooms.outlier.add_pdu_outlier(&event_id, &event);
state_map.insert(shortstatekey, event_id.clone());
}
info!("Compressing state from send_knock");
let compressed: CompressedState = services
.rooms
.state_compressor
.compress_state_events(state_map.iter().map(|(ssk, eid)| (ssk, eid.borrow())))
.collect()
.await;
debug!("Saving compressed state");
let HashSetCompressStateEvent {
shortstatehash: statehash_before_knock,
added,
removed,
} = services
.rooms
.state_compressor
.save_state(room_id, Arc::new(compressed))
.await?;
debug!("Forcing state for new room");
services
.rooms
.state
.force_state(room_id, statehash_before_knock, added, removed, &state_lock)
.await?;
let statehash_after_knock = services
.rooms
.state
.append_to_state(&parsed_knock_pdu)
.await?;
info!("Updating membership locally to knock state with provided stripped state events");
services
.rooms
.state_cache
.update_membership(
room_id,
sender_user,
parsed_knock_pdu
.get_content::<RoomMemberEventContent>()
.expect("we just created this"),
sender_user,
Some(send_knock_response.knock_room_state),
None,
false,
)
.await?;
info!("Appending room knock event locally");
services
.rooms
.timeline
.append_pdu(
&parsed_knock_pdu,
knock_event,
once(parsed_knock_pdu.event_id.borrow()),
&state_lock,
)
.await?;
info!("Setting final room state for new room");
// We set the room state after inserting the pdu, so that we never have a moment
// in time where events in the current room state do not exist
services
.rooms
.state
.set_room_state(room_id, statehash_after_knock, &state_lock);
Ok(())
}
async fn make_knock_request(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
servers: &[OwnedServerName],
) -> Result<(federation::knock::create_knock_event_template::v1::Response, OwnedServerName)> {
let mut make_knock_response_and_server =
Err!(BadServerResponse("No server available to assist in knocking."));
let mut make_knock_counter: usize = 0;
for remote_server in servers {
if services.globals.server_is_ours(remote_server) {
continue;
}
info!("Asking {remote_server} for make_knock ({make_knock_counter})");
let make_knock_response = services
.sending
.send_federation_request(
remote_server,
federation::knock::create_knock_event_template::v1::Request {
room_id: room_id.to_owned(),
user_id: sender_user.to_owned(),
ver: services.server.supported_room_versions().collect(),
},
)
.await;
trace!("make_knock response: {make_knock_response:?}");
make_knock_counter = make_knock_counter.saturating_add(1);
make_knock_response_and_server = make_knock_response.map(|r| (r, remote_server.clone()));
if make_knock_response_and_server.is_ok() {
break;
}
if make_knock_counter > 40 {
warn!(
"50 servers failed to provide valid make_knock response, assuming no server can \
assist in knocking."
);
make_knock_response_and_server =
Err!(BadServerResponse("No server available to assist in knocking."));
return make_knock_response_and_server;
}
}
make_knock_response_and_server
}

View file

@ -0,0 +1,386 @@
use std::collections::HashSet;
use axum::extract::State;
use conduwuit::{
Err, Result, debug_info, debug_warn, err,
matrix::{event::gen_event_id, pdu::PduBuilder},
utils::{self, FutureBoolExt, future::ReadyEqExt},
warn,
};
use futures::{FutureExt, StreamExt, TryFutureExt, pin_mut};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, RoomVersionId, UserId,
api::{
client::membership::leave_room,
federation::{self},
},
events::{
StateEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
};
use service::Services;
use crate::Ruma;
/// # `POST /_matrix/client/v3/rooms/{roomId}/leave`
///
/// Tries to leave the sender user from a room.
///
/// - This should always work if the user is currently joined.
pub(crate) async fn leave_room_route(
State(services): State<crate::State>,
body: Ruma<leave_room::v3::Request>,
) -> Result<leave_room::v3::Response> {
leave_room(&services, body.sender_user(), &body.room_id, body.reason.clone())
.boxed()
.await
.map(|()| leave_room::v3::Response::new())
}
// Make a user leave all their joined rooms, rescinds knocks, forgets all rooms,
// and ignores errors
pub async fn leave_all_rooms(services: &Services, user_id: &UserId) {
let rooms_joined = services
.rooms
.state_cache
.rooms_joined(user_id)
.map(ToOwned::to_owned);
let rooms_invited = services
.rooms
.state_cache
.rooms_invited(user_id)
.map(|(r, _)| r);
let rooms_knocked = services
.rooms
.state_cache
.rooms_knocked(user_id)
.map(|(r, _)| r);
let all_rooms: Vec<_> = rooms_joined
.chain(rooms_invited)
.chain(rooms_knocked)
.collect()
.await;
for room_id in all_rooms {
// ignore errors
if let Err(e) = leave_room(services, user_id, &room_id, None).boxed().await {
warn!(%user_id, "Failed to leave {room_id} remotely: {e}");
}
services.rooms.state_cache.forget(&room_id, user_id);
}
}
pub async fn leave_room(
services: &Services,
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
) -> Result {
let default_member_content = RoomMemberEventContent {
membership: MembershipState::Leave,
reason: reason.clone(),
join_authorized_via_users_server: None,
is_direct: None,
avatar_url: None,
displayname: None,
third_party_invite: None,
blurhash: None,
redact_events: None,
};
let is_banned = services.rooms.metadata.is_banned(room_id);
let is_disabled = services.rooms.metadata.is_disabled(room_id);
pin_mut!(is_banned, is_disabled);
if is_banned.or(is_disabled).await {
// the room is banned/disabled, the room must be rejected locally since we
// cant/dont want to federate with this server
services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
None,
None,
true,
)
.await?;
return Ok(());
}
let dont_have_room = services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.eq(&false);
let not_knocked = services
.rooms
.state_cache
.is_knocked(user_id, room_id)
.eq(&false);
// Ask a remote server if we don't have this room and are not knocking on it
if dont_have_room.and(not_knocked).await {
if let Err(e) = remote_leave_room(services, user_id, room_id, reason.clone())
.boxed()
.await
{
warn!(%user_id, "Failed to leave room {room_id} remotely: {e}");
// Don't tell the client about this error
}
let last_state = services
.rooms
.state_cache
.invite_state(user_id, room_id)
.or_else(|_| services.rooms.state_cache.knock_state(user_id, room_id))
.or_else(|_| services.rooms.state_cache.left_state(user_id, room_id))
.await
.ok();
// We always drop the invite, we can't rely on other servers
services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
last_state,
None,
true,
)
.await?;
} else {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let Ok(event) = services
.rooms
.state_accessor
.room_state_get_content::<RoomMemberEventContent>(
room_id,
&StateEventType::RoomMember,
user_id.as_str(),
)
.await
else {
debug_warn!(
"Trying to leave a room you are not a member of, marking room as left locally."
);
return services
.rooms
.state_cache
.update_membership(
room_id,
user_id,
default_member_content,
user_id,
None,
None,
true,
)
.await;
};
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason,
join_authorized_via_users_server: None,
is_direct: None,
..event
}),
user_id,
room_id,
&state_lock,
)
.await?;
}
Ok(())
}
async fn remote_leave_room(
services: &Services,
user_id: &UserId,
room_id: &RoomId,
reason: Option<String>,
) -> Result<()> {
let mut make_leave_response_and_server =
Err!(BadServerResponse("No remote server available to assist in leaving {room_id}."));
let mut servers: HashSet<OwnedServerName> = services
.rooms
.state_cache
.servers_invite_via(room_id)
.map(ToOwned::to_owned)
.collect()
.await;
match services
.rooms
.state_cache
.invite_state(user_id, room_id)
.await
{
| Ok(invite_state) => {
servers.extend(
invite_state
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
},
| _ => {
match services
.rooms
.state_cache
.knock_state(user_id, room_id)
.await
{
| Ok(knock_state) => {
servers.extend(
knock_state
.iter()
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.filter_map(|sender| {
if !services.globals.user_is_local(sender) {
Some(sender.server_name().to_owned())
} else {
None
}
}),
);
},
| _ => {},
}
},
}
if let Some(room_id_server_name) = room_id.server_name() {
servers.insert(room_id_server_name.to_owned());
}
debug_info!("servers in remote_leave_room: {servers:?}");
for remote_server in servers {
let make_leave_response = services
.sending
.send_federation_request(
&remote_server,
federation::membership::prepare_leave_event::v1::Request {
room_id: room_id.to_owned(),
user_id: user_id.to_owned(),
},
)
.await;
make_leave_response_and_server = make_leave_response.map(|r| (r, remote_server));
if make_leave_response_and_server.is_ok() {
break;
}
}
let (make_leave_response, remote_server) = make_leave_response_and_server?;
let Some(room_version_id) = make_leave_response.room_version else {
return Err!(BadServerResponse(warn!(
"No room version was returned by {remote_server} for {room_id}, room version is \
likely not supported by conduwuit"
)));
};
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(warn!(
"Remote room version {room_version_id} for {room_id} is not supported by conduwuit",
)));
}
let mut leave_event_stub = serde_json::from_str::<CanonicalJsonObject>(
make_leave_response.event.get(),
)
.map_err(|e| {
err!(BadServerResponse(warn!(
"Invalid make_leave event json received from {remote_server} for {room_id}: {e:?}"
)))
})?;
// TODO: Is origin needed?
leave_event_stub.insert(
"origin".to_owned(),
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
);
leave_event_stub.insert(
"origin_server_ts".to_owned(),
CanonicalJsonValue::Integer(
utils::millis_since_unix_epoch()
.try_into()
.expect("Timestamp is valid js_int value"),
),
);
// Inject the reason key into the event content dict if it exists
if let Some(reason) = reason {
if let Some(CanonicalJsonValue::Object(content)) = leave_event_stub.get_mut("content") {
content.insert("reason".to_owned(), CanonicalJsonValue::String(reason));
}
}
// room v3 and above removed the "event_id" field from remote PDU format
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
leave_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut leave_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&leave_event_stub, &room_version_id)?;
// Add event_id back
leave_event_stub
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
// It has enough fields to be called a proper event now
let leave_event = leave_event_stub;
services
.sending
.send_federation_request(
&remote_server,
federation::membership::create_leave_event::v2::Request {
room_id: room_id.to_owned(),
event_id,
pdu: services
.sending
.convert_to_outgoing_federation_event(leave_event.clone())
.await,
},
)
.await?;
Ok(())
}

View file

@ -0,0 +1,147 @@
use axum::extract::State;
use conduwuit::{
Err, Event, Result, at,
utils::{
future::TryExtExt,
stream::{BroadbandExt, ReadyExt},
},
};
use futures::{FutureExt, StreamExt, future::join};
use ruma::{
api::client::membership::{
get_member_events::{self, v3::MembershipEventFilter},
joined_members::{self, v3::RoomMember},
},
events::{
StateEventType,
room::member::{MembershipState, RoomMemberEventContent},
},
};
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/members`
///
/// Lists all joined users in a room (TODO: at a specific point in time, with a
/// specific membership).
///
/// - Only works if the user is currently joined
pub(crate) async fn get_member_events_route(
State(services): State<crate::State>,
body: Ruma<get_member_events::v3::Request>,
) -> Result<get_member_events::v3::Response> {
let sender_user = body.sender_user();
let membership = body.membership.as_ref();
let not_membership = body.not_membership.as_ref();
if !services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, &body.room_id)
.await
{
return Err!(Request(Forbidden("You don't have permission to view this room.")));
}
Ok(get_member_events::v3::Response {
chunk: services
.rooms
.state_accessor
.room_state_full(&body.room_id)
.ready_filter_map(Result::ok)
.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
.map(at!(1))
.ready_filter_map(|pdu| membership_filter(pdu, membership, not_membership))
.map(Event::into_format)
.collect()
.boxed()
.await,
})
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/joined_members`
///
/// Lists all members of a room.
///
/// - The sender user must be in the room
/// - TODO: An appservice just needs a puppet joined
pub(crate) async fn joined_members_route(
State(services): State<crate::State>,
body: Ruma<joined_members::v3::Request>,
) -> Result<joined_members::v3::Response> {
if !services
.rooms
.state_accessor
.user_can_see_state_events(body.sender_user(), &body.room_id)
.await
{
return Err!(Request(Forbidden("You don't have permission to view this room.")));
}
Ok(joined_members::v3::Response {
joined: services
.rooms
.state_cache
.room_members(&body.room_id)
.map(ToOwned::to_owned)
.broad_then(|user_id| async move {
let (display_name, avatar_url) = join(
services.users.displayname(&user_id).ok(),
services.users.avatar_url(&user_id).ok(),
)
.await;
(user_id, RoomMember { display_name, avatar_url })
})
.collect()
.await,
})
}
fn membership_filter<Pdu: Event>(
pdu: Pdu,
for_membership: Option<&MembershipEventFilter>,
not_membership: Option<&MembershipEventFilter>,
) -> Option<impl Event> {
let membership_state_filter = match for_membership {
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
| Some(MembershipEventFilter::Leave) => MembershipState::Leave,
| Some(_) | None => MembershipState::Join,
};
let not_membership_state_filter = match not_membership {
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
| Some(MembershipEventFilter::Join) => MembershipState::Join,
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
| Some(_) | None => MembershipState::Leave,
};
let evt_membership = pdu.get_content::<RoomMemberEventContent>().ok()?.membership;
if for_membership.is_some() && not_membership.is_some() {
if membership_state_filter != evt_membership
|| not_membership_state_filter == evt_membership
{
None
} else {
Some(pdu)
}
} else if for_membership.is_some() && not_membership.is_none() {
if membership_state_filter != evt_membership {
None
} else {
Some(pdu)
}
} else if not_membership.is_some() && for_membership.is_none() {
if not_membership_state_filter == evt_membership {
None
} else {
Some(pdu)
}
} else {
Some(pdu)
}
}

View file

@ -0,0 +1,156 @@
mod ban;
mod forget;
mod invite;
mod join;
mod kick;
mod knock;
mod leave;
mod members;
mod unban;
use std::net::IpAddr;
use axum::extract::State;
use conduwuit::{Err, Result, warn};
use futures::{FutureExt, StreamExt};
use ruma::{OwnedRoomId, RoomId, ServerName, UserId, api::client::membership::joined_rooms};
use service::Services;
pub(crate) use self::{
ban::ban_user_route,
forget::forget_room_route,
invite::{invite_helper, invite_user_route},
join::{join_room_by_id_or_alias_route, join_room_by_id_route},
kick::kick_user_route,
knock::knock_room_route,
leave::leave_room_route,
members::{get_member_events_route, joined_members_route},
unban::unban_user_route,
};
pub use self::{
join::join_room_by_id_helper,
leave::{leave_all_rooms, leave_room},
};
use crate::{Ruma, client::full_user_deactivate};
/// # `POST /_matrix/client/r0/joined_rooms`
///
/// Lists all rooms the user has joined.
pub(crate) async fn joined_rooms_route(
State(services): State<crate::State>,
body: Ruma<joined_rooms::v3::Request>,
) -> Result<joined_rooms::v3::Response> {
Ok(joined_rooms::v3::Response {
joined_rooms: services
.rooms
.state_cache
.rooms_joined(body.sender_user())
.map(ToOwned::to_owned)
.collect()
.await,
})
}
/// Checks if the room is banned in any way possible and the sender user is not
/// an admin.
///
/// Performs automatic deactivation if `auto_deactivate_banned_room_attempts` is
/// enabled
#[tracing::instrument(skip(services))]
pub(crate) async fn banned_room_check(
services: &Services,
user_id: &UserId,
room_id: Option<&RoomId>,
server_name: Option<&ServerName>,
client_ip: IpAddr,
) -> Result {
if services.users.is_admin(user_id).await {
return Ok(());
}
if let Some(room_id) = room_id {
if services.rooms.metadata.is_banned(room_id).await
|| services
.moderation
.is_remote_server_forbidden(room_id.server_name().expect("legacy room mxid"))
{
warn!(
"User {user_id} who is not an admin attempted to send an invite for or \
attempted to join a banned room or banned room server name: {room_id}"
);
if services.server.config.auto_deactivate_banned_room_attempts {
warn!(
"Automatically deactivating user {user_id} due to attempted banned room join"
);
if services.server.config.admin_room_notices {
services
.admin
.send_text(&format!(
"Automatically deactivating user {user_id} due to attempted banned \
room join from IP {client_ip}"
))
.await;
}
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(user_id)
.map(Into::into)
.collect()
.await;
full_user_deactivate(services, user_id, &all_joined_rooms)
.boxed()
.await?;
}
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
}
} else if let Some(server_name) = server_name {
if services
.config
.forbidden_remote_server_names
.is_match(server_name.host())
{
warn!(
"User {user_id} who is not an admin tried joining a room which has the server \
name {server_name} that is globally forbidden. Rejecting.",
);
if services.server.config.auto_deactivate_banned_room_attempts {
warn!(
"Automatically deactivating user {user_id} due to attempted banned room join"
);
if services.server.config.admin_room_notices {
services
.admin
.send_text(&format!(
"Automatically deactivating user {user_id} due to attempted banned \
room join from IP {client_ip}"
))
.await;
}
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(user_id)
.map(Into::into)
.collect()
.await;
full_user_deactivate(services, user_id, &all_joined_rooms)
.boxed()
.await?;
}
return Err!(Request(Forbidden("This remote server is banned on this homeserver.")));
}
}
Ok(())
}

View file

@ -0,0 +1,58 @@
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::membership::unban_user,
events::room::member::{MembershipState, RoomMemberEventContent},
};
use crate::Ruma;
/// # `POST /_matrix/client/r0/rooms/{roomId}/unban`
///
/// Tries to send an unban event into the room.
pub(crate) async fn unban_user_route(
State(services): State<crate::State>,
body: Ruma<unban_user::v3::Request>,
) -> Result<unban_user::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let current_member_content = services
.rooms
.state_accessor
.get_member(&body.room_id, &body.user_id)
.await
.unwrap_or_else(|_| RoomMemberEventContent::new(MembershipState::Leave));
if current_member_content.membership != MembershipState::Ban {
return Err!(Request(Forbidden(
"Cannot unban a user who is not banned (current membership: {})",
current_member_content.membership
)));
}
services
.rooms
.timeline
.build_and_append_pdu(
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason: body.reason.clone(),
join_authorized_via_users_server: None,
third_party_invite: None,
is_direct: None,
..current_member_content
}),
sender_user,
&body.room_id,
&state_lock,
)
.await?;
drop(state_lock);
Ok(unban_user::v3::Response::new())
}

View file

@ -1,12 +1,11 @@
use core::panic;
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Result, at, Err, Result, at,
matrix::{ matrix::{
Event, event::{Event, Matches},
pdu::{PduCount, PduEvent}, pdu::PduCount,
}, },
ref_at,
utils::{ utils::{
IterStream, ReadyExt, IterStream, ReadyExt,
result::{FlatOk, LogErr}, result::{FlatOk, LogErr},
@ -34,6 +33,7 @@ use ruma::{
}, },
serde::Raw, serde::Raw,
}; };
use tracing::warn;
use crate::Ruma; use crate::Ruma;
@ -73,7 +73,7 @@ pub(crate) async fn get_message_events_route(
) -> Result<get_message_events::v3::Response> { ) -> Result<get_message_events::v3::Response> {
debug_assert!(IGNORED_MESSAGE_TYPES.is_sorted(), "IGNORED_MESSAGE_TYPES is not sorted"); debug_assert!(IGNORED_MESSAGE_TYPES.is_sorted(), "IGNORED_MESSAGE_TYPES is not sorted");
let sender_user = body.sender_user(); let sender_user = body.sender_user();
let sender_device = body.sender_device.as_ref(); let sender_device = body.sender_device.as_deref();
let room_id = &body.room_id; let room_id = &body.room_id;
let filter = &body.filter; let filter = &body.filter;
@ -137,18 +137,17 @@ pub(crate) async fn get_message_events_route(
let lazy_loading_context = lazy_loading::Context { let lazy_loading_context = lazy_loading::Context {
user_id: sender_user, user_id: sender_user,
device_id: match sender_device { device_id: sender_device.or_else(|| {
| Some(device_id) => device_id, if let Some(registration) = body.appservice_info.as_ref() {
| None => Some(<&DeviceId>::from(registration.registration.id.as_str()))
if let Some(registration) = body.appservice_info.as_ref() { } else {
<&DeviceId>::from(registration.registration.id.as_str()) warn!(
} else { "No device_id provided and no appservice registration found, this should be \
panic!( unreachable"
"No device_id provided and no appservice registration found, this \ );
should be unreachable" None
); }
}, }),
},
room_id, room_id,
token: Some(from.into_unsigned()), token: Some(from.into_unsigned()),
options: Some(&filter.lazy_load_options), options: Some(&filter.lazy_load_options),
@ -177,7 +176,7 @@ pub(crate) async fn get_message_events_route(
let chunk = events let chunk = events
.into_iter() .into_iter()
.map(at!(1)) .map(at!(1))
.map(PduEvent::into_room_event) .map(Event::into_format)
.collect(); .collect();
Ok(get_message_events::v3::Response { Ok(get_message_events::v3::Response {
@ -218,7 +217,9 @@ where
pin_mut!(receipts); pin_mut!(receipts);
let witness: Witness = events let witness: Witness = events
.stream() .stream()
.map(|(_, pdu)| pdu.sender.clone()) .map(ref_at!(1))
.map(Event::sender)
.map(ToOwned::to_owned)
.chain( .chain(
receipts receipts
.ready_take_while(|(_, c, _)| *c <= newest.into_unsigned()) .ready_take_while(|(_, c, _)| *c <= newest.into_unsigned())
@ -243,7 +244,7 @@ async fn get_member_event(
.rooms .rooms
.state_accessor .state_accessor
.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str()) .room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())
.map_ok(PduEvent::into_state_event) .map_ok(Event::into_format)
.await .await
.ok() .ok()
} }
@ -263,27 +264,33 @@ pub(crate) async fn ignored_filter(
} }
#[inline] #[inline]
pub(crate) async fn is_ignored_pdu( pub(crate) async fn is_ignored_pdu<Pdu>(
services: &Services, services: &Services,
pdu: &PduEvent, event: &Pdu,
user_id: &UserId, user_id: &UserId,
) -> bool { ) -> bool
where
Pdu: Event + Send + Sync,
{
// exclude Synapse's dummy events from bloating up response bodies. clients // exclude Synapse's dummy events from bloating up response bodies. clients
// don't need to see this. // don't need to see this.
if pdu.kind.to_cow_str() == "org.matrix.dummy_event" { if event.kind().to_cow_str() == "org.matrix.dummy_event" {
return true; return true;
} }
let ignored_type = IGNORED_MESSAGE_TYPES.binary_search(&pdu.kind).is_ok(); let ignored_type = IGNORED_MESSAGE_TYPES.binary_search(event.kind()).is_ok();
let ignored_server = services let ignored_server = services
.moderation .moderation
.is_remote_server_ignored(pdu.sender().server_name()); .is_remote_server_ignored(event.sender().server_name());
if ignored_type if ignored_type
&& (ignored_server && (ignored_server
|| (!services.config.send_messages_from_ignored_users_to_client || (!services.config.send_messages_from_ignored_users_to_client
&& services.users.user_is_ignored(&pdu.sender, user_id).await)) && services
.users
.user_is_ignored(event.sender(), user_id)
.await))
{ {
return true; return true;
} }
@ -302,7 +309,7 @@ pub(crate) async fn visibility_filter(
services services
.rooms .rooms
.state_accessor .state_accessor
.user_can_see_event(user_id, &pdu.room_id, &pdu.event_id) .user_can_see_event(user_id, pdu.room_id(), pdu.event_id())
.await .await
.then_some(item) .then_some(item)
} }
@ -310,7 +317,7 @@ pub(crate) async fn visibility_filter(
#[inline] #[inline]
pub(crate) fn event_filter(item: PdusIterItem, filter: &RoomEventFilter) -> Option<PdusIterItem> { pub(crate) fn event_filter(item: PdusIterItem, filter: &RoomEventFilter) -> Option<PdusIterItem> {
let (_, pdu) = &item; let (_, pdu) = &item;
pdu.matches(filter).then_some(item) filter.matches(pdu).then_some(item)
} }
#[cfg_attr(debug_assertions, conduwuit::ctor)] #[cfg_attr(debug_assertions, conduwuit::ctor)]

View file

@ -1,11 +1,8 @@
use std::time::Duration; use std::time::Duration;
use axum::extract::State; use axum::extract::State;
use conduwuit::{Error, Result, utils}; use conduwuit::{Err, Result, utils};
use ruma::{ use ruma::{api::client::account, authentication::TokenType};
api::client::{account, error::ErrorKind},
authentication::TokenType,
};
use super::TOKEN_LENGTH; use super::TOKEN_LENGTH;
use crate::Ruma; use crate::Ruma;
@ -19,17 +16,15 @@ pub(crate) async fn create_openid_token_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<account::request_openid_token::v3::Request>, body: Ruma<account::request_openid_token::v3::Request>,
) -> Result<account::request_openid_token::v3::Response> { ) -> Result<account::request_openid_token::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if sender_user != &body.user_id { if sender_user != body.user_id {
return Err(Error::BadRequest( return Err!(Request(InvalidParam(
ErrorKind::InvalidParam,
"Not allowed to request OpenID tokens on behalf of other users", "Not allowed to request OpenID tokens on behalf of other users",
)); )));
} }
let access_token = utils::random_string(TOKEN_LENGTH); let access_token = utils::random_string(TOKEN_LENGTH);
let expires_in = services let expires_in = services
.users .users
.create_openid_token(&body.user_id, &access_token)?; .create_openid_token(&body.user_id, &access_token)?;

View file

@ -2,21 +2,21 @@ use std::collections::BTreeMap;
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Error, Result, Err, Result,
matrix::pdu::PduBuilder, matrix::pdu::PduBuilder,
utils::{IterStream, stream::TryIgnore}, utils::{IterStream, future::TryExtExt, stream::TryIgnore},
warn, warn,
}; };
use conduwuit_service::Services; use conduwuit_service::Services;
use futures::{StreamExt, TryStreamExt, future::join3}; use futures::{
StreamExt, TryStreamExt,
future::{join, join3, join4},
};
use ruma::{ use ruma::{
OwnedMxcUri, OwnedRoomId, UserId, OwnedMxcUri, OwnedRoomId, UserId,
api::{ api::{
client::{ client::profile::{
error::ErrorKind, get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
profile::{
get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
},
}, },
federation, federation,
}, },
@ -35,7 +35,7 @@ pub(crate) async fn set_displayname_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_display_name::v3::Request>, body: Ruma<set_display_name::v3::Request>,
) -> Result<set_display_name::v3::Response> { ) -> Result<set_display_name::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
@ -110,7 +110,7 @@ pub(crate) async fn get_displayname_route(
if !services.users.exists(&body.user_id).await { if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over // Return 404 if this user doesn't exist and we couldn't fetch it over
// federation // federation
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found.")); return Err!(Request(NotFound("Profile was not found.")));
} }
Ok(get_display_name::v3::Response { Ok(get_display_name::v3::Response {
@ -127,7 +127,7 @@ pub(crate) async fn set_avatar_url_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_avatar_url::v3::Request>, body: Ruma<set_avatar_url::v3::Request>,
) -> Result<set_avatar_url::v3::Response> { ) -> Result<set_avatar_url::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
@ -195,11 +195,9 @@ pub(crate) async fn get_avatar_url_route(
services services
.users .users
.set_displayname(&body.user_id, response.displayname.clone()); .set_displayname(&body.user_id, response.displayname.clone());
services services
.users .users
.set_avatar_url(&body.user_id, response.avatar_url.clone()); .set_avatar_url(&body.user_id, response.avatar_url.clone());
services services
.users .users
.set_blurhash(&body.user_id, response.blurhash.clone()); .set_blurhash(&body.user_id, response.blurhash.clone());
@ -214,13 +212,16 @@ pub(crate) async fn get_avatar_url_route(
if !services.users.exists(&body.user_id).await { if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over // Return 404 if this user doesn't exist and we couldn't fetch it over
// federation // federation
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found.")); return Err!(Request(NotFound("Profile was not found.")));
} }
Ok(get_avatar_url::v3::Response { let (avatar_url, blurhash) = join(
avatar_url: services.users.avatar_url(&body.user_id).await.ok(), services.users.avatar_url(&body.user_id).ok(),
blurhash: services.users.blurhash(&body.user_id).await.ok(), services.users.blurhash(&body.user_id).ok(),
}) )
.await;
Ok(get_avatar_url::v3::Response { avatar_url, blurhash })
} }
/// # `GET /_matrix/client/v3/profile/{userId}` /// # `GET /_matrix/client/v3/profile/{userId}`
@ -253,15 +254,12 @@ pub(crate) async fn get_profile_route(
services services
.users .users
.set_displayname(&body.user_id, response.displayname.clone()); .set_displayname(&body.user_id, response.displayname.clone());
services services
.users .users
.set_avatar_url(&body.user_id, response.avatar_url.clone()); .set_avatar_url(&body.user_id, response.avatar_url.clone());
services services
.users .users
.set_blurhash(&body.user_id, response.blurhash.clone()); .set_blurhash(&body.user_id, response.blurhash.clone());
services services
.users .users
.set_timezone(&body.user_id, response.tz.clone()); .set_timezone(&body.user_id, response.tz.clone());
@ -287,7 +285,7 @@ pub(crate) async fn get_profile_route(
if !services.users.exists(&body.user_id).await { if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over // Return 404 if this user doesn't exist and we couldn't fetch it over
// federation // federation
return Err(Error::BadRequest(ErrorKind::NotFound, "Profile was not found.")); return Err!(Request(NotFound("Profile was not found.")));
} }
let mut custom_profile_fields: BTreeMap<String, serde_json::Value> = services let mut custom_profile_fields: BTreeMap<String, serde_json::Value> = services
@ -300,11 +298,19 @@ pub(crate) async fn get_profile_route(
custom_profile_fields.remove("us.cloke.msc4175.tz"); custom_profile_fields.remove("us.cloke.msc4175.tz");
custom_profile_fields.remove("m.tz"); custom_profile_fields.remove("m.tz");
let (avatar_url, blurhash, displayname, tz) = join4(
services.users.avatar_url(&body.user_id).ok(),
services.users.blurhash(&body.user_id).ok(),
services.users.displayname(&body.user_id).ok(),
services.users.timezone(&body.user_id).ok(),
)
.await;
Ok(get_profile::v3::Response { Ok(get_profile::v3::Response {
avatar_url: services.users.avatar_url(&body.user_id).await.ok(), avatar_url,
blurhash: services.users.blurhash(&body.user_id).await.ok(), blurhash,
displayname: services.users.displayname(&body.user_id).await.ok(), displayname,
tz: services.users.timezone(&body.user_id).await.ok(), tz,
custom_profile_fields, custom_profile_fields,
}) })
} }
@ -316,16 +322,12 @@ pub async fn update_displayname(
all_joined_rooms: &[OwnedRoomId], all_joined_rooms: &[OwnedRoomId],
) { ) {
let (current_avatar_url, current_blurhash, current_displayname) = join3( let (current_avatar_url, current_blurhash, current_displayname) = join3(
services.users.avatar_url(user_id), services.users.avatar_url(user_id).ok(),
services.users.blurhash(user_id), services.users.blurhash(user_id).ok(),
services.users.displayname(user_id), services.users.displayname(user_id).ok(),
) )
.await; .await;
let current_avatar_url = current_avatar_url.ok();
let current_blurhash = current_blurhash.ok();
let current_displayname = current_displayname.ok();
if displayname == current_displayname { if displayname == current_displayname {
return; return;
} }
@ -369,16 +371,12 @@ pub async fn update_avatar_url(
all_joined_rooms: &[OwnedRoomId], all_joined_rooms: &[OwnedRoomId],
) { ) {
let (current_avatar_url, current_blurhash, current_displayname) = join3( let (current_avatar_url, current_blurhash, current_displayname) = join3(
services.users.avatar_url(user_id), services.users.avatar_url(user_id).ok(),
services.users.blurhash(user_id), services.users.blurhash(user_id).ok(),
services.users.displayname(user_id), services.users.displayname(user_id).ok(),
) )
.await; .await;
let current_avatar_url = current_avatar_url.ok();
let current_blurhash = current_blurhash.ok();
let current_displayname = current_displayname.ok();
if current_avatar_url == avatar_url && current_blurhash == blurhash { if current_avatar_url == avatar_url && current_blurhash == blurhash {
return; return;
} }

View file

@ -79,17 +79,14 @@ pub(crate) async fn get_pushrules_all_route(
global_ruleset.update_with_server_default(Ruleset::server_default(sender_user)); global_ruleset.update_with_server_default(Ruleset::server_default(sender_user));
let ty = GlobalAccountDataEventType::PushRules;
let event = PushRulesEvent {
content: PushRulesEventContent { global: global_ruleset.clone() },
};
services services
.account_data .account_data
.update( .update(None, sender_user, ty.to_string().into(), &serde_json::to_value(event)?)
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(PushRulesEvent {
content: PushRulesEventContent { global: global_ruleset.clone() },
})
.expect("to json always works"),
)
.await?; .await?;
} }
}; };
@ -106,7 +103,7 @@ pub(crate) async fn get_pushrules_global_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_pushrules_global_scope::v3::Request>, body: Ruma<get_pushrules_global_scope::v3::Request>,
) -> Result<get_pushrules_global_scope::v3::Response> { ) -> Result<get_pushrules_global_scope::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let Some(content_value) = services let Some(content_value) = services
.account_data .account_data
@ -118,19 +115,17 @@ pub(crate) async fn get_pushrules_global_route(
else { else {
// user somehow has non-existent push rule event. recreate it and return server // user somehow has non-existent push rule event. recreate it and return server
// default silently // default silently
let ty = GlobalAccountDataEventType::PushRules;
let event = PushRulesEvent {
content: PushRulesEventContent {
global: Ruleset::server_default(sender_user),
},
};
services services
.account_data .account_data
.update( .update(None, sender_user, ty.to_string().into(), &serde_json::to_value(event)?)
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?; .await?;
return Ok(get_pushrules_global_scope::v3::Response { return Ok(get_pushrules_global_scope::v3::Response {
@ -223,7 +218,7 @@ pub(crate) async fn get_pushrule_route(
if let Some(rule) = rule { if let Some(rule) = rule {
Ok(get_pushrule::v3::Response { rule }) Ok(get_pushrule::v3::Response { rule })
} else { } else {
Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found.")) Err!(Request(NotFound("Push rule not found.")))
} }
} }
@ -234,9 +229,8 @@ pub(crate) async fn set_pushrule_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_pushrule::v3::Request>, body: Ruma<set_pushrule::v3::Request>,
) -> Result<set_pushrule::v3::Response> { ) -> Result<set_pushrule::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let body = body.body; let body = &body.body;
let mut account_data: PushRulesEvent = services let mut account_data: PushRulesEvent = services
.account_data .account_data
.get_global(sender_user, GlobalAccountDataEventType::PushRules) .get_global(sender_user, GlobalAccountDataEventType::PushRules)
@ -275,14 +269,10 @@ pub(crate) async fn set_pushrule_route(
return Err(err); return Err(err);
} }
let ty = GlobalAccountDataEventType::PushRules;
services services
.account_data .account_data
.update( .update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?; .await?;
Ok(set_pushrule::v3::Response {}) Ok(set_pushrule::v3::Response {})
@ -295,7 +285,7 @@ pub(crate) async fn get_pushrule_actions_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_pushrule_actions::v3::Request>, body: Ruma<get_pushrule_actions::v3::Request>,
) -> Result<get_pushrule_actions::v3::Response> { ) -> Result<get_pushrule_actions::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
// remove old deprecated mentions push rules as per MSC4210 // remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)] #[allow(deprecated)]
@ -329,7 +319,7 @@ pub(crate) async fn set_pushrule_actions_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_pushrule_actions::v3::Request>, body: Ruma<set_pushrule_actions::v3::Request>,
) -> Result<set_pushrule_actions::v3::Response> { ) -> Result<set_pushrule_actions::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let mut account_data: PushRulesEvent = services let mut account_data: PushRulesEvent = services
.account_data .account_data
@ -343,17 +333,13 @@ pub(crate) async fn set_pushrule_actions_route(
.set_actions(body.kind.clone(), &body.rule_id, body.actions.clone()) .set_actions(body.kind.clone(), &body.rule_id, body.actions.clone())
.is_err() .is_err()
{ {
return Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found.")); return Err!(Request(NotFound("Push rule not found.")));
} }
let ty = GlobalAccountDataEventType::PushRules;
services services
.account_data .account_data
.update( .update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?; .await?;
Ok(set_pushrule_actions::v3::Response {}) Ok(set_pushrule_actions::v3::Response {})
@ -366,7 +352,7 @@ pub(crate) async fn get_pushrule_enabled_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_pushrule_enabled::v3::Request>, body: Ruma<get_pushrule_enabled::v3::Request>,
) -> Result<get_pushrule_enabled::v3::Response> { ) -> Result<get_pushrule_enabled::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
// remove old deprecated mentions push rules as per MSC4210 // remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)] #[allow(deprecated)]
@ -400,7 +386,7 @@ pub(crate) async fn set_pushrule_enabled_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_pushrule_enabled::v3::Request>, body: Ruma<set_pushrule_enabled::v3::Request>,
) -> Result<set_pushrule_enabled::v3::Response> { ) -> Result<set_pushrule_enabled::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let mut account_data: PushRulesEvent = services let mut account_data: PushRulesEvent = services
.account_data .account_data
@ -414,17 +400,13 @@ pub(crate) async fn set_pushrule_enabled_route(
.set_enabled(body.kind.clone(), &body.rule_id, body.enabled) .set_enabled(body.kind.clone(), &body.rule_id, body.enabled)
.is_err() .is_err()
{ {
return Err(Error::BadRequest(ErrorKind::NotFound, "Push rule not found.")); return Err!(Request(NotFound("Push rule not found.")));
} }
let ty = GlobalAccountDataEventType::PushRules;
services services
.account_data .account_data
.update( .update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?; .await?;
Ok(set_pushrule_enabled::v3::Response {}) Ok(set_pushrule_enabled::v3::Response {})
@ -437,7 +419,7 @@ pub(crate) async fn delete_pushrule_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_pushrule::v3::Request>, body: Ruma<delete_pushrule::v3::Request>,
) -> Result<delete_pushrule::v3::Response> { ) -> Result<delete_pushrule::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let mut account_data: PushRulesEvent = services let mut account_data: PushRulesEvent = services
.account_data .account_data
@ -463,14 +445,10 @@ pub(crate) async fn delete_pushrule_route(
return Err(err); return Err(err);
} }
let ty = GlobalAccountDataEventType::PushRules;
services services
.account_data .account_data
.update( .update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?; .await?;
Ok(delete_pushrule::v3::Response {}) Ok(delete_pushrule::v3::Response {})
@ -483,7 +461,7 @@ pub(crate) async fn get_pushers_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_pushers::v3::Request>, body: Ruma<get_pushers::v3::Request>,
) -> Result<get_pushers::v3::Response> { ) -> Result<get_pushers::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
Ok(get_pushers::v3::Response { Ok(get_pushers::v3::Response {
pushers: services.pusher.get_pushers(sender_user).await, pushers: services.pusher.get_pushers(sender_user).await,
@ -499,7 +477,7 @@ pub(crate) async fn set_pushers_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_pusher::v3::Request>, body: Ruma<set_pusher::v3::Request>,
) -> Result<set_pusher::v3::Response> { ) -> Result<set_pusher::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
services services
.pusher .pusher
@ -515,19 +493,16 @@ async fn recreate_push_rules_and_return(
services: &Services, services: &Services,
sender_user: &ruma::UserId, sender_user: &ruma::UserId,
) -> Result<get_pushrules_all::v3::Response> { ) -> Result<get_pushrules_all::v3::Response> {
let ty = GlobalAccountDataEventType::PushRules;
let event = PushRulesEvent {
content: PushRulesEventContent {
global: Ruleset::server_default(sender_user),
},
};
services services
.account_data .account_data
.update( .update(None, sender_user, ty.to_string().into(), &serde_json::to_value(event)?)
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?; .await?;
Ok(get_pushrules_all::v3::Response { Ok(get_pushrules_all::v3::Response {

View file

@ -37,7 +37,7 @@ pub(crate) async fn set_read_marker_route(
Some(&body.room_id), Some(&body.room_id),
sender_user, sender_user,
RoomAccountDataEventType::FullyRead, RoomAccountDataEventType::FullyRead,
&serde_json::to_value(fully_read_event).expect("to json value always works"), &serde_json::to_value(fully_read_event)?,
) )
.await?; .await?;
} }
@ -151,7 +151,7 @@ pub(crate) async fn create_receipt_route(
Some(&body.room_id), Some(&body.room_id),
sender_user, sender_user,
RoomAccountDataEventType::FullyRead, RoomAccountDataEventType::FullyRead,
&serde_json::to_value(fully_read_event).expect("to json value always works"), &serde_json::to_value(fully_read_event)?,
) )
.await?; .await?;
}, },

View file

@ -15,8 +15,8 @@ pub(crate) async fn redact_event_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<redact_event::v3::Request>, body: Ruma<redact_event::v3::Request>,
) -> Result<redact_event::v3::Response> { ) -> Result<redact_event::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let body = body.body; let body = &body.body;
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
// TODO: Users can redact their own messages while suspended // TODO: Users can redact their own messages while suspended
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));

View file

@ -1,10 +1,10 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Result, at, Result, at,
matrix::pdu::PduCount, matrix::{Event, event::RelationTypeEqual, pdu::PduCount},
utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt}, utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt},
}; };
use conduwuit_service::{Services, rooms::timeline::PdusIterItem}; use conduwuit_service::Services;
use futures::StreamExt; use futures::StreamExt;
use ruma::{ use ruma::{
EventId, RoomId, UInt, UserId, EventId, RoomId, UInt, UserId,
@ -129,7 +129,7 @@ async fn paginate_relations_with_filter(
// Spec (v1.10) recommends depth of at least 3 // Spec (v1.10) recommends depth of at least 3
let depth: u8 = if recurse { 3 } else { 1 }; let depth: u8 = if recurse { 3 } else { 1 };
let events: Vec<PdusIterItem> = services let events: Vec<_> = services
.rooms .rooms
.pdu_metadata .pdu_metadata
.get_relations(sender_user, room_id, target, start, limit, depth, dir) .get_relations(sender_user, room_id, target, start, limit, depth, dir)
@ -138,12 +138,12 @@ async fn paginate_relations_with_filter(
.filter(|(_, pdu)| { .filter(|(_, pdu)| {
filter_event_type filter_event_type
.as_ref() .as_ref()
.is_none_or(|kind| *kind == pdu.kind) .is_none_or(|kind| kind == pdu.kind())
}) })
.filter(|(_, pdu)| { .filter(|(_, pdu)| {
filter_rel_type filter_rel_type
.as_ref() .as_ref()
.is_none_or(|rel_type| pdu.relation_type_equal(rel_type)) .is_none_or(|rel_type| rel_type.relation_type_equal(pdu))
}) })
.stream() .stream()
.ready_take_while(|(count, _)| Some(*count) != to) .ready_take_while(|(count, _)| Some(*count) != to)
@ -167,22 +167,22 @@ async fn paginate_relations_with_filter(
chunk: events chunk: events
.into_iter() .into_iter()
.map(at!(1)) .map(at!(1))
.map(|pdu| pdu.to_message_like_event()) .map(Event::into_format)
.collect(), .collect(),
}) })
} }
async fn visibility_filter( async fn visibility_filter<Pdu: Event + Send + Sync>(
services: &Services, services: &Services,
sender_user: &UserId, sender_user: &UserId,
item: PdusIterItem, item: (PduCount, Pdu),
) -> Option<PdusIterItem> { ) -> Option<(PduCount, Pdu)> {
let (_, pdu) = &item; let (_, pdu) = &item;
services services
.rooms .rooms
.state_accessor .state_accessor
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id) .user_can_see_event(sender_user, pdu.room_id(), pdu.event_id())
.await .await
.then_some(item) .then_some(item)
} }

View file

@ -1,23 +1,33 @@
use std::time::Duration; use std::{fmt::Write as _, ops::Mul, time::Duration};
use axum::extract::State; use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, Error, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt}; use conduwuit::{Err, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
use conduwuit_service::Services; use conduwuit_service::Services;
use rand::Rng; use rand::Rng;
use ruma::{ use ruma::{
EventId, RoomId, UserId, EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
api::client::{ api::client::{
error::ErrorKind, report_user,
room::{report_content, report_room}, room::{report_content, report_room},
}, },
events::room::message, events::{Mentions, room::message::RoomMessageEventContent},
int, int,
}; };
use tokio::time::sleep; use tokio::time::sleep;
use crate::Ruma; use crate::Ruma;
struct Report {
sender: OwnedUserId,
room_id: Option<OwnedRoomId>,
event_id: Option<OwnedEventId>,
user_id: Option<OwnedUserId>,
report_type: String,
reason: Option<String>,
score: Option<ruma::Int>,
}
/// # `POST /_matrix/client/v3/rooms/{roomId}/report` /// # `POST /_matrix/client/v3/rooms/{roomId}/report`
/// ///
/// Reports an abusive room to homeserver admins /// Reports an abusive room to homeserver admins
@ -27,19 +37,14 @@ pub(crate) async fn report_room_route(
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
body: Ruma<report_room::v3::Request>, body: Ruma<report_room::v3::Request>,
) -> Result<report_room::v3::Response> { ) -> Result<report_room::v3::Response> {
// user authentication let sender_user = body.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
info!( }
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
body.room_id,
body.reason.as_deref().unwrap_or("")
);
if body.reason.as_ref().is_some_and(|s| s.len() > 750) { if body.reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err(Error::BadRequest( return Err!(Request(
ErrorKind::InvalidParam, InvalidParam("Reason too long, should be 750 characters or fewer",)
"Reason too long, should be 750 characters or fewer",
)); ));
} }
@ -55,19 +60,23 @@ pub(crate) async fn report_room_route(
"Room does not exist to us, no local users have joined at all" "Room does not exist to us, no local users have joined at all"
))); )));
} }
info!(
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
body.room_id,
body.reason.as_deref().unwrap_or("")
);
// send admin room message that we received the report with an @room ping for let report = Report {
// urgency sender: sender_user.to_owned(),
services room_id: Some(body.room_id.clone()),
.admin event_id: None,
.send_message(message::RoomMessageEventContent::text_markdown(format!( user_id: None,
"@room Room report received from {} -\n\nRoom ID: {}\n\nReport Reason: {}", report_type: "room".to_owned(),
sender_user.to_owned(), reason: body.reason.clone(),
body.room_id, score: None,
body.reason.as_deref().unwrap_or("") };
)))
.await services.admin.send_message(build_report(report)).await.ok();
.ok();
Ok(report_room::v3::Response {}) Ok(report_room::v3::Response {})
} }
@ -82,15 +91,10 @@ pub(crate) async fn report_event_route(
body: Ruma<report_content::v3::Request>, body: Ruma<report_content::v3::Request>,
) -> Result<report_content::v3::Response> { ) -> Result<report_content::v3::Response> {
// user authentication // user authentication
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
info!( return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
"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; delay_response().await;
@ -109,27 +113,73 @@ pub(crate) async fn report_event_route(
&pdu, &pdu,
) )
.await?; .await?;
info!(
// send admin room message that we received the report with an @room ping for "Received event report by user {sender_user} for room {} and event ID {}, with reason: \
// urgency \"{}\"",
services body.room_id,
.admin body.event_id,
.send_message(message::RoomMessageEventContent::text_markdown(format!( body.reason.as_deref().unwrap_or("")
"@room Event report received from {} -\n\nEvent ID: {}\nRoom ID: {}\nSent By: \ );
{}\n\nReport Score: {}\nReport Reason: {}", let report = Report {
sender_user.to_owned(), sender: sender_user.to_owned(),
pdu.event_id, room_id: Some(body.room_id.clone()),
pdu.room_id, event_id: Some(body.event_id.clone()),
pdu.sender, user_id: None,
body.score.unwrap_or_else(|| ruma::Int::from(0)), report_type: "event".to_owned(),
body.reason.as_deref().unwrap_or("") reason: body.reason.clone(),
))) score: body.score,
.await };
.ok(); services.admin.send_message(build_report(report)).await.ok();
Ok(report_content::v3::Response {}) Ok(report_content::v3::Response {})
} }
#[tracing::instrument(skip_all, fields(%client), name = "report_user")]
pub(crate) async fn report_user_route(
State(services): State<crate::State>,
InsecureClientIp(client): InsecureClientIp,
body: Ruma<report_user::v3::Request>,
) -> Result<report_user::v3::Response> {
// user authentication
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if body.reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err!(Request(
InvalidParam("Reason too long, should be 750 characters or fewer",)
));
}
delay_response().await;
if !services.users.is_active_local(&body.user_id).await {
// return 200 as to not reveal if the user exists. Recommended by spec.
return Ok(report_user::v3::Response {});
}
let report = Report {
sender: sender_user.to_owned(),
room_id: None,
event_id: None,
user_id: Some(body.user_id.clone()),
report_type: "user".to_owned(),
reason: body.reason.clone(),
score: None,
};
info!(
"Received room report from {sender_user} for user {} with reason: \"{}\"",
body.user_id,
body.reason.as_deref().unwrap_or("")
);
services.admin.send_message(build_report(report)).await.ok();
Ok(report_user::v3::Response {})
}
/// in the following order: /// in the following order:
/// ///
/// check if the room ID from the URI matches the PDU's room ID /// check if the room ID from the URI matches the PDU's room ID
@ -151,23 +201,16 @@ async fn is_event_report_valid(
); );
if room_id != pdu.room_id { if room_id != pdu.room_id {
return Err(Error::BadRequest( return Err!(Request(NotFound("Event ID does not belong to the reported room",)));
ErrorKind::NotFound,
"Event ID does not belong to the reported room",
));
} }
if score.is_some_and(|s| s > int!(0) || s < int!(-100)) { if score.is_some_and(|s| s > int!(0) || s < int!(-100)) {
return Err(Error::BadRequest( return Err!(Request(InvalidParam("Invalid score, must be within 0 to -100",)));
ErrorKind::InvalidParam,
"Invalid score, must be within 0 to -100",
));
} }
if reason.as_ref().is_some_and(|s| s.len() > 750) { if reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err(Error::BadRequest( return Err!(Request(
ErrorKind::InvalidParam, InvalidParam("Reason too long, should be 750 characters or fewer",)
"Reason too long, should be 750 characters or fewer",
)); ));
} }
@ -178,15 +221,35 @@ async fn is_event_report_valid(
.ready_any(|user_id| user_id == sender_user) .ready_any(|user_id| user_id == sender_user)
.await .await
{ {
return Err(Error::BadRequest( return Err!(Request(NotFound("You are not in the room you are reporting.",)));
ErrorKind::NotFound,
"You are not in the room you are reporting.",
));
} }
Ok(()) Ok(())
} }
/// Builds a report message to be sent to the admin room.
fn build_report(report: Report) -> RoomMessageEventContent {
let mut text =
format!("@room New {} report received from {}:\n\n", report.report_type, report.sender);
if report.user_id.is_some() {
let _ = writeln!(text, "- Reported User ID: `{}`", report.user_id.unwrap());
}
if report.room_id.is_some() {
let _ = writeln!(text, "- Reported Room ID: `{}`", report.room_id.unwrap());
}
if report.event_id.is_some() {
let _ = writeln!(text, "- Reported Event ID: `{}`", report.event_id.unwrap());
}
if let Some(score) = report.score {
let _ = writeln!(text, "- User-supplied offensiveness score: {}%", score.mul(int!(-1)));
}
if let Some(reason) = report.reason {
let _ = writeln!(text, "- Report Reason: {reason}");
}
RoomMessageEventContent::text_markdown(text).add_mentions(Mentions::with_room_mention())
}
/// even though this is kinda security by obscurity, let's still make a small /// even though this is kinda security by obscurity, let's still make a small
/// random delay sending a response per spec suggestion regarding /// random delay sending a response per spec suggestion regarding
/// enumerating for potential events existing in our server. /// enumerating for potential events existing in our server.
@ -196,5 +259,6 @@ async fn delay_response() {
"Got successful /report request, waiting {time_to_wait} seconds before sending \ "Got successful /report request, waiting {time_to_wait} seconds before sending \
successful response." successful response."
); );
sleep(Duration::from_secs(time_to_wait)).await; sleep(Duration::from_secs(time_to_wait)).await;
} }

View file

@ -1,7 +1,7 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{Error, Result}; use conduwuit::{Err, Result};
use futures::StreamExt; use futures::StreamExt;
use ruma::api::client::{error::ErrorKind, room::aliases}; use ruma::api::client::room::aliases;
use crate::Ruma; use crate::Ruma;
@ -15,7 +15,7 @@ pub(crate) async fn get_room_aliases_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<aliases::v3::Request>, body: Ruma<aliases::v3::Request>,
) -> Result<aliases::v3::Response> { ) -> Result<aliases::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if !services if !services
.rooms .rooms
@ -23,10 +23,7 @@ pub(crate) async fn get_room_aliases_route(
.user_can_see_state_events(sender_user, &body.room_id) .user_can_see_state_events(sender_user, &body.room_id)
.await .await
{ {
return Err(Error::BadRequest( return Err!(Request(Forbidden("You don't have permission to view this room.",)));
ErrorKind::forbidden(),
"You don't have permission to view this room.",
));
} }
Ok(aliases::v3::Response { Ok(aliases::v3::Response {

View file

@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Error, Result, debug_info, debug_warn, err, error, info, Err, Result, debug_info, debug_warn, err, info,
matrix::{StateKey, pdu::PduBuilder}, matrix::{StateKey, pdu::PduBuilder},
warn, warn,
}; };
@ -10,10 +10,7 @@ use conduwuit_service::{Services, appservice::RegistrationInfo};
use futures::FutureExt; use futures::FutureExt;
use ruma::{ use ruma::{
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId, CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
api::client::{ api::client::room::{self, create_room},
error::ErrorKind,
room::{self, create_room},
},
events::{ events::{
TimelineEventType, TimelineEventType,
room::{ room::{
@ -58,16 +55,13 @@ pub(crate) async fn create_room_route(
) -> Result<create_room::v3::Response> { ) -> Result<create_room::v3::Response> {
use create_room::v3::RoomPreset; use create_room::v3::RoomPreset;
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if !services.globals.allow_room_creation() if !services.globals.allow_room_creation()
&& body.appservice_info.is_none() && body.appservice_info.is_none()
&& !services.users.is_admin(sender_user).await && !services.users.is_admin(sender_user).await
{ {
return Err(Error::BadRequest( return Err!(Request(Forbidden("Room creation has been disabled.",)));
ErrorKind::forbidden(),
"Room creation has been disabled.",
));
} }
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
@ -81,10 +75,7 @@ pub(crate) async fn create_room_route(
// check if room ID doesn't already exist instead of erroring on auth check // check if room ID doesn't already exist instead of erroring on auth check
if services.rooms.short.get_shortroomid(&room_id).await.is_ok() { if services.rooms.short.get_shortroomid(&room_id).await.is_ok() {
return Err(Error::BadRequest( return Err!(Request(RoomInUse("Room with that custom room ID already exists",)));
ErrorKind::RoomInUse,
"Room with that custom room ID already exists",
));
} }
if body.visibility == room::Visibility::Public if body.visibility == room::Visibility::Public
@ -92,19 +83,17 @@ pub(crate) async fn create_room_route(
&& !services.users.is_admin(sender_user).await && !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none() && body.appservice_info.is_none()
{ {
info!( warn!(
"Non-admin user {sender_user} tried to publish {0} to the room directory while \ "Non-admin user {sender_user} tried to publish {room_id} to the room directory \
\"lockdown_public_room_directory\" is enabled", while \"lockdown_public_room_directory\" is enabled"
&room_id
); );
if services.server.config.admin_room_notices { if services.server.config.admin_room_notices {
services services
.admin .admin
.send_text(&format!( .notice(&format!(
"Non-admin user {sender_user} tried to publish {0} to the room directory \ "Non-admin user {sender_user} tried to publish {room_id} to the room \
while \"lockdown_public_room_directory\" is enabled", directory while \"lockdown_public_room_directory\" is enabled"
&room_id
)) ))
.await; .await;
} }
@ -129,10 +118,9 @@ pub(crate) async fn create_room_route(
if services.server.supported_room_version(&room_version) { if services.server.supported_room_version(&room_version) {
room_version room_version
} else { } else {
return Err(Error::BadRequest( return Err!(Request(UnsupportedRoomVersion(
ErrorKind::UnsupportedRoomVersion, "This server does not support that room version."
"This server does not support that room version.", )));
));
}, },
| None => services.server.config.default_room_version.clone(), | None => services.server.config.default_room_version.clone(),
}; };
@ -144,16 +132,17 @@ pub(crate) async fn create_room_route(
let mut content = content let mut content = content
.deserialize_as::<CanonicalJsonObject>() .deserialize_as::<CanonicalJsonObject>()
.map_err(|e| { .map_err(|e| {
error!("Failed to deserialise content as canonical JSON: {}", e); err!(Request(BadJson(error!(
Error::bad_database("Failed to deserialise content as canonical JSON.") "Failed to deserialise content as canonical JSON: {e}"
))))
})?; })?;
match room_version { match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => { | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => {
content.insert( content.insert(
"creator".into(), "creator".into(),
json!(&sender_user).try_into().map_err(|e| { json!(&sender_user).try_into().map_err(|e| {
info!("Invalid creation content: {e}"); err!(Request(BadJson(debug_error!("Invalid creation content: {e}"))))
Error::BadRequest(ErrorKind::BadJson, "Invalid creation content")
})?, })?,
); );
}, },
@ -163,9 +152,9 @@ pub(crate) async fn create_room_route(
} }
content.insert( content.insert(
"room_version".into(), "room_version".into(),
json!(room_version.as_str()).try_into().map_err(|_| { json!(room_version.as_str())
Error::BadRequest(ErrorKind::BadJson, "Invalid creation content") .try_into()
})?, .map_err(|e| err!(Request(BadJson("Invalid creation content: {e}"))))?,
); );
content content
}, },
@ -174,21 +163,13 @@ pub(crate) async fn create_room_route(
let content = match room_version { let content = match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(sender_user.clone()), RoomCreateEventContent::new_v1(sender_user.to_owned()),
| _ => RoomCreateEventContent::new_v11(), | _ => RoomCreateEventContent::new_v11(),
}; };
let mut content = serde_json::from_str::<CanonicalJsonObject>( let mut content =
to_raw_value(&content) serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())
.expect("we just created this as content was None") .unwrap();
.get(), content.insert("room_version".into(), json!(room_version.as_str()).try_into()?);
)
.unwrap();
content.insert(
"room_version".into(),
json!(room_version.as_str())
.try_into()
.expect("we just created this as content was None"),
);
content content
}, },
}; };
@ -200,8 +181,7 @@ pub(crate) async fn create_room_route(
.build_and_append_pdu( .build_and_append_pdu(
PduBuilder { PduBuilder {
event_type: TimelineEventType::RoomCreate, event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content) content: to_raw_value(&create_content)?,
.expect("create event content serialization"),
state_key: Some(StateKey::new()), state_key: Some(StateKey::new()),
..Default::default() ..Default::default()
}, },
@ -239,7 +219,7 @@ pub(crate) async fn create_room_route(
| _ => RoomPreset::PrivateChat, // Room visibility should not be custom | _ => RoomPreset::PrivateChat, // Room visibility should not be custom
}); });
let mut users = BTreeMap::from_iter([(sender_user.clone(), int!(100))]); let mut users = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
if preset == RoomPreset::TrustedPrivateChat { if preset == RoomPreset::TrustedPrivateChat {
for invite in &body.invite { for invite in &body.invite {
@ -267,8 +247,7 @@ pub(crate) async fn create_room_route(
.build_and_append_pdu( .build_and_append_pdu(
PduBuilder { PduBuilder {
event_type: TimelineEventType::RoomPowerLevels, event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_content) content: to_raw_value(&power_levels_content)?,
.expect("serialized power_levels event content"),
state_key: Some(StateKey::new()), state_key: Some(StateKey::new()),
..Default::default() ..Default::default()
}, },
@ -357,8 +336,7 @@ pub(crate) async fn create_room_route(
// 6. Events listed in initial_state // 6. Events listed in initial_state
for event in &body.initial_state { for event in &body.initial_state {
let mut pdu_builder = event.deserialize_as::<PduBuilder>().map_err(|e| { let mut pdu_builder = event.deserialize_as::<PduBuilder>().map_err(|e| {
warn!("Invalid initial state event: {:?}", e); err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
Error::BadRequest(ErrorKind::InvalidParam, "Invalid initial state event.")
})?; })?;
debug_info!("Room creation initial state event: {event:?}"); debug_info!("Room creation initial state event: {event:?}");
@ -367,7 +345,7 @@ pub(crate) async fn create_room_route(
// state event in there with the content of literally `{}` (not null or empty // state event in there with the content of literally `{}` (not null or empty
// string), let's just skip it over and warn. // string), let's just skip it over and warn.
if pdu_builder.content.get().eq("{}") { if pdu_builder.content.get().eq("{}") {
info!("skipping empty initial state event with content of `{{}}`: {event:?}"); debug_warn!("skipping empty initial state event with content of `{{}}`: {event:?}");
debug_warn!("content: {}", pdu_builder.content.get()); debug_warn!("content: {}", pdu_builder.content.get());
continue; continue;
} }
@ -514,9 +492,7 @@ fn default_power_levels_content(
if let Some(power_level_content_override) = power_level_content_override { if let Some(power_level_content_override) = power_level_content_override {
let json: JsonObject = serde_json::from_str(power_level_content_override.json().get()) let json: JsonObject = serde_json::from_str(power_level_content_override.json().get())
.map_err(|_| { .map_err(|e| err!(Request(BadJson("Invalid power_level_content_override: {e:?}"))))?;
Error::BadRequest(ErrorKind::BadJson, "Invalid power_level_content_override.")
})?;
for (key, value) in json { for (key, value) in json {
power_levels_content[key] = value; power_levels_content[key] = value;
@ -534,16 +510,14 @@ async fn room_alias_check(
) -> Result<OwnedRoomAliasId> { ) -> Result<OwnedRoomAliasId> {
// Basic checks on the room alias validity // Basic checks on the room alias validity
if room_alias_name.contains(':') { if room_alias_name.contains(':') {
return Err(Error::BadRequest( return Err!(Request(InvalidParam(
ErrorKind::InvalidParam,
"Room alias contained `:` which is not allowed. Please note that this expects a \ "Room alias contained `:` which is not allowed. Please note that this expects a \
localpart, not the full room alias.", localpart, not the full room alias.",
)); )));
} else if room_alias_name.contains(char::is_whitespace) { } else if room_alias_name.contains(char::is_whitespace) {
return Err(Error::BadRequest( return Err!(Request(InvalidParam(
ErrorKind::InvalidParam,
"Room alias contained spaces which is not a valid room alias.", "Room alias contained spaces which is not a valid room alias.",
)); )));
} }
// check if room alias is forbidden // check if room alias is forbidden
@ -552,7 +526,7 @@ async fn room_alias_check(
.forbidden_alias_names() .forbidden_alias_names()
.is_match(room_alias_name) .is_match(room_alias_name)
{ {
return Err(Error::BadRequest(ErrorKind::Unknown, "Room alias name is forbidden.")); return Err!(Request(Unknown("Room alias name is forbidden.")));
} }
let server_name = services.globals.server_name(); let server_name = services.globals.server_name();
@ -572,25 +546,19 @@ async fn room_alias_check(
.await .await
.is_ok() .is_ok()
{ {
return Err(Error::BadRequest(ErrorKind::RoomInUse, "Room alias already exists.")); return Err!(Request(RoomInUse("Room alias already exists.")));
} }
if let Some(info) = appservice_info { if let Some(info) = appservice_info {
if !info.aliases.is_match(full_room_alias.as_str()) { if !info.aliases.is_match(full_room_alias.as_str()) {
return Err(Error::BadRequest( return Err!(Request(Exclusive("Room alias is not in namespace.")));
ErrorKind::Exclusive,
"Room alias is not in namespace.",
));
} }
} else if services } else if services
.appservice .appservice
.is_exclusive_alias(&full_room_alias) .is_exclusive_alias(&full_room_alias)
.await .await
{ {
return Err(Error::BadRequest( return Err!(Request(Exclusive("Room alias reserved by appservice.",)));
ErrorKind::Exclusive,
"Room alias reserved by appservice.",
));
} }
debug_info!("Full room alias: {full_room_alias}"); debug_info!("Full room alias: {full_room_alias}");
@ -606,24 +574,33 @@ fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<Own
.forbidden_alias_names() .forbidden_alias_names()
.is_match(custom_room_id) .is_match(custom_room_id)
{ {
return Err(Error::BadRequest(ErrorKind::Unknown, "Custom room ID is forbidden.")); return Err!(Request(Unknown("Custom room ID is forbidden.")));
}
if custom_room_id.contains(':') {
return Err!(Request(InvalidParam(
"Custom room ID contained `:` which is not allowed. Please note that this expects a \
localpart, not the full room ID.",
)));
} else if custom_room_id.contains(char::is_whitespace) {
return Err!(Request(InvalidParam(
"Custom room ID contained spaces which is not valid."
)));
} }
let server_name = services.globals.server_name(); let server_name = services.globals.server_name();
let mut room_id = custom_room_id.to_owned(); let mut room_id = custom_room_id.to_owned();
if custom_room_id.contains(':') { if custom_room_id.contains(':') {
if !custom_room_id.starts_with('!') { if !custom_room_id.starts_with('!') {
return Err(Error::BadRequest( return Err!(Request(InvalidParam(
ErrorKind::InvalidParam,
"Custom room ID contains an unexpected `:` which is not allowed.", "Custom room ID contains an unexpected `:` which is not allowed.",
)); )));
} }
} else if custom_room_id.starts_with('!') { } else if custom_room_id.starts_with('!') {
return Err(Error::BadRequest( return Err!(Request(InvalidParam(
ErrorKind::InvalidParam,
"Room ID is prefixed with !, but is not fully qualified. You likely did not want \ "Room ID is prefixed with !, but is not fully qualified. You likely did not want \
this.", this.",
)); )));
} else { } else {
room_id = format!("!{custom_room_id}:{server_name}"); room_id = format!("!{custom_room_id}:{server_name}");
} }
@ -635,10 +612,7 @@ fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<Own
.expect("failed to extract server name from room ID") .expect("failed to extract server name from room ID")
!= server_name != server_name
{ {
Err(Error::BadRequest( Err!(Request(InvalidParam("Custom room ID must be on this server.",)))
ErrorKind::InvalidParam,
"Custom room ID must be on this server.",
))
} else { } else {
Ok(full_room_id) Ok(full_room_id)
} }

View file

@ -40,5 +40,5 @@ pub(crate) async fn get_room_event_route(
event.add_age().ok(); event.add_age().ok();
Ok(get_room_event::v3::Response { event: event.into_room_event() }) Ok(get_room_event::v3::Response { event: event.into_format() })
} }

View file

@ -1,9 +1,9 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, PduEvent, Result, at, Err, Event, Result, at,
utils::{BoolExt, stream::TryTools}, utils::{BoolExt, stream::TryTools},
}; };
use futures::TryStreamExt; use futures::{FutureExt, TryStreamExt, future::try_join4};
use ruma::api::client::room::initial_sync::v3::{PaginationChunk, Request, Response}; use ruma::api::client::room::initial_sync::v3::{PaginationChunk, Request, Response};
use crate::Ruma; use crate::Ruma;
@ -25,22 +25,33 @@ pub(crate) async fn room_initial_sync_route(
return Err!(Request(Forbidden("No room preview available."))); return Err!(Request(Forbidden("No room preview available.")));
} }
let membership = services
.rooms
.state_cache
.user_membership(body.sender_user(), room_id)
.map(Ok);
let visibility = services.rooms.directory.visibility(room_id).map(Ok);
let state = services
.rooms
.state_accessor
.room_state_full_pdus(room_id)
.map_ok(Event::into_format)
.try_collect::<Vec<_>>();
let limit = LIMIT_MAX; let limit = LIMIT_MAX;
let events: Vec<_> = services let events = services
.rooms .rooms
.timeline .timeline
.pdus_rev(None, room_id, None) .pdus_rev(None, room_id, None)
.try_take(limit) .try_take(limit)
.try_collect() .try_collect::<Vec<_>>();
.await?;
let state: Vec<_> = services let (membership, visibility, state, events) =
.rooms try_join4(membership, visibility, state, events)
.state_accessor .boxed()
.room_state_full_pdus(room_id) .await?;
.map_ok(PduEvent::into_state_event)
.try_collect()
.await?;
let messages = PaginationChunk { let messages = PaginationChunk {
start: events.last().map(at!(0)).as_ref().map(ToString::to_string), start: events.last().map(at!(0)).as_ref().map(ToString::to_string),
@ -55,7 +66,7 @@ pub(crate) async fn room_initial_sync_route(
chunk: events chunk: events
.into_iter() .into_iter()
.map(at!(1)) .map(at!(1))
.map(PduEvent::into_room_event) .map(Event::into_format)
.collect(), .collect(),
}; };
@ -64,11 +75,7 @@ pub(crate) async fn room_initial_sync_route(
account_data: None, account_data: None,
state: state.into(), state: state.into(),
messages: messages.chunk.is_empty().or_some(messages), messages: messages.chunk.is_empty().or_some(messages),
visibility: services.rooms.directory.visibility(room_id).await.into(), visibility: visibility.into(),
membership: services membership,
.rooms
.state_cache
.user_membership(body.sender_user(), room_id)
.await,
}) })
} }

View file

@ -43,10 +43,9 @@ pub(crate) async fn get_room_summary_legacy(
} }
/// # `GET /_matrix/client/unstable/im.nheko.summary/summary/{roomIdOrAlias}` /// # `GET /_matrix/client/unstable/im.nheko.summary/summary/{roomIdOrAlias}`
/// # `GET /_matrix/client/v1/room_summary/{roomIdOrAlias}`
/// ///
/// Returns a short description of the state of a room. /// Returns a short description of the state of a room.
///
/// An implementation of [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)
#[tracing::instrument(skip_all, fields(%client), name = "room_summary")] #[tracing::instrument(skip_all, fields(%client), name = "room_summary")]
pub(crate) async fn get_room_summary( pub(crate) async fn get_room_summary(
State(services): State<crate::State>, State(services): State<crate::State>,
@ -113,13 +112,15 @@ async fn local_room_summary_response(
) -> Result<get_summary::msc3266::Response> { ) -> Result<get_summary::msc3266::Response> {
trace!(?sender_user, "Sending local room summary response for {room_id:?}"); trace!(?sender_user, "Sending local room summary response for {room_id:?}");
let join_rule = services.rooms.state_accessor.get_join_rules(room_id); let join_rule = services.rooms.state_accessor.get_join_rules(room_id);
let world_readable = services.rooms.state_accessor.is_world_readable(room_id); let world_readable = services.rooms.state_accessor.is_world_readable(room_id);
let guest_can_join = services.rooms.state_accessor.guest_can_join(room_id); let guest_can_join = services.rooms.state_accessor.guest_can_join(room_id);
let (join_rule, world_readable, guest_can_join) = let (join_rule, world_readable, guest_can_join) =
join3(join_rule, world_readable, guest_can_join).await; join3(join_rule, world_readable, guest_can_join).await;
trace!("{join_rule:?}, {world_readable:?}, {guest_can_join:?}");
trace!("{join_rule:?}, {world_readable:?}, {guest_can_join:?}");
user_can_see_summary( user_can_see_summary(
services, services,
room_id, room_id,

View file

@ -2,7 +2,7 @@ use std::cmp::max;
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Error, Result, err, info, Err, Error, Event, Result, err, info,
matrix::{StateKey, pdu::PduBuilder}, matrix::{StateKey, pdu::PduBuilder},
}; };
use futures::StreamExt; use futures::StreamExt;
@ -215,7 +215,7 @@ pub(crate) async fn upgrade_room_route(
.room_state_get(&body.room_id, event_type, "") .room_state_get(&body.room_id, event_type, "")
.await .await
{ {
| Ok(v) => v.content.clone(), | Ok(v) => v.content().to_owned(),
| Err(_) => continue, // Skipping missing events. | Err(_) => continue, // Skipping missing events.
}; };

View file

@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Result, at, is_true, Err, Result, at, is_true,
matrix::pdu::PduEvent, matrix::Event,
result::FlatOk, result::FlatOk,
utils::{IterStream, stream::ReadyExt}, utils::{IterStream, stream::ReadyExt},
}; };
@ -144,7 +144,7 @@ async fn category_room_events(
.map(at!(2)) .map(at!(2))
.flatten() .flatten()
.stream() .stream()
.map(PduEvent::into_room_event) .map(Event::into_format)
.map(|result| SearchResult { .map(|result| SearchResult {
rank: None, rank: None,
result: Some(result), result: Some(result),
@ -185,7 +185,7 @@ async fn procure_room_state(services: &Services, room_id: &RoomId) -> Result<Roo
.rooms .rooms
.state_accessor .state_accessor
.room_state_full_pdus(room_id) .room_state_full_pdus(room_id)
.map_ok(PduEvent::into_state_event) .map_ok(Event::into_format)
.try_collect() .try_collect()
.await?; .await?;

View file

@ -269,11 +269,9 @@ pub(crate) async fn login_token_route(
return Err!(Request(Forbidden("Login via an existing session is not enabled"))); return Err!(Request(Forbidden("Login via an existing session is not enabled")));
} }
let sender_user = body.sender_user();
let sender_device = body.sender_device();
// This route SHOULD have UIA // This route SHOULD have UIA
// TODO: How do we make only UIA sessions that have not been used before valid? // TODO: How do we make only UIA sessions that have not been used before valid?
let (sender_user, sender_device) = body.sender();
let mut uiaainfo = uiaa::UiaaInfo { let mut uiaainfo = uiaa::UiaaInfo {
flows: vec![uiaa::AuthFlow { stages: vec![uiaa::AuthType::Password] }], flows: vec![uiaa::AuthFlow { stages: vec![uiaa::AuthType::Password] }],
@ -335,12 +333,9 @@ pub(crate) async fn logout_route(
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
body: Ruma<logout::v3::Request>, body: Ruma<logout::v3::Request>,
) -> Result<logout::v3::Response> { ) -> Result<logout::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");
services services
.users .users
.remove_device(sender_user, sender_device) .remove_device(body.sender_user(), body.sender_device())
.await; .await;
Ok(logout::v3::Response::new()) Ok(logout::v3::Response::new())
@ -365,12 +360,10 @@ pub(crate) async fn logout_all_route(
InsecureClientIp(client): InsecureClientIp, InsecureClientIp(client): InsecureClientIp,
body: Ruma<logout_all::v3::Request>, body: Ruma<logout_all::v3::Request>,
) -> Result<logout_all::v3::Response> { ) -> Result<logout_all::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
services services
.users .users
.all_device_ids(sender_user) .all_device_ids(body.sender_user())
.for_each(|device_id| services.users.remove_device(sender_user, device_id)) .for_each(|device_id| services.users.remove_device(body.sender_user(), device_id))
.await; .await;
Ok(logout_all::v3::Response::new()) Ok(logout_all::v3::Response::new())

View file

@ -1,11 +1,11 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Result, err, Err, Result, err,
matrix::pdu::{PduBuilder, PduEvent}, matrix::{Event, pdu::PduBuilder},
utils::BoolExt, utils::BoolExt,
}; };
use conduwuit_service::Services; use conduwuit_service::Services;
use futures::TryStreamExt; use futures::{FutureExt, TryStreamExt};
use ruma::{ use ruma::{
OwnedEventId, RoomId, UserId, OwnedEventId, RoomId, UserId,
api::client::state::{get_state_events, get_state_events_for_key, send_state_event}, api::client::state::{get_state_events, get_state_events_for_key, send_state_event},
@ -21,6 +21,7 @@ use ruma::{
}, },
serde::Raw, serde::Raw,
}; };
use serde_json::json;
use crate::{Ruma, RumaResponse}; use crate::{Ruma, RumaResponse};
@ -63,6 +64,7 @@ pub(crate) async fn send_state_event_for_empty_key_route(
body: Ruma<send_state_event::v3::Request>, body: Ruma<send_state_event::v3::Request>,
) -> Result<RumaResponse<send_state_event::v3::Response>> { ) -> Result<RumaResponse<send_state_event::v3::Response>> {
send_state_event_for_key_route(State(services), body) send_state_event_for_key_route(State(services), body)
.boxed()
.await .await
.map(RumaResponse) .map(RumaResponse)
} }
@ -77,7 +79,7 @@ pub(crate) async fn get_state_events_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_state_events::v3::Request>, body: Ruma<get_state_events::v3::Request>,
) -> Result<get_state_events::v3::Response> { ) -> Result<get_state_events::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if !services if !services
.rooms .rooms
@ -93,7 +95,7 @@ pub(crate) async fn get_state_events_route(
.rooms .rooms
.state_accessor .state_accessor
.room_state_full_pdus(&body.room_id) .room_state_full_pdus(&body.room_id)
.map_ok(PduEvent::into_state_event) .map_ok(Event::into_format)
.try_collect() .try_collect()
.await?, .await?,
}) })
@ -144,7 +146,18 @@ pub(crate) async fn get_state_events_for_key_route(
Ok(get_state_events_for_key::v3::Response { Ok(get_state_events_for_key::v3::Response {
content: event_format.or(|| event.get_content_as_value()), content: event_format.or(|| event.get_content_as_value()),
event: event_format.then(|| event.into_state_event_value()), event: event_format.then(|| {
json!({
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
"unsigned": event.unsigned(),
})
}),
}) })
} }

View file

@ -473,9 +473,7 @@ async fn handle_left_room(
prev_batch: Some(next_batch.to_string()), prev_batch: Some(next_batch.to_string()),
events: Vec::new(), events: Vec::new(),
}, },
state: RoomState { state: RoomState { events: vec![event.into_format()] },
events: vec![event.into_sync_state_event()],
},
})); }));
} }
@ -559,7 +557,7 @@ async fn handle_left_room(
continue; continue;
} }
left_state_events.push(pdu.into_sync_state_event()); left_state_events.push(pdu.into_format());
} }
} }
@ -645,7 +643,7 @@ async fn load_joined_room(
let lazy_loading_context = &lazy_loading::Context { let lazy_loading_context = &lazy_loading::Context {
user_id: sender_user, user_id: sender_user,
device_id: sender_device, device_id: Some(sender_device),
room_id, room_id,
token: Some(since), token: Some(since),
options: Some(&filter.room.state.lazy_load_options), options: Some(&filter.room.state.lazy_load_options),
@ -755,7 +753,7 @@ async fn load_joined_room(
.wide_filter_map(|item| ignored_filter(services, item, sender_user)) .wide_filter_map(|item| ignored_filter(services, item, sender_user))
.map(at!(1)) .map(at!(1))
.chain(joined_sender_member.into_iter().stream()) .chain(joined_sender_member.into_iter().stream())
.map(|pdu| pdu.to_sync_room_event()) .map(Event::into_format)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let account_data_events = services let account_data_events = services
@ -877,10 +875,7 @@ async fn load_joined_room(
events: room_events, events: room_events,
}, },
state: RoomState { state: RoomState {
events: state_events events: state_events.into_iter().map(Event::into_format).collect(),
.into_iter()
.map(PduEvent::into_sync_state_event)
.collect(),
}, },
ephemeral: Ephemeral { events: edus }, ephemeral: Ephemeral { events: edus },
unread_thread_notifications: BTreeMap::new(), unread_thread_notifications: BTreeMap::new(),

View file

@ -6,7 +6,7 @@ use std::{
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Error, PduCount, PduEvent, Result, debug, error, extract_variant, Err, Error, Event, PduCount, Result, at, debug, error, extract_variant,
matrix::TypeStateKey, matrix::TypeStateKey,
utils::{ utils::{
BoolExt, IterStream, ReadyExt, TryFutureExtExt, BoolExt, IterStream, ReadyExt, TryFutureExtExt,
@ -604,7 +604,8 @@ pub(crate) async fn sync_events_v4_route(
.iter() .iter()
.stream() .stream()
.filter_map(|item| ignored_filter(&services, item.clone(), sender_user)) .filter_map(|item| ignored_filter(&services, item.clone(), sender_user))
.map(|(_, pdu)| pdu.to_sync_room_event()) .map(at!(1))
.map(Event::into_format)
.collect() .collect()
.await; .await;
@ -626,7 +627,7 @@ pub(crate) async fn sync_events_v4_route(
.state_accessor .state_accessor
.room_state_get(room_id, &state.0, &state.1) .room_state_get(room_id, &state.0, &state.1)
.await .await
.map(PduEvent::into_sync_state_event) .map(Event::into_format)
.ok() .ok()
}) })
.collect() .collect()

View file

@ -7,11 +7,8 @@ use std::{
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Error, Result, error, extract_variant, is_equal_to, Err, Error, Result, at, error, extract_variant, is_equal_to,
matrix::{ matrix::{Event, TypeStateKey, pdu::PduCount},
TypeStateKey,
pdu::{PduCount, PduEvent},
},
trace, trace,
utils::{ utils::{
BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt, BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt,
@ -515,7 +512,8 @@ where
.iter() .iter()
.stream() .stream()
.filter_map(|item| ignored_filter(services, item.clone(), sender_user)) .filter_map(|item| ignored_filter(services, item.clone(), sender_user))
.map(|(_, pdu)| pdu.to_sync_room_event()) .map(at!(1))
.map(Event::into_format)
.collect() .collect()
.await; .await;
@ -537,7 +535,7 @@ where
.state_accessor .state_accessor
.room_state_get(room_id, &state.0, &state.1) .room_state_get(room_id, &state.0, &state.1)
.await .await
.map(PduEvent::into_sync_state_event) .map(Event::into_format)
.ok() .ok()
}) })
.collect() .collect()

View file

@ -21,7 +21,7 @@ pub(crate) async fn update_tag_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<create_tag::v3::Request>, body: Ruma<create_tag::v3::Request>,
) -> Result<create_tag::v3::Response> { ) -> Result<create_tag::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let mut tags_event = services let mut tags_event = services
.account_data .account_data
@ -42,7 +42,7 @@ pub(crate) async fn update_tag_route(
Some(&body.room_id), Some(&body.room_id),
sender_user, sender_user,
RoomAccountDataEventType::Tag, RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"), &serde_json::to_value(tags_event)?,
) )
.await?; .await?;
@ -58,7 +58,7 @@ pub(crate) async fn delete_tag_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_tag::v3::Request>, body: Ruma<delete_tag::v3::Request>,
) -> Result<delete_tag::v3::Response> { ) -> Result<delete_tag::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let mut tags_event = services let mut tags_event = services
.account_data .account_data
@ -76,7 +76,7 @@ pub(crate) async fn delete_tag_route(
Some(&body.room_id), Some(&body.room_id),
sender_user, sender_user,
RoomAccountDataEventType::Tag, RoomAccountDataEventType::Tag,
&serde_json::to_value(tags_event).expect("to json value always works"), &serde_json::to_value(tags_event)?,
) )
.await?; .await?;
@ -92,7 +92,7 @@ pub(crate) async fn get_tags_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_tags::v3::Request>, body: Ruma<get_tags::v3::Request>,
) -> Result<get_tags::v3::Response> { ) -> Result<get_tags::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let tags_event = services let tags_event = services
.account_data .account_data

View file

@ -1,7 +1,10 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Result, at, Result, at,
matrix::pdu::{PduCount, PduEvent}, matrix::{
Event,
pdu::{PduCount, PduEvent},
},
}; };
use futures::StreamExt; use futures::StreamExt;
use ruma::{api::client::threads::get_threads, uint}; use ruma::{api::client::threads::get_threads, uint};
@ -56,7 +59,7 @@ pub(crate) async fn get_threads_route(
chunk: threads chunk: threads
.into_iter() .into_iter()
.map(at!(1)) .map(at!(1))
.map(PduEvent::into_room_event) .map(Event::into_format)
.collect(), .collect(),
}) })
} }

View file

@ -21,7 +21,7 @@ pub(crate) async fn send_event_to_device_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<send_event_to_device::v3::Request>, body: Ruma<send_event_to_device::v3::Request>,
) -> Result<send_event_to_device::v3::Response> { ) -> Result<send_event_to_device::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
let sender_device = body.sender_device.as_deref(); let sender_device = body.sender_device.as_deref();
// Check if this is a new transaction id // Check if this is a new transaction id
@ -47,7 +47,7 @@ pub(crate) async fn send_event_to_device_route(
serde_json::to_writer( serde_json::to_writer(
&mut buf, &mut buf,
&federation::transactions::edu::Edu::DirectToDevice(DirectDeviceContent { &federation::transactions::edu::Edu::DirectToDevice(DirectDeviceContent {
sender: sender_user.clone(), sender: sender_user.to_owned(),
ev_type: body.event_type.clone(), ev_type: body.event_type.clone(),
message_id: count.to_string().into(), message_id: count.to_string().into(),
messages, messages,

View file

@ -69,7 +69,7 @@ pub(crate) async fn delete_timezone_key_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_timezone_key::unstable::Request>, body: Ruma<delete_timezone_key::unstable::Request>,
) -> Result<delete_timezone_key::unstable::Response> { ) -> Result<delete_timezone_key::unstable::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if *sender_user != body.user_id && body.appservice_info.is_none() { if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user"))); return Err!(Request(Forbidden("You cannot update the profile of another user")));
@ -97,7 +97,7 @@ pub(crate) async fn set_timezone_key_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_timezone_key::unstable::Request>, body: Ruma<set_timezone_key::unstable::Request>,
) -> Result<set_timezone_key::unstable::Response> { ) -> Result<set_timezone_key::unstable::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if *sender_user != body.user_id && body.appservice_info.is_none() { if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user"))); return Err!(Request(Forbidden("You cannot update the profile of another user")));
@ -125,7 +125,7 @@ pub(crate) async fn set_profile_key_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_profile_key::unstable::Request>, body: Ruma<set_profile_key::unstable::Request>,
) -> Result<set_profile_key::unstable::Response> { ) -> Result<set_profile_key::unstable::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if *sender_user != body.user_id && body.appservice_info.is_none() { if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user"))); return Err!(Request(Forbidden("You cannot update the profile of another user")));
@ -218,7 +218,7 @@ pub(crate) async fn delete_profile_key_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_profile_key::unstable::Request>, body: Ruma<delete_profile_key::unstable::Request>,
) -> Result<delete_profile_key::unstable::Response> { ) -> Result<delete_profile_key::unstable::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user();
if *sender_user != body.user_id && body.appservice_info.is_none() { if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user"))); return Err!(Request(Forbidden("You cannot update the profile of another user")));

View file

@ -37,7 +37,11 @@ pub(crate) async fn get_supported_versions_route(
"v1.3".to_owned(), "v1.3".to_owned(),
"v1.4".to_owned(), "v1.4".to_owned(),
"v1.5".to_owned(), "v1.5".to_owned(),
"v1.8".to_owned(),
"v1.11".to_owned(), "v1.11".to_owned(),
"v1.12".to_owned(),
"v1.13".to_owned(),
"v1.14".to_owned(),
], ],
unstable_features: BTreeMap::from_iter([ unstable_features: BTreeMap::from_iter([
("org.matrix.e2e_cross_signing".to_owned(), true), ("org.matrix.e2e_cross_signing".to_owned(), true),

View file

@ -1,10 +1,7 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Result, Result,
utils::{ utils::{future::BoolExt, stream::BroadbandExt},
future::BoolExt,
stream::{BroadbandExt, ReadyExt},
},
}; };
use futures::{FutureExt, StreamExt, pin_mut}; use futures::{FutureExt, StreamExt, pin_mut};
use ruma::{ use ruma::{
@ -37,17 +34,18 @@ pub(crate) async fn search_users_route(
let mut users = services let mut users = services
.users .users
.stream() .stream()
.ready_filter(|user_id| user_id.as_str().to_lowercase().contains(&search_term))
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
.broad_filter_map(async |user_id| { .broad_filter_map(async |user_id| {
let display_name = services.users.displayname(&user_id).await.ok(); let display_name = services.users.displayname(&user_id).await.ok();
let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term);
let display_name_matches = display_name let display_name_matches = display_name
.as_deref() .as_deref()
.map(str::to_lowercase) .map(str::to_lowercase)
.is_some_and(|display_name| display_name.contains(&search_term)); .is_some_and(|display_name| display_name.contains(&search_term));
if !display_name_matches { if !user_id_matches && !display_name_matches {
return None; return None;
} }

View file

@ -94,6 +94,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::redact_event_route) .ruma_route(&client::redact_event_route)
.ruma_route(&client::report_event_route) .ruma_route(&client::report_event_route)
.ruma_route(&client::report_room_route) .ruma_route(&client::report_room_route)
.ruma_route(&client::report_user_route)
.ruma_route(&client::create_alias_route) .ruma_route(&client::create_alias_route)
.ruma_route(&client::delete_alias_route) .ruma_route(&client::delete_alias_route)
.ruma_route(&client::get_alias_route) .ruma_route(&client::get_alias_route)

View file

@ -2,7 +2,10 @@ use axum::extract::State;
use axum_client_ip::InsecureClientIp; use axum_client_ip::InsecureClientIp;
use base64::{Engine as _, engine::general_purpose}; use base64::{Engine as _, engine::general_purpose};
use conduwuit::{ use conduwuit::{
Err, Error, PduEvent, Result, err, pdu::gen_event_id, utils, utils::hash::sha256, warn, Err, Error, PduEvent, Result, err,
matrix::{Event, event::gen_event_id},
utils::{self, hash::sha256},
warn,
}; };
use ruma::{ use ruma::{
CanonicalJsonValue, OwnedUserId, UserId, CanonicalJsonValue, OwnedUserId, UserId,
@ -56,7 +59,7 @@ pub(crate) async fn create_invite_route(
} }
let mut signed_event = utils::to_canonical_object(&body.event) let mut signed_event = utils::to_canonical_object(&body.event)
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invite event is invalid."))?; .map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?;
let invited_user: OwnedUserId = signed_event let invited_user: OwnedUserId = signed_event
.get("state_key") .get("state_key")
@ -111,7 +114,7 @@ pub(crate) async fn create_invite_route(
let pdu: PduEvent = serde_json::from_value(event.into()) let pdu: PduEvent = serde_json::from_value(event.into())
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?; .map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
invite_state.push(pdu.to_stripped_state_event()); invite_state.push(pdu.to_format());
// If we are active in the room, the remote server will notify us about the // If we are active in the room, the remote server will notify us about the
// join/invite through /send. If we are not in the room, we need to manually // join/invite through /send. If we are not in the room, we need to manually
@ -144,7 +147,7 @@ pub(crate) async fn create_invite_route(
.send_appservice_request( .send_appservice_request(
appservice.registration.clone(), appservice.registration.clone(),
ruma::api::appservice::event::push_events::v1::Request { ruma::api::appservice::event::push_events::v1::Request {
events: vec![pdu.to_room_event()], events: vec![pdu.to_format()],
txn_id: general_purpose::URL_SAFE_NO_PAD txn_id: general_purpose::URL_SAFE_NO_PAD
.encode(sha256::hash(pdu.event_id.as_bytes())) .encode(sha256::hash(pdu.event_id.as_bytes()))
.into(), .into(),

View file

@ -5,7 +5,7 @@ use std::borrow::Borrow;
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Result, at, err, Err, Result, at, err,
pdu::gen_event_id_canonical_json, matrix::event::gen_event_id_canonical_json,
utils::stream::{IterStream, TryBroadbandExt}, utils::stream::{IterStream, TryBroadbandExt},
warn, warn,
}; };

View file

@ -1,7 +1,7 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Err, Result, err, Err, Result, err,
matrix::pdu::{PduEvent, gen_event_id_canonical_json}, matrix::{event::gen_event_id_canonical_json, pdu::PduEvent},
warn, warn,
}; };
use futures::FutureExt; use futures::FutureExt;

View file

@ -1,7 +1,7 @@
#![allow(deprecated)] #![allow(deprecated)]
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Result, err, matrix::pdu::gen_event_id_canonical_json}; use conduwuit::{Err, Result, err, matrix::event::gen_event_id_canonical_json};
use conduwuit_service::Services; use conduwuit_service::Services;
use futures::FutureExt; use futures::FutureExt;
use ruma::{ use ruma::{

View file

@ -513,6 +513,22 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub allow_registration: bool, pub allow_registration: bool,
/// If registration is enabled, and this setting is true, new users
/// registered after the first admin user will be automatically suspended
/// and will require an admin to run `!admin users unsuspend <user_id>`.
///
/// Suspended users are still able to read messages, make profile updates,
/// leave rooms, and deactivate their account, however cannot send messages,
/// invites, or create/join or otherwise modify rooms.
/// They are effectively read-only.
///
/// If you want to use this to screen people who register on your server,
/// you should add a room to `auto_join_rooms` that is public, and contains
/// information that new users can read (since they won't be able to DM
/// anyone, or send a message, and may be confused).
#[serde(default)]
pub suspend_on_register: bool,
/// Enabling this setting opens registration to anyone without restrictions. /// Enabling this setting opens registration to anyone without restrictions.
/// This makes your server vulnerable to abuse /// This makes your server vulnerable to abuse
#[serde(default)] #[serde(default)]
@ -1155,7 +1171,7 @@ pub struct Config {
/// 3 to 5 = Statistics with possible performance impact. /// 3 to 5 = Statistics with possible performance impact.
/// 6 = All statistics. /// 6 = All statistics.
/// ///
/// default: 1 /// default: 3
#[serde(default = "default_rocksdb_stats_level")] #[serde(default = "default_rocksdb_stats_level")]
pub rocksdb_stats_level: u8, pub rocksdb_stats_level: u8,
@ -1823,12 +1839,10 @@ pub struct Config {
pub stream_amplification: usize, pub stream_amplification: usize,
/// Number of sender task workers; determines sender parallelism. Default is /// Number of sender task workers; determines sender parallelism. Default is
/// '0' which means the value is determined internally, likely matching the /// core count. Override by setting a different value.
/// number of tokio worker-threads or number of cores, etc. Override by
/// setting a non-zero value.
/// ///
/// default: 0 /// default: core count
#[serde(default)] #[serde(default = "default_sender_workers")]
pub sender_workers: usize, pub sender_workers: usize,
/// Enables listener sockets; can be set to false to disable listening. This /// Enables listener sockets; can be set to false to disable listening. This
@ -2059,45 +2073,48 @@ fn default_database_backups_to_keep() -> i16 { 1 }
fn default_db_write_buffer_capacity_mb() -> f64 { 48.0 + parallelism_scaled_f64(4.0) } fn default_db_write_buffer_capacity_mb() -> f64 { 48.0 + parallelism_scaled_f64(4.0) }
fn default_db_cache_capacity_mb() -> f64 { 128.0 + parallelism_scaled_f64(64.0) } fn default_db_cache_capacity_mb() -> f64 { 512.0 + parallelism_scaled_f64(512.0) }
fn default_pdu_cache_capacity() -> u32 { parallelism_scaled_u32(10_000).saturating_add(100_000) } fn default_pdu_cache_capacity() -> u32 { parallelism_scaled_u32(50_000).saturating_add(100_000) }
fn default_cache_capacity_modifier() -> f64 { 1.0 } fn default_cache_capacity_modifier() -> f64 { 1.0 }
fn default_auth_chain_cache_capacity() -> u32 { fn default_auth_chain_cache_capacity() -> u32 {
parallelism_scaled_u32(10_000).saturating_add(100_000)
}
fn default_shorteventid_cache_capacity() -> u32 {
parallelism_scaled_u32(50_000).saturating_add(100_000) parallelism_scaled_u32(50_000).saturating_add(100_000)
} }
fn default_shorteventid_cache_capacity() -> u32 {
parallelism_scaled_u32(100_000).saturating_add(100_000)
}
fn default_eventidshort_cache_capacity() -> u32 { fn default_eventidshort_cache_capacity() -> u32 {
parallelism_scaled_u32(25_000).saturating_add(100_000) parallelism_scaled_u32(50_000).saturating_add(100_000)
} }
fn default_eventid_pdu_cache_capacity() -> u32 { fn default_eventid_pdu_cache_capacity() -> u32 {
parallelism_scaled_u32(25_000).saturating_add(100_000) parallelism_scaled_u32(50_000).saturating_add(100_000)
} }
fn default_shortstatekey_cache_capacity() -> u32 { fn default_shortstatekey_cache_capacity() -> u32 {
parallelism_scaled_u32(10_000).saturating_add(100_000) parallelism_scaled_u32(50_000).saturating_add(100_000)
} }
fn default_statekeyshort_cache_capacity() -> u32 { fn default_statekeyshort_cache_capacity() -> u32 {
parallelism_scaled_u32(10_000).saturating_add(100_000) parallelism_scaled_u32(50_000).saturating_add(100_000)
} }
fn default_servernameevent_data_cache_capacity() -> u32 { fn default_servernameevent_data_cache_capacity() -> u32 {
parallelism_scaled_u32(100_000).saturating_add(500_000) parallelism_scaled_u32(100_000).saturating_add(100_000)
} }
fn default_stateinfo_cache_capacity() -> u32 { parallelism_scaled_u32(100) } fn default_stateinfo_cache_capacity() -> u32 {
parallelism_scaled_u32(500).clamp(100, 12000)
}
fn default_roomid_spacehierarchy_cache_capacity() -> u32 { parallelism_scaled_u32(1000) } fn default_roomid_spacehierarchy_cache_capacity() -> u32 {
parallelism_scaled_u32(500).clamp(100, 12000) }
fn default_dns_cache_entries() -> u32 { 32768 } fn default_dns_cache_entries() -> u32 { 327680 }
fn default_dns_min_ttl() -> u64 { 60 * 180 } fn default_dns_min_ttl() -> u64 { 60 * 180 }
@ -2199,7 +2216,7 @@ fn default_typing_client_timeout_max_s() -> u64 { 45 }
fn default_rocksdb_recovery_mode() -> u8 { 1 } fn default_rocksdb_recovery_mode() -> u8 { 1 }
fn default_rocksdb_log_level() -> String { "error".to_owned() } fn default_rocksdb_log_level() -> String { "info".to_owned() }
fn default_rocksdb_log_time_to_roll() -> usize { 0 } fn default_rocksdb_log_time_to_roll() -> usize { 0 }
@ -2231,7 +2248,7 @@ fn default_rocksdb_compression_level() -> i32 { 32767 }
#[allow(clippy::doc_markdown)] #[allow(clippy::doc_markdown)]
fn default_rocksdb_bottommost_compression_level() -> i32 { 32767 } fn default_rocksdb_bottommost_compression_level() -> i32 { 32767 }
fn default_rocksdb_stats_level() -> u8 { 1 } fn default_rocksdb_stats_level() -> u8 { 3 }
// I know, it's a great name // I know, it's a great name
#[must_use] #[must_use]
@ -2286,14 +2303,13 @@ fn default_admin_log_capture() -> String {
fn default_admin_room_tag() -> String { "m.server_notice".to_owned() } fn default_admin_room_tag() -> String { "m.server_notice".to_owned() }
#[allow(clippy::as_conversions, clippy::cast_precision_loss)] #[allow(clippy::as_conversions, clippy::cast_precision_loss)]
fn parallelism_scaled_f64(val: f64) -> f64 { val * (sys::available_parallelism() as f64) } pub fn parallelism_scaled_f64(val: f64) -> f64 { val * (sys::available_parallelism() as f64) }
fn parallelism_scaled_u32(val: u32) -> u32 { pub fn parallelism_scaled_u32(val: u32) -> u32 { val.saturating_mul(sys::available_parallelism() as u32) }
let val = val.try_into().expect("failed to cast u32 to usize");
parallelism_scaled(val).try_into().unwrap_or(u32::MAX)
}
fn parallelism_scaled(val: usize) -> usize { val.saturating_mul(sys::available_parallelism()) } pub fn parallelism_scaled_i32(val: i32) -> i32 { val.saturating_mul(sys::available_parallelism() as i32) }
pub fn parallelism_scaled(val: usize) -> usize { val.saturating_mul(sys::available_parallelism()) }
fn default_trusted_server_batch_size() -> usize { 256 } fn default_trusted_server_batch_size() -> usize { 256 }
@ -2313,6 +2329,8 @@ fn default_stream_width_scale() -> f32 { 1.0 }
fn default_stream_amplification() -> usize { 1024 } fn default_stream_amplification() -> usize { 1024 }
fn default_sender_workers() -> usize { parallelism_scaled(1) }
fn default_client_receive_timeout() -> u64 { 75 } fn default_client_receive_timeout() -> u64 { 75 }
fn default_client_request_timeout() -> u64 { 180 } fn default_client_request_timeout() -> u64 { 180 }

View file

@ -88,10 +88,7 @@ impl PartialProxyConfig {
} }
} }
match (included_because, excluded_because) { match (included_because, excluded_because) {
| (Some(a), Some(b)) if a.more_specific_than(b) => Some(&self.url), /* included for | (Some(a), Some(b)) if a.more_specific_than(b) => Some(&self.url),
* a more specific
* reason */
// than excluded
| (Some(_), None) => Some(&self.url), | (Some(_), None) => Some(&self.url),
| _ => None, | _ => None,
} }

View file

@ -84,10 +84,12 @@ fn append_features(features: &mut Vec<String>, manifest: &str) -> Result<()> {
fn init_dependencies() -> Result<DepsSet> { fn init_dependencies() -> Result<DepsSet> {
let manifest = Manifest::from_str(WORKSPACE_MANIFEST)?; let manifest = Manifest::from_str(WORKSPACE_MANIFEST)?;
Ok(manifest let deps_set = manifest
.workspace .workspace
.as_ref() .as_ref()
.expect("manifest has workspace section") .expect("manifest has workspace section")
.dependencies .dependencies
.clone()) .clone();
Ok(deps_set)
} }

View file

@ -1,63 +1,188 @@
use ruma::{EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId, events::TimelineEventType}; mod content;
use serde_json::value::RawValue as RawJsonValue; mod filter;
mod format;
mod id;
mod redact;
mod relation;
mod type_ext;
mod unsigned;
use std::fmt::Debug;
use ruma::{
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId,
RoomVersionId, UserId, events::TimelineEventType,
};
use serde::Deserialize;
use serde_json::{Value as JsonValue, value::RawValue as RawJsonValue};
pub use self::{filter::Matches, id::*, relation::RelationTypeEqual, type_ext::TypeExt};
use super::{pdu::Pdu, state_key::StateKey};
use crate::{Result, utils};
/// Abstraction of a PDU so users can have their own PDU types. /// Abstraction of a PDU so users can have their own PDU types.
pub trait Event { pub trait Event: Clone + Debug {
/// Serialize into a Ruma JSON format, consuming.
#[inline]
fn into_format<T>(self) -> T
where
T: From<format::Owned<Self>>,
Self: Sized,
{
format::Owned(self).into()
}
/// Serialize into a Ruma JSON format
#[inline]
fn to_format<'a, T>(&'a self) -> T
where
T: From<format::Ref<'a, Self>>,
Self: Sized + 'a,
{
format::Ref(self).into()
}
#[inline]
fn contains_unsigned_property<T>(&self, property: &str, is_type: T) -> bool
where
T: FnOnce(&JsonValue) -> bool,
Self: Sized,
{
unsigned::contains_unsigned_property::<T, _>(self, property, is_type)
}
#[inline]
fn get_unsigned_property<T>(&self, property: &str) -> Result<T>
where
T: for<'de> Deserialize<'de>,
Self: Sized,
{
unsigned::get_unsigned_property::<T, _>(self, property)
}
#[inline]
fn get_unsigned_as_value(&self) -> JsonValue
where
Self: Sized,
{
unsigned::get_unsigned_as_value(self)
}
#[inline]
fn get_unsigned<T>(&self) -> Result<T>
where
T: for<'de> Deserialize<'de>,
Self: Sized,
{
unsigned::get_unsigned::<T, _>(self)
}
#[inline]
fn get_content_as_value(&self) -> JsonValue
where
Self: Sized,
{
content::as_value(self)
}
#[inline]
fn get_content<T>(&self) -> Result<T>
where
for<'de> T: Deserialize<'de>,
Self: Sized,
{
content::get::<T, _>(self)
}
#[inline]
fn redacts_id(&self, room_version: &RoomVersionId) -> Option<OwnedEventId>
where
Self: Sized,
{
redact::redacts_id(self, room_version)
}
#[inline]
fn is_redacted(&self) -> bool
where
Self: Sized,
{
redact::is_redacted(self)
}
#[inline]
fn into_canonical_object(self) -> CanonicalJsonObject
where
Self: Sized,
{
utils::to_canonical_object(self.into_pdu()).expect("failed to create Value::Object")
}
#[inline]
fn to_canonical_object(&self) -> CanonicalJsonObject {
utils::to_canonical_object(self.as_pdu()).expect("failed to create Value::Object")
}
#[inline]
fn into_value(self) -> JsonValue
where
Self: Sized,
{
serde_json::to_value(self.into_pdu()).expect("failed to create JSON Value")
}
#[inline]
fn to_value(&self) -> JsonValue {
serde_json::to_value(self.as_pdu()).expect("failed to create JSON Value")
}
#[inline]
fn as_mut_pdu(&mut self) -> &mut Pdu { unimplemented!("not a mutable Pdu") }
fn as_pdu(&self) -> &Pdu;
fn into_pdu(self) -> Pdu;
fn is_owned(&self) -> bool;
//
// Canonical properties
//
/// All the authenticating events for this event.
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_;
/// The event's content.
fn content(&self) -> &RawJsonValue;
/// The `EventId` of this event. /// The `EventId` of this event.
fn event_id(&self) -> &EventId; fn event_id(&self) -> &EventId;
/// The time of creation on the originating server.
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch;
/// The events before this event.
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_;
/// If this event is a redaction event this is the event it redacts.
fn redacts(&self) -> Option<&EventId>;
/// The `RoomId` of this event. /// The `RoomId` of this event.
fn room_id(&self) -> &RoomId; fn room_id(&self) -> &RoomId;
/// The `UserId` of this event. /// The `UserId` of this event.
fn sender(&self) -> &UserId; fn sender(&self) -> &UserId;
/// The time of creation on the originating server.
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch;
/// The event type.
fn event_type(&self) -> &TimelineEventType;
/// The event's content.
fn content(&self) -> &RawJsonValue;
/// The state key for this event. /// The state key for this event.
fn state_key(&self) -> Option<&str>; fn state_key(&self) -> Option<&str>;
/// The events before this event. /// The event type.
// Requires GATs to avoid boxing (and TAIT for making it convenient). fn kind(&self) -> &TimelineEventType;
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_;
/// All the authenticating events for this event. /// Metadata container; peer-trusted only.
// Requires GATs to avoid boxing (and TAIT for making it convenient). fn unsigned(&self) -> Option<&RawJsonValue>;
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_;
/// If this event is a redaction event this is the event it redacts. //#[deprecated]
fn redacts(&self) -> Option<&EventId>; #[inline]
} fn event_type(&self) -> &TimelineEventType { self.kind() }
impl<T: Event> Event for &T {
fn event_id(&self) -> &EventId { (*self).event_id() }
fn room_id(&self) -> &RoomId { (*self).room_id() }
fn sender(&self) -> &UserId { (*self).sender() }
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { (*self).origin_server_ts() }
fn event_type(&self) -> &TimelineEventType { (*self).event_type() }
fn content(&self) -> &RawJsonValue { (*self).content() }
fn state_key(&self) -> Option<&str> { (*self).state_key() }
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_ {
(*self).prev_events()
}
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_ {
(*self).auth_events()
}
fn redacts(&self) -> Option<&EventId> { (*self).redacts() }
} }

View file

@ -0,0 +1,21 @@
use serde::Deserialize;
use serde_json::value::Value as JsonValue;
use super::Event;
use crate::{Result, err};
#[inline]
#[must_use]
pub(super) fn as_value<E: Event>(event: &E) -> JsonValue {
get(event).expect("Failed to represent Event content as JsonValue")
}
#[inline]
pub(super) fn get<T, E>(event: &E) -> Result<T>
where
T: for<'de> Deserialize<'de>,
E: Event,
{
serde_json::from_str(event.content().get())
.map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))
}

View file

@ -0,0 +1,93 @@
use ruma::api::client::filter::{RoomEventFilter, UrlFilter};
use serde_json::Value;
use super::Event;
use crate::is_equal_to;
pub trait Matches<E: Event> {
fn matches(&self, event: &E) -> bool;
}
impl<E: Event> Matches<E> for &RoomEventFilter {
#[inline]
fn matches(&self, event: &E) -> bool {
if !matches_sender(event, self) {
return false;
}
if !matches_room(event, self) {
return false;
}
if !matches_type(event, self) {
return false;
}
if !matches_url(event, self) {
return false;
}
true
}
}
fn matches_room<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
if filter.not_rooms.iter().any(is_equal_to!(event.room_id())) {
return false;
}
if let Some(rooms) = filter.rooms.as_ref() {
if !rooms.iter().any(is_equal_to!(event.room_id())) {
return false;
}
}
true
}
fn matches_sender<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
if filter.not_senders.iter().any(is_equal_to!(event.sender())) {
return false;
}
if let Some(senders) = filter.senders.as_ref() {
if !senders.iter().any(is_equal_to!(event.sender())) {
return false;
}
}
true
}
fn matches_type<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
let kind = event.kind().to_cow_str();
if filter.not_types.iter().any(is_equal_to!(&kind)) {
return false;
}
if let Some(types) = filter.types.as_ref() {
if !types.iter().any(is_equal_to!(&kind)) {
return false;
}
}
true
}
fn matches_url<E: Event>(event: &E, filter: &RoomEventFilter) -> bool {
let Some(url_filter) = filter.url_filter.as_ref() else {
return true;
};
//TODO: might be better to use Ruma's Raw rather than serde here
let url = event
.get_content_as_value()
.get("url")
.is_some_and(Value::is_string);
match url_filter {
| UrlFilter::EventsWithUrl => url,
| UrlFilter::EventsWithoutUrl => !url,
}
}

View file

@ -0,0 +1,219 @@
use ruma::{
events::{
AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent,
AnySyncTimelineEvent, AnyTimelineEvent, StateEvent, room::member::RoomMemberEventContent,
space::child::HierarchySpaceChildEvent,
},
serde::Raw,
};
use serde_json::json;
use super::{Event, redact};
pub struct Owned<E: Event>(pub(super) E);
pub struct Ref<'a, E: Event>(pub(super) &'a E);
impl<E: Event> From<Owned<E>> for Raw<AnySyncTimelineEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnySyncTimelineEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let (redacts, content) = redact::copy(event);
let mut json = json!({
"content": content,
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"sender": event.sender(),
"type": event.event_type(),
});
if let Some(redacts) = redacts {
json["redacts"] = json!(redacts);
}
if let Some(state_key) = event.state_key() {
json["state_key"] = json!(state_key);
}
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnyTimelineEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnyTimelineEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let (redacts, content) = redact::copy(event);
let mut json = json!({
"content": content,
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id(),
"sender": event.sender(),
"type": event.kind(),
});
if let Some(redacts) = redacts {
json["redacts"] = json!(redacts);
}
if let Some(state_key) = event.state_key() {
json["state_key"] = json!(state_key);
}
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnyMessageLikeEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnyMessageLikeEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let (redacts, content) = redact::copy(event);
let mut json = json!({
"content": content,
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id(),
"sender": event.sender(),
"type": event.kind(),
});
if let Some(redacts) = &redacts {
json["redacts"] = json!(redacts);
}
if let Some(state_key) = event.state_key() {
json["state_key"] = json!(state_key);
}
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnyStateEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnyStateEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let mut json = json!({
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
});
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnySyncStateEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnySyncStateEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let mut json = json!({
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
});
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<AnyStrippedStateEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<AnyStrippedStateEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let json = json!({
"content": event.content(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
});
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<HierarchySpaceChildEvent> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<HierarchySpaceChildEvent> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let json = json!({
"content": event.content(),
"origin_server_ts": event.origin_server_ts(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
});
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}
impl<E: Event> From<Owned<E>> for Raw<StateEvent<RoomMemberEventContent>> {
fn from(event: Owned<E>) -> Self { Ref(&event.0).into() }
}
impl<'a, E: Event> From<Ref<'a, E>> for Raw<StateEvent<RoomMemberEventContent>> {
fn from(event: Ref<'a, E>) -> Self {
let event = event.0;
let mut json = json!({
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"redacts": event.redacts(),
"room_id": event.room_id(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
});
if let Some(unsigned) = event.unsigned() {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Failed to serialize Event value")
}
}

View file

@ -0,0 +1,86 @@
use ruma::{
OwnedEventId, RoomVersionId,
events::{TimelineEventType, room::redaction::RoomRedactionEventContent},
};
use serde::Deserialize;
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
use super::Event;
/// Copies the `redacts` property of the event to the `content` dict and
/// vice-versa.
///
/// This follows the specification's
/// [recommendation](https://spec.matrix.org/v1.10/rooms/v11/#moving-the-redacts-property-of-mroomredaction-events-to-a-content-property):
///
/// > For backwards-compatibility with older clients, servers should add a
/// > redacts property to the top level of m.room.redaction events in when
/// > serving such events over the Client-Server API.
///
/// > For improved compatibility with newer clients, servers should add a
/// > redacts property to the content of m.room.redaction events in older
/// > room versions when serving such events over the Client-Server API.
#[must_use]
pub(super) fn copy<E: Event>(event: &E) -> (Option<OwnedEventId>, Box<RawJsonValue>) {
if *event.event_type() != TimelineEventType::RoomRedaction {
return (event.redacts().map(ToOwned::to_owned), event.content().to_owned());
}
let Ok(mut content) = event.get_content::<RoomRedactionEventContent>() else {
return (event.redacts().map(ToOwned::to_owned), event.content().to_owned());
};
if let Some(redacts) = content.redacts {
return (Some(redacts), event.content().to_owned());
}
if let Some(redacts) = event.redacts().map(ToOwned::to_owned) {
content.redacts = Some(redacts);
return (
event.redacts().map(ToOwned::to_owned),
to_raw_value(&content).expect("Must be valid, we only added redacts field"),
);
}
(event.redacts().map(ToOwned::to_owned), event.content().to_owned())
}
#[must_use]
pub(super) fn is_redacted<E: Event>(event: &E) -> bool {
let Some(unsigned) = event.unsigned() else {
return false;
};
let Ok(unsigned) = ExtractRedactedBecause::deserialize(unsigned) else {
return false;
};
unsigned.redacted_because.is_some()
}
#[must_use]
pub(super) fn redacts_id<E: Event>(
event: &E,
room_version: &RoomVersionId,
) -> Option<OwnedEventId> {
use RoomVersionId::*;
if *event.kind() != TimelineEventType::RoomRedaction {
return None;
}
match *room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
event.redacts().map(ToOwned::to_owned),
| _ =>
event
.get_content::<RoomRedactionEventContent>()
.ok()?
.redacts,
}
}
#[derive(Deserialize)]
struct ExtractRedactedBecause {
redacted_because: Option<serde::de::IgnoredAny>,
}

View file

@ -0,0 +1,28 @@
use ruma::events::relation::RelationType;
use serde::Deserialize;
use super::Event;
pub trait RelationTypeEqual<E: Event> {
fn relation_type_equal(&self, event: &E) -> bool;
}
#[derive(Clone, Debug, Deserialize)]
struct ExtractRelatesToEventId {
#[serde(rename = "m.relates_to")]
relates_to: ExtractRelType,
}
#[derive(Clone, Debug, Deserialize)]
struct ExtractRelType {
rel_type: RelationType,
}
impl<E: Event> RelationTypeEqual<E> for RelationType {
fn relation_type_equal(&self, event: &E) -> bool {
event
.get_content()
.map(|c: ExtractRelatesToEventId| c.relates_to.rel_type)
.is_ok_and(|r| r == *self)
}
}

View file

@ -0,0 +1,32 @@
use ruma::events::{StateEventType, TimelineEventType};
use super::StateKey;
/// Convenience trait for adding event type plus state key to state maps.
pub trait TypeExt {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey);
}
impl TypeExt for StateEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self, state_key.into())
}
}
impl TypeExt for &StateEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self.clone(), state_key.into())
}
}
impl TypeExt for TimelineEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self.into(), state_key.into())
}
}
impl TypeExt for &TimelineEventType {
fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, StateKey) {
(self.clone().into(), state_key.into())
}
}

View file

@ -0,0 +1,51 @@
use serde::Deserialize;
use serde_json::value::Value as JsonValue;
use super::Event;
use crate::{Result, err, is_true};
pub(super) fn contains_unsigned_property<F, E>(event: &E, property: &str, is_type: F) -> bool
where
F: FnOnce(&JsonValue) -> bool,
E: Event,
{
get_unsigned_as_value(event)
.get(property)
.map(is_type)
.is_some_and(is_true!())
}
pub(super) fn get_unsigned_property<T, E>(event: &E, property: &str) -> Result<T>
where
T: for<'de> Deserialize<'de>,
E: Event,
{
get_unsigned_as_value(event)
.get_mut(property)
.map(JsonValue::take)
.map(serde_json::from_value)
.ok_or(err!(Request(NotFound("property not found in unsigned object"))))?
.map_err(|e| err!(Database("Failed to deserialize unsigned.{property} into type: {e}")))
}
#[must_use]
pub(super) fn get_unsigned_as_value<E>(event: &E) -> JsonValue
where
E: Event,
{
get_unsigned::<JsonValue, E>(event).unwrap_or_default()
}
pub(super) fn get_unsigned<T, E>(event: &E) -> Result<T>
where
T: for<'de> Deserialize<'de>,
E: Event,
{
event
.unsigned()
.as_ref()
.map(|raw| raw.get())
.map(serde_json::from_str)
.ok_or(err!(Request(NotFound("\"unsigned\" property not found in pdu"))))?
.map_err(|e| err!(Database("Failed to deserialize \"unsigned\" into value: {e}")))
}

View file

@ -2,8 +2,10 @@
pub mod event; pub mod event;
pub mod pdu; pub mod pdu;
pub mod state_key;
pub mod state_res; pub mod state_res;
pub use event::Event; pub use event::{Event, TypeExt as EventTypeExt};
pub use pdu::{PduBuilder, PduCount, PduEvent, PduId, RawPduId, StateKey}; pub use pdu::{Pdu, PduBuilder, PduCount, PduEvent, PduId, RawPduId, ShortId};
pub use state_res::{EventTypeExt, RoomVersion, StateMap, TypeStateKey}; pub use state_key::StateKey;
pub use state_res::{RoomVersion, StateMap, TypeStateKey};

View file

@ -1,14 +1,8 @@
mod builder; mod builder;
mod content;
mod count; mod count;
mod event_id;
mod filter;
mod id; mod id;
mod raw_id; mod raw_id;
mod redact; mod redact;
mod relation;
mod state_key;
mod strip;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
mod unsigned; mod unsigned;
@ -26,38 +20,50 @@ pub use self::{
Count as PduCount, Id as PduId, Pdu as PduEvent, RawId as RawPduId, Count as PduCount, Id as PduId, Pdu as PduEvent, RawId as RawPduId,
builder::{Builder, Builder as PduBuilder}, builder::{Builder, Builder as PduBuilder},
count::Count, count::Count,
event_id::*, id::{ShortId, *},
id::*,
raw_id::*, raw_id::*,
state_key::{ShortStateKey, StateKey},
}; };
use super::Event; use super::{Event, StateKey};
use crate::Result; use crate::Result;
/// Persistent Data Unit (Event) /// Persistent Data Unit (Event)
#[derive(Clone, Deserialize, Serialize, Debug)] #[derive(Clone, Deserialize, Serialize, Debug)]
pub struct Pdu { pub struct Pdu {
pub event_id: OwnedEventId, pub event_id: OwnedEventId,
pub room_id: OwnedRoomId, pub room_id: OwnedRoomId,
pub sender: OwnedUserId, pub sender: OwnedUserId,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub origin: Option<OwnedServerName>, pub origin: Option<OwnedServerName>,
pub origin_server_ts: UInt, pub origin_server_ts: UInt,
#[serde(rename = "type")] #[serde(rename = "type")]
pub kind: TimelineEventType, pub kind: TimelineEventType,
pub content: Box<RawJsonValue>, pub content: Box<RawJsonValue>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub state_key: Option<StateKey>, pub state_key: Option<StateKey>,
pub prev_events: Vec<OwnedEventId>, pub prev_events: Vec<OwnedEventId>,
pub depth: UInt, pub depth: UInt,
pub auth_events: Vec<OwnedEventId>, pub auth_events: Vec<OwnedEventId>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub redacts: Option<OwnedEventId>, pub redacts: Option<OwnedEventId>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub unsigned: Option<Box<RawJsonValue>>, pub unsigned: Option<Box<RawJsonValue>>,
pub hashes: EventHash, pub hashes: EventHash,
#[serde(default, skip_serializing_if = "Option::is_none")]
// BTreeMap<Box<ServerName>, BTreeMap<ServerSigningKeyId, String>> // BTreeMap<Box<ServerName>, BTreeMap<ServerSigningKeyId, String>>
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signatures: Option<Box<RawJsonValue>>, pub signatures: Option<Box<RawJsonValue>>,
} }
@ -79,31 +85,106 @@ impl Pdu {
} }
impl Event for Pdu { impl Event for Pdu {
fn event_id(&self) -> &EventId { &self.event_id } #[inline]
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
fn room_id(&self) -> &RoomId { &self.room_id } self.auth_events.iter().map(AsRef::as_ref)
}
fn sender(&self) -> &UserId { &self.sender }
fn event_type(&self) -> &TimelineEventType { &self.kind }
#[inline]
fn content(&self) -> &RawJsonValue { &self.content } fn content(&self) -> &RawJsonValue { &self.content }
#[inline]
fn event_id(&self) -> &EventId { &self.event_id }
#[inline]
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch { fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
MilliSecondsSinceUnixEpoch(self.origin_server_ts) MilliSecondsSinceUnixEpoch(self.origin_server_ts)
} }
fn state_key(&self) -> Option<&str> { self.state_key.as_deref() } #[inline]
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_ {
self.prev_events.iter().map(AsRef::as_ref) self.prev_events.iter().map(AsRef::as_ref)
} }
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Send + '_ { #[inline]
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
#[inline]
fn room_id(&self) -> &RoomId { &self.room_id }
#[inline]
fn sender(&self) -> &UserId { &self.sender }
#[inline]
fn state_key(&self) -> Option<&str> { self.state_key.as_deref() }
#[inline]
fn kind(&self) -> &TimelineEventType { &self.kind }
#[inline]
fn unsigned(&self) -> Option<&RawJsonValue> { self.unsigned.as_deref() }
#[inline]
fn as_mut_pdu(&mut self) -> &mut Pdu { self }
#[inline]
fn as_pdu(&self) -> &Pdu { self }
#[inline]
fn into_pdu(self) -> Pdu { self }
#[inline]
fn is_owned(&self) -> bool { true }
}
impl Event for &Pdu {
#[inline]
fn auth_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
self.auth_events.iter().map(AsRef::as_ref) self.auth_events.iter().map(AsRef::as_ref)
} }
#[inline]
fn content(&self) -> &RawJsonValue { &self.content }
#[inline]
fn event_id(&self) -> &EventId { &self.event_id }
#[inline]
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
MilliSecondsSinceUnixEpoch(self.origin_server_ts)
}
#[inline]
fn prev_events(&self) -> impl DoubleEndedIterator<Item = &EventId> + Clone + Send + '_ {
self.prev_events.iter().map(AsRef::as_ref)
}
#[inline]
fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() } fn redacts(&self) -> Option<&EventId> { self.redacts.as_deref() }
#[inline]
fn room_id(&self) -> &RoomId { &self.room_id }
#[inline]
fn sender(&self) -> &UserId { &self.sender }
#[inline]
fn state_key(&self) -> Option<&str> { self.state_key.as_deref() }
#[inline]
fn kind(&self) -> &TimelineEventType { &self.kind }
#[inline]
fn unsigned(&self) -> Option<&RawJsonValue> { self.unsigned.as_deref() }
#[inline]
fn as_pdu(&self) -> &Pdu { self }
#[inline]
fn into_pdu(self) -> Pdu { self.clone() }
#[inline]
fn is_owned(&self) -> bool { false }
} }
/// Prevent derived equality which wouldn't limit itself to event_id /// Prevent derived equality which wouldn't limit itself to event_id

View file

@ -1,20 +0,0 @@
use serde::Deserialize;
use serde_json::value::Value as JsonValue;
use crate::{Result, err, implement};
#[must_use]
#[implement(super::Pdu)]
pub fn get_content_as_value(&self) -> JsonValue {
self.get_content()
.expect("pdu content must be a valid JSON value")
}
#[implement(super::Pdu)]
pub fn get_content<T>(&self) -> Result<T>
where
T: for<'de> Deserialize<'de>,
{
serde_json::from_str(self.content.get())
.map_err(|e| err!(Database("Failed to deserialize pdu content into type: {e}")))
}

View file

@ -1,90 +0,0 @@
use ruma::api::client::filter::{RoomEventFilter, UrlFilter};
use serde_json::Value;
use crate::{implement, is_equal_to};
#[implement(super::Pdu)]
#[must_use]
pub fn matches(&self, filter: &RoomEventFilter) -> bool {
if !self.matches_sender(filter) {
return false;
}
if !self.matches_room(filter) {
return false;
}
if !self.matches_type(filter) {
return false;
}
if !self.matches_url(filter) {
return false;
}
true
}
#[implement(super::Pdu)]
fn matches_room(&self, filter: &RoomEventFilter) -> bool {
if filter.not_rooms.contains(&self.room_id) {
return false;
}
if let Some(rooms) = filter.rooms.as_ref() {
if !rooms.contains(&self.room_id) {
return false;
}
}
true
}
#[implement(super::Pdu)]
fn matches_sender(&self, filter: &RoomEventFilter) -> bool {
if filter.not_senders.contains(&self.sender) {
return false;
}
if let Some(senders) = filter.senders.as_ref() {
if !senders.contains(&self.sender) {
return false;
}
}
true
}
#[implement(super::Pdu)]
fn matches_type(&self, filter: &RoomEventFilter) -> bool {
let event_type = &self.kind.to_cow_str();
if filter.not_types.iter().any(is_equal_to!(event_type)) {
return false;
}
if let Some(types) = filter.types.as_ref() {
if !types.iter().any(is_equal_to!(event_type)) {
return false;
}
}
true
}
#[implement(super::Pdu)]
fn matches_url(&self, filter: &RoomEventFilter) -> bool {
let Some(url_filter) = filter.url_filter.as_ref() else {
return true;
};
//TODO: might be better to use Ruma's Raw rather than serde here
let url = serde_json::from_str::<Value>(self.content.get())
.expect("parsing content JSON failed")
.get("url")
.is_some_and(Value::is_string);
match url_filter {
| UrlFilter::EventsWithUrl => url,
| UrlFilter::EventsWithoutUrl => !url,
}
}

View file

@ -3,6 +3,7 @@ use crate::utils::u64_from_u8x8;
pub type ShortRoomId = ShortId; pub type ShortRoomId = ShortId;
pub type ShortEventId = ShortId; pub type ShortEventId = ShortId;
pub type ShortStateKey = ShortId;
pub type ShortId = u64; pub type ShortId = u64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]

View file

@ -1,117 +1,29 @@
use ruma::{ use ruma::{RoomVersionId, canonical_json::redact_content_in_place};
OwnedEventId, RoomVersionId, use serde_json::{Value as JsonValue, json, value::to_raw_value};
canonical_json::redact_content_in_place,
events::{TimelineEventType, room::redaction::RoomRedactionEventContent},
};
use serde::Deserialize;
use serde_json::{
json,
value::{RawValue as RawJsonValue, to_raw_value},
};
use crate::{Error, Result, implement}; use crate::{Error, Result, err, implement};
#[derive(Deserialize)]
struct ExtractRedactedBecause {
redacted_because: Option<serde::de::IgnoredAny>,
}
#[implement(super::Pdu)] #[implement(super::Pdu)]
pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: &Self) -> Result { pub fn redact(&mut self, room_version_id: &RoomVersionId, reason: JsonValue) -> Result {
self.unsigned = None; self.unsigned = None;
let mut content = serde_json::from_str(self.content.get()) let mut content = serde_json::from_str(self.content.get())
.map_err(|_| Error::bad_database("PDU in db has invalid content."))?; .map_err(|e| err!(Request(BadJson("Failed to deserialize content into type: {e}"))))?;
redact_content_in_place(&mut content, room_version_id, self.kind.to_string()) redact_content_in_place(&mut content, room_version_id, self.kind.to_string())
.map_err(|e| Error::Redaction(self.sender.server_name().to_owned(), e))?; .map_err(|e| Error::Redaction(self.sender.server_name().to_owned(), e))?;
self.unsigned = Some( let reason = serde_json::to_value(reason).expect("Failed to preserialize reason");
to_raw_value(&json!({
"redacted_because": serde_json::to_value(reason).expect("to_value(Pdu) always works")
}))
.expect("to string always works"),
);
self.content = to_raw_value(&content).expect("to string always works"); let redacted_because = json!({
"redacted_because": reason,
});
self.unsigned = to_raw_value(&redacted_because)
.expect("Failed to serialize unsigned")
.into();
self.content = to_raw_value(&content).expect("Failed to serialize content");
Ok(()) Ok(())
} }
#[implement(super::Pdu)]
#[must_use]
pub fn is_redacted(&self) -> bool {
let Some(unsigned) = &self.unsigned else {
return false;
};
let Ok(unsigned) = ExtractRedactedBecause::deserialize(&**unsigned) else {
return false;
};
unsigned.redacted_because.is_some()
}
/// Copies the `redacts` property of the event to the `content` dict and
/// vice-versa.
///
/// This follows the specification's
/// [recommendation](https://spec.matrix.org/v1.10/rooms/v11/#moving-the-redacts-property-of-mroomredaction-events-to-a-content-property):
///
/// > For backwards-compatibility with older clients, servers should add a
/// > redacts
/// > property to the top level of m.room.redaction events in when serving
/// > such events
/// > over the Client-Server API.
///
/// > For improved compatibility with newer clients, servers should add a
/// > redacts property
/// > to the content of m.room.redaction events in older room versions when
/// > serving
/// > such events over the Client-Server API.
#[implement(super::Pdu)]
#[must_use]
pub fn copy_redacts(&self) -> (Option<OwnedEventId>, Box<RawJsonValue>) {
if self.kind == TimelineEventType::RoomRedaction {
if let Ok(mut content) =
serde_json::from_str::<RoomRedactionEventContent>(self.content.get())
{
match content.redacts {
| Some(redacts) => {
return (Some(redacts), self.content.clone());
},
| _ => match self.redacts.clone() {
| Some(redacts) => {
content.redacts = Some(redacts);
return (
self.redacts.clone(),
to_raw_value(&content)
.expect("Must be valid, we only added redacts field"),
);
},
| _ => {},
},
}
}
}
(self.redacts.clone(), self.content.clone())
}
#[implement(super::Pdu)]
#[must_use]
pub fn redacts_id(&self, room_version: &RoomVersionId) -> Option<OwnedEventId> {
use RoomVersionId::*;
if self.kind != TimelineEventType::RoomRedaction {
return None;
}
match *room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 => self.redacts.clone(),
| _ =>
self.get_content::<RoomRedactionEventContent>()
.ok()?
.redacts,
}
}

View file

@ -1,22 +0,0 @@
use ruma::events::relation::RelationType;
use serde::Deserialize;
use crate::implement;
#[derive(Clone, Debug, Deserialize)]
struct ExtractRelType {
rel_type: RelationType,
}
#[derive(Clone, Debug, Deserialize)]
struct ExtractRelatesToEventId {
#[serde(rename = "m.relates_to")]
relates_to: ExtractRelType,
}
#[implement(super::Pdu)]
#[must_use]
pub fn relation_type_equal(&self, rel_type: &RelationType) -> bool {
self.get_content()
.map(|c: ExtractRelatesToEventId| c.relates_to.rel_type)
.is_ok_and(|r| r == *rel_type)
}

View file

@ -1,257 +0,0 @@
use ruma::{
events::{
AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent, AnySyncStateEvent,
AnySyncTimelineEvent, AnyTimelineEvent, StateEvent, room::member::RoomMemberEventContent,
space::child::HierarchySpaceChildEvent,
},
serde::Raw,
};
use serde_json::{json, value::Value as JsonValue};
use crate::implement;
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_room_event(self) -> Raw<AnyTimelineEvent> { self.to_room_event() }
#[implement(super::Pdu)]
#[must_use]
pub fn to_room_event(&self) -> Raw<AnyTimelineEvent> {
let value = self.to_room_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn to_room_event_value(&self) -> JsonValue {
let (redacts, content) = self.copy_redacts();
let mut json = json!({
"content": content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"room_id": self.room_id,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
if let Some(state_key) = &self.state_key {
json["state_key"] = json!(state_key);
}
if let Some(redacts) = &redacts {
json["redacts"] = json!(redacts);
}
json
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_message_like_event(self) -> Raw<AnyMessageLikeEvent> { self.to_message_like_event() }
#[implement(super::Pdu)]
#[must_use]
pub fn to_message_like_event(&self) -> Raw<AnyMessageLikeEvent> {
let value = self.to_message_like_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn to_message_like_event_value(&self) -> JsonValue {
let (redacts, content) = self.copy_redacts();
let mut json = json!({
"content": content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"room_id": self.room_id,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
if let Some(state_key) = &self.state_key {
json["state_key"] = json!(state_key);
}
if let Some(redacts) = &redacts {
json["redacts"] = json!(redacts);
}
json
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_sync_room_event(self) -> Raw<AnySyncTimelineEvent> { self.to_sync_room_event() }
#[implement(super::Pdu)]
#[must_use]
pub fn to_sync_room_event(&self) -> Raw<AnySyncTimelineEvent> {
let value = self.to_sync_room_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn to_sync_room_event_value(&self) -> JsonValue {
let (redacts, content) = self.copy_redacts();
let mut json = json!({
"content": content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
if let Some(state_key) = &self.state_key {
json["state_key"] = json!(state_key);
}
if let Some(redacts) = &redacts {
json["redacts"] = json!(redacts);
}
json
}
#[implement(super::Pdu)]
#[must_use]
pub fn into_state_event(self) -> Raw<AnyStateEvent> {
let value = self.into_state_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_state_event_value(self) -> JsonValue {
let mut json = json!({
"content": self.content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"room_id": self.room_id,
"state_key": self.state_key,
});
if let Some(unsigned) = self.unsigned {
json["unsigned"] = json!(unsigned);
}
json
}
#[implement(super::Pdu)]
#[must_use]
pub fn into_sync_state_event(self) -> Raw<AnySyncStateEvent> {
let value = self.into_sync_state_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_sync_state_event_value(self) -> JsonValue {
let mut json = json!({
"content": self.content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"state_key": self.state_key,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
json
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_stripped_state_event(self) -> Raw<AnyStrippedStateEvent> {
self.to_stripped_state_event()
}
#[implement(super::Pdu)]
#[must_use]
pub fn to_stripped_state_event(&self) -> Raw<AnyStrippedStateEvent> {
let value = self.to_stripped_state_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn to_stripped_state_event_value(&self) -> JsonValue {
json!({
"content": self.content,
"type": self.kind,
"sender": self.sender,
"state_key": self.state_key,
})
}
#[implement(super::Pdu)]
#[must_use]
pub fn into_stripped_spacechild_state_event(self) -> Raw<HierarchySpaceChildEvent> {
let value = self.into_stripped_spacechild_state_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_stripped_spacechild_state_event_value(self) -> JsonValue {
json!({
"content": self.content,
"type": self.kind,
"sender": self.sender,
"state_key": self.state_key,
"origin_server_ts": self.origin_server_ts,
})
}
#[implement(super::Pdu)]
#[must_use]
pub fn into_member_event(self) -> Raw<StateEvent<RoomMemberEventContent>> {
let value = self.into_member_event_value();
serde_json::from_value(value).expect("Failed to serialize Event value")
}
#[implement(super::Pdu)]
#[must_use]
#[inline]
pub fn into_member_event_value(self) -> JsonValue {
let mut json = json!({
"content": self.content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"redacts": self.redacts,
"room_id": self.room_id,
"state_key": self.state_key,
});
if let Some(unsigned) = self.unsigned {
json["unsigned"] = json!(unsigned);
}
json
}

View file

@ -1,11 +1,10 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use ruma::MilliSecondsSinceUnixEpoch; use ruma::MilliSecondsSinceUnixEpoch;
use serde::Deserialize;
use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue, to_raw_value}; use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue, to_raw_value};
use super::Pdu; use super::Pdu;
use crate::{Result, err, implement, is_true}; use crate::{Result, err, implement};
#[implement(Pdu)] #[implement(Pdu)]
pub fn remove_transaction_id(&mut self) -> Result { pub fn remove_transaction_id(&mut self) -> Result {
@ -74,43 +73,3 @@ pub fn add_relation(&mut self, name: &str, pdu: Option<&Pdu>) -> Result {
Ok(()) Ok(())
} }
#[implement(Pdu)]
pub fn contains_unsigned_property<F>(&self, property: &str, is_type: F) -> bool
where
F: FnOnce(&JsonValue) -> bool,
{
self.get_unsigned_as_value()
.get(property)
.map(is_type)
.is_some_and(is_true!())
}
#[implement(Pdu)]
pub fn get_unsigned_property<T>(&self, property: &str) -> Result<T>
where
T: for<'de> Deserialize<'de>,
{
self.get_unsigned_as_value()
.get_mut(property)
.map(JsonValue::take)
.map(serde_json::from_value)
.ok_or(err!(Request(NotFound("property not found in unsigned object"))))?
.map_err(|e| err!(Database("Failed to deserialize unsigned.{property} into type: {e}")))
}
#[implement(Pdu)]
#[must_use]
pub fn get_unsigned_as_value(&self) -> JsonValue {
self.get_unsigned::<JsonValue>().unwrap_or_default()
}
#[implement(Pdu)]
pub fn get_unsigned<T>(&self) -> Result<JsonValue> {
self.unsigned
.as_ref()
.map(|raw| raw.get())
.map(serde_json::from_str)
.ok_or(err!(Request(NotFound("\"unsigned\" property not found in pdu"))))?
.map_err(|e| err!(Database("Failed to deserialize \"unsigned\" into value: {e}")))
}

View file

@ -1,8 +1,5 @@
use smallstr::SmallString; use smallstr::SmallString;
use super::ShortId;
pub type StateKey = SmallString<[u8; INLINE_SIZE]>; pub type StateKey = SmallString<[u8; INLINE_SIZE]>;
pub type ShortStateKey = ShortId;
const INLINE_SIZE: usize = 48; const INLINE_SIZE: usize = 48;

View file

@ -13,7 +13,6 @@ use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId,
events::{ events::{
StateEventType, TimelineEventType, StateEventType, TimelineEventType,
pdu::{EventHash, Pdu, RoomV3Pdu},
room::{ room::{
join_rules::{JoinRule, RoomJoinRulesEventContent}, join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent}, member::{MembershipState, RoomMemberEventContent},
@ -26,8 +25,10 @@ use serde_json::{
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value}, value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
}; };
use self::event::PduEvent; use crate::{
use crate::state_res::{self as state_res, Error, Event, Result, StateMap}; matrix::{Event, Pdu, pdu::EventHash},
state_res::{self as state_res, Error, Result, StateMap},
};
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
@ -60,7 +61,7 @@ fn resolution_shallow_auth_chain(c: &mut test::Bencher) {
c.iter(|| async { c.iter(|| async {
let ev_map = store.0.clone(); let ev_map = store.0.clone();
let state_sets = [&state_at_bob, &state_at_charlie]; let state_sets = [&state_at_bob, &state_at_charlie];
let fetch = |id: OwnedEventId| ready(ev_map.get(&id).clone()); let fetch = |id: OwnedEventId| ready(ev_map.get(&id).map(ToOwned::to_owned));
let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some()); let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some());
let auth_chain_sets: Vec<HashSet<_>> = state_sets let auth_chain_sets: Vec<HashSet<_>> = state_sets
.iter() .iter()
@ -142,7 +143,7 @@ fn resolve_deeper_event_set(c: &mut test::Bencher) {
}) })
.collect(); .collect();
let fetch = |id: OwnedEventId| ready(inner.get(&id).clone()); let fetch = |id: OwnedEventId| ready(inner.get(&id).map(ToOwned::to_owned));
let exists = |id: OwnedEventId| ready(inner.get(&id).is_some()); let exists = |id: OwnedEventId| ready(inner.get(&id).is_some());
let _ = match state_res::resolve( let _ = match state_res::resolve(
&RoomVersionId::V6, &RoomVersionId::V6,
@ -246,7 +247,7 @@ impl<E: Event + Clone> TestStore<E> {
} }
} }
impl TestStore<PduEvent> { impl TestStore<Pdu> {
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn set_up( fn set_up(
&mut self, &mut self,
@ -380,7 +381,7 @@ fn to_pdu_event<S>(
content: Box<RawJsonValue>, content: Box<RawJsonValue>,
auth_events: &[S], auth_events: &[S],
prev_events: &[S], prev_events: &[S],
) -> PduEvent ) -> Pdu
where where
S: AsRef<str>, S: AsRef<str>,
{ {
@ -403,30 +404,28 @@ where
.map(event_id) .map(event_id)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let state_key = state_key.map(ToOwned::to_owned); Pdu {
PduEvent {
event_id: id.try_into().unwrap(), event_id: id.try_into().unwrap(),
rest: Pdu::RoomV3Pdu(RoomV3Pdu { room_id: room_id().to_owned(),
room_id: room_id().to_owned(), sender: sender.to_owned(),
sender: sender.to_owned(), origin_server_ts: ts.try_into().unwrap(),
origin_server_ts: MilliSecondsSinceUnixEpoch(ts.try_into().unwrap()), state_key: state_key.map(Into::into),
state_key, kind: ev_type,
kind: ev_type, content,
content, origin: None,
redacts: None, redacts: None,
unsigned: btreemap! {}, unsigned: None,
auth_events, auth_events,
prev_events, prev_events,
depth: uint!(0), depth: uint!(0),
hashes: EventHash::new(String::new()), hashes: EventHash { sha256: String::new() },
signatures: Signatures::new(), signatures: None,
}),
} }
} }
// all graphs start with these input events // all graphs start with these input events
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> { fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
vec![ vec![
to_pdu_event::<&EventId>( to_pdu_event::<&EventId>(
"CREATE", "CREATE",
@ -508,7 +507,7 @@ fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
// all graphs start with these input events // all graphs start with these input events
#[allow(non_snake_case)] #[allow(non_snake_case)]
fn BAN_STATE_SET() -> HashMap<OwnedEventId, PduEvent> { fn BAN_STATE_SET() -> HashMap<OwnedEventId, Pdu> {
vec![ vec![
to_pdu_event( to_pdu_event(
"PA", "PA",
@ -551,119 +550,3 @@ fn BAN_STATE_SET() -> HashMap<OwnedEventId, PduEvent> {
.map(|ev| (ev.event_id().to_owned(), ev)) .map(|ev| (ev.event_id().to_owned(), ev))
.collect() .collect()
} }
/// Convenience trait for adding event type plus state key to state maps.
trait EventTypeExt {
fn with_state_key(self, state_key: impl Into<String>) -> (StateEventType, String);
}
impl EventTypeExt for &TimelineEventType {
fn with_state_key(self, state_key: impl Into<String>) -> (StateEventType, String) {
(self.to_string().into(), state_key.into())
}
}
mod event {
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId,
events::{TimelineEventType, pdu::Pdu},
};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue as RawJsonValue;
use super::Event;
impl Event for PduEvent {
fn event_id(&self) -> &EventId { &self.event_id }
fn room_id(&self) -> &RoomId {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.room_id,
| Pdu::RoomV3Pdu(ev) => &ev.room_id,
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn sender(&self) -> &UserId {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.sender,
| Pdu::RoomV3Pdu(ev) => &ev.sender,
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn event_type(&self) -> &TimelineEventType {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.kind,
| Pdu::RoomV3Pdu(ev) => &ev.kind,
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn content(&self) -> &RawJsonValue {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.content,
| Pdu::RoomV3Pdu(ev) => &ev.content,
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.origin_server_ts,
| Pdu::RoomV3Pdu(ev) => ev.origin_server_ts,
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn state_key(&self) -> Option<&str> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.state_key.as_deref(),
| Pdu::RoomV3Pdu(ev) => ev.state_key.as_deref(),
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn prev_events(&self) -> Box<dyn DoubleEndedIterator<Item = &EventId> + Send + '_> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) =>
Box::new(ev.prev_events.iter().map(|(id, _)| id.as_ref())),
| Pdu::RoomV3Pdu(ev) => Box::new(ev.prev_events.iter().map(AsRef::as_ref)),
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn auth_events(&self) -> Box<dyn DoubleEndedIterator<Item = &EventId> + Send + '_> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) =>
Box::new(ev.auth_events.iter().map(|(id, _)| id.as_ref())),
| Pdu::RoomV3Pdu(ev) => Box::new(ev.auth_events.iter().map(AsRef::as_ref)),
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
fn redacts(&self) -> Option<&EventId> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.redacts.as_deref(),
| Pdu::RoomV3Pdu(ev) => ev.redacts.as_deref(),
#[cfg(not(feature = "unstable-exhaustive-types"))]
| _ => unreachable!("new PDU version"),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub(crate) struct PduEvent {
pub(crate) event_id: OwnedEventId,
#[serde(flatten)]
pub(crate) rest: Pdu,
}
}

View file

@ -13,6 +13,7 @@ use ruma::{
power_levels::RoomPowerLevelsEventContent, power_levels::RoomPowerLevelsEventContent,
third_party_invite::RoomThirdPartyInviteEventContent, third_party_invite::RoomThirdPartyInviteEventContent,
}, },
EventId,
int, int,
serde::{Base64, Raw}, serde::{Base64, Raw},
}; };
@ -21,7 +22,6 @@ use serde::{
de::{Error as _, IgnoredAny}, de::{Error as _, IgnoredAny},
}; };
use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue}; use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue};
use super::{ use super::{
Error, Event, Result, StateEventType, StateKey, TimelineEventType, Error, Event, Result, StateEventType, StateKey, TimelineEventType,
power_levels::{ power_levels::{
@ -136,17 +136,17 @@ pub fn auth_types_for_event(
event_id = incoming_event.event_id().as_str(), event_id = incoming_event.event_id().as_str(),
) )
)] )]
pub async fn auth_check<F, Fut, Fetched, Incoming>( pub async fn auth_check<E, F, Fut>(
room_version: &RoomVersion, room_version: &RoomVersion,
incoming_event: &Incoming, incoming_event: &E,
current_third_party_invite: Option<&Incoming>, current_third_party_invite: Option<&E>,
fetch_state: F, fetch_state: F,
) -> Result<bool, Error> ) -> Result<bool, Error>
where where
F: Fn(&StateEventType, &str) -> Fut + Send, F: Fn(&StateEventType, &str) -> Fut + Send,
Fut: Future<Output = Option<Fetched>> + Send, Fut: Future<Output = Option<E>> + Send,
Fetched: Event + Send, E: Event + Send + Sync,
Incoming: Event + Send + Sync, for<'a> &'a E: Event + Send,
{ {
debug!( debug!(
event_id = format!("{}", incoming_event.event_id()), event_id = format!("{}", incoming_event.event_id()),
@ -217,8 +217,9 @@ where
} }
/* /*
// TODO: In the past this code caused problems federating with synapse, maybe this has been // TODO: In the past this code was commented as it caused problems with Synapse. This is no
// resolved already. Needs testing. // longer the case. This needs to be implemented.
// See also: https://github.com/ruma/ruma/pull/2064
// //
// 2. Reject if auth_events // 2. Reject if auth_events
// a. auth_events cannot have duplicate keys since it's a BTree // a. auth_events cannot have duplicate keys since it's a BTree
@ -241,20 +242,46 @@ where
} }
*/ */
let (room_create_event, power_levels_event, sender_member_event) = join3( // let (room_create_event, power_levels_event, sender_member_event) = join3(
fetch_state(&StateEventType::RoomCreate, ""), // fetch_state(&StateEventType::RoomCreate, ""),
fetch_state(&StateEventType::RoomPowerLevels, ""), // fetch_state(&StateEventType::RoomPowerLevels, ""),
fetch_state(&StateEventType::RoomMember, sender.as_str()), // fetch_state(&StateEventType::RoomMember, sender.as_str()),
) // )
.await; // .await;
let room_create_event = fetch_state(&StateEventType::RoomCreate, "").await;
let power_levels_event = fetch_state(&StateEventType::RoomPowerLevels, "").await;
let sender_member_event = fetch_state(&StateEventType::RoomMember, sender.as_str()).await;
let room_create_event = match room_create_event { let room_create_event = match room_create_event {
| None => { | None => {
warn!("no m.room.create event in auth chain"); error!(
create_event = room_create_event.as_ref().map(Event::event_id).unwrap_or(<&EventId>::try_from("$unknown").unwrap()).as_str(),
power_levels = power_levels_event.as_ref().map(Event::event_id).unwrap_or(<&EventId>::try_from("$unknown").unwrap()).as_str(),
member_event = sender_member_event.as_ref().map(Event::event_id).unwrap_or(<&EventId>::try_from("$unknown").unwrap()).as_str(),
"no m.room.create event found for {} ({})!",
incoming_event.event_id().as_str(),
incoming_event.room_id().as_str()
);
return Ok(false); return Ok(false);
}, },
| Some(e) => e, | Some(e) => e,
}; };
// just re-check 1.2 to work around a bug
let Some(room_id_server_name) = incoming_event.room_id().server_name() else {
warn!("room ID has no servername");
return Ok(false);
};
if room_id_server_name != room_create_event.sender().server_name() {
warn!(
"servername of room ID origin ({}) does not match servername of m.room.create \
sender ({})",
room_id_server_name,
room_create_event.sender().server_name()
);
return Ok(false);
}
// 3. If event does not have m.room.create in auth_events reject // 3. If event does not have m.room.create in auth_events reject
if !incoming_event if !incoming_event
@ -514,20 +541,24 @@ where
/// event and the current State. /// event and the current State.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
fn valid_membership_change( fn valid_membership_change<E>(
room_version: &RoomVersion, room_version: &RoomVersion,
target_user: &UserId, target_user: &UserId,
target_user_membership_event: Option<&impl Event>, target_user_membership_event: Option<&E>,
sender: &UserId, sender: &UserId,
sender_membership_event: Option<&impl Event>, sender_membership_event: Option<&E>,
current_event: impl Event, current_event: &E,
current_third_party_invite: Option<&impl Event>, current_third_party_invite: Option<&E>,
power_levels_event: Option<&impl Event>, power_levels_event: Option<&E>,
join_rules_event: Option<&impl Event>, join_rules_event: Option<&E>,
user_for_join_auth: Option<&UserId>, user_for_join_auth: Option<&UserId>,
user_for_join_auth_membership: &MembershipState, user_for_join_auth_membership: &MembershipState,
create_room: &impl Event, create_room: &E,
) -> Result<bool> { ) -> Result<bool>
where
E: Event + Send + Sync,
for<'a> &'a E: Event + Send,
{
#[derive(Deserialize)] #[derive(Deserialize)]
struct GetThirdPartyInvite { struct GetThirdPartyInvite {
third_party_invite: Option<Raw<ThirdPartyInvite>>, third_party_invite: Option<Raw<ThirdPartyInvite>>,
@ -820,7 +851,7 @@ fn valid_membership_change(
/// ///
/// Does the event have the correct userId as its state_key if it's not the "" /// Does the event have the correct userId as its state_key if it's not the ""
/// state_key. /// state_key.
fn can_send_event(event: impl Event, ple: Option<impl Event>, user_level: Int) -> bool { fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int) -> bool {
let event_type_power_level = get_send_level(event.event_type(), event.state_key(), ple); let event_type_power_level = get_send_level(event.event_type(), event.state_key(), ple);
debug!( debug!(
@ -846,8 +877,8 @@ fn can_send_event(event: impl Event, ple: Option<impl Event>, user_level: Int) -
/// Confirm that the event sender has the required power levels. /// Confirm that the event sender has the required power levels.
fn check_power_levels( fn check_power_levels(
room_version: &RoomVersion, room_version: &RoomVersion,
power_event: impl Event, power_event: &impl Event,
previous_power_event: Option<impl Event>, previous_power_event: Option<&impl Event>,
user_level: Int, user_level: Int,
) -> Option<bool> { ) -> Option<bool> {
match power_event.state_key() { match power_event.state_key() {
@ -1010,7 +1041,7 @@ fn get_deserialize_levels(
/// given event. /// given event.
fn check_redaction( fn check_redaction(
_room_version: &RoomVersion, _room_version: &RoomVersion,
redaction_event: impl Event, redaction_event: &impl Event,
user_level: Int, user_level: Int,
redact_level: Int, redact_level: Int,
) -> Result<bool> { ) -> Result<bool> {
@ -1039,7 +1070,7 @@ fn check_redaction(
fn get_send_level( fn get_send_level(
e_type: &TimelineEventType, e_type: &TimelineEventType,
state_key: Option<&str>, state_key: Option<&str>,
power_lvl: Option<impl Event>, power_lvl: Option<&impl Event>,
) -> Int { ) -> Int {
power_lvl power_lvl
.and_then(|ple| { .and_then(|ple| {
@ -1062,7 +1093,7 @@ fn verify_third_party_invite(
target_user: Option<&UserId>, target_user: Option<&UserId>,
sender: &UserId, sender: &UserId,
tp_id: &ThirdPartyInvite, tp_id: &ThirdPartyInvite,
current_third_party_invite: Option<impl Event>, current_third_party_invite: Option<&impl Event>,
) -> bool { ) -> bool {
// 1. Check for user being banned happens before this is called // 1. Check for user being banned happens before this is called
// checking for mxid and token keys is done by ruma when deserializing // checking for mxid and token keys is done by ruma when deserializing
@ -1128,12 +1159,15 @@ mod tests {
}; };
use serde_json::value::to_raw_value as to_raw_json_value; use serde_json::value::to_raw_value as to_raw_json_value;
use crate::state_res::{ use crate::{
Event, EventTypeExt, RoomVersion, StateMap, matrix::{Event, EventTypeExt, Pdu as PduEvent},
event_auth::valid_membership_change, state_res::{
test_utils::{ RoomVersion, StateMap,
INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, PduEvent, alice, charlie, ella, event_id, event_auth::valid_membership_change,
member_content_ban, member_content_join, room_id, to_pdu_event, test_utils::{
INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, alice, charlie, ella, event_id,
member_content_ban, member_content_join, room_id, to_pdu_event,
},
}, },
}; };

View file

@ -37,7 +37,7 @@ pub use self::{
}; };
use crate::{ use crate::{
debug, debug_error, debug, debug_error,
matrix::{event::Event, pdu::StateKey}, matrix::{Event, StateKey},
trace, trace,
utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, WidebandExt}, utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, WidebandExt},
warn, warn,
@ -74,7 +74,7 @@ type Result<T, E = Error> = crate::Result<T, E>;
/// event is part of the same room. /// event is part of the same room.
//#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets, //#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets,
//#[tracing::instrument(level event_fetch))] //#[tracing::instrument(level event_fetch))]
pub async fn resolve<'a, E, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>( pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>(
room_version: &RoomVersionId, room_version: &RoomVersionId,
state_sets: Sets, state_sets: Sets,
auth_chain_sets: &'a [HashSet<OwnedEventId, Hasher>], auth_chain_sets: &'a [HashSet<OwnedEventId, Hasher>],
@ -83,14 +83,14 @@ pub async fn resolve<'a, E, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Exis
) -> Result<StateMap<OwnedEventId>> ) -> Result<StateMap<OwnedEventId>>
where where
Fetch: Fn(OwnedEventId) -> FetchFut + Sync, Fetch: Fn(OwnedEventId) -> FetchFut + Sync,
FetchFut: Future<Output = Option<E>> + Send, FetchFut: Future<Output = Option<Pdu>> + Send,
Exists: Fn(OwnedEventId) -> ExistsFut + Sync, Exists: Fn(OwnedEventId) -> ExistsFut + Sync,
ExistsFut: Future<Output = bool> + Send, ExistsFut: Future<Output = bool> + Send,
Sets: IntoIterator<IntoIter = SetIter> + Send, Sets: IntoIterator<IntoIter = SetIter> + Send,
SetIter: Iterator<Item = &'a StateMap<OwnedEventId>> + Clone + Send, SetIter: Iterator<Item = &'a StateMap<OwnedEventId>> + Clone + Send,
Hasher: BuildHasher + Send + Sync, Hasher: BuildHasher + Send + Sync,
E: Event + Clone + Send + Sync, Pdu: Event + Clone + Send + Sync,
for<'b> &'b E: Send, for<'b> &'b Pdu: Event + Send,
{ {
debug!("State resolution starting"); debug!("State resolution starting");
@ -221,6 +221,7 @@ where
let state_sets_iter = let state_sets_iter =
state_sets_iter.inspect(|_| state_set_count = state_set_count.saturating_add(1)); state_sets_iter.inspect(|_| state_set_count = state_set_count.saturating_add(1));
for (k, v) in state_sets_iter.flatten() { for (k, v) in state_sets_iter.flatten() {
occurrences occurrences
.entry(k) .entry(k)
@ -305,6 +306,7 @@ where
let pl = get_power_level_for_sender(&event_id, fetch_event) let pl = get_power_level_for_sender(&event_id, fetch_event)
.await .await
.ok()?; .ok()?;
Some((event_id, pl)) Some((event_id, pl))
}) })
.inspect(|(event_id, pl)| { .inspect(|(event_id, pl)| {
@ -522,6 +524,7 @@ where
Fut: Future<Output = Option<E>> + Send, Fut: Future<Output = Option<E>> + Send,
S: Stream<Item = &'a EventId> + Send + 'a, S: Stream<Item = &'a EventId> + Send + 'a,
E: Event + Clone + Send + Sync, E: Event + Clone + Send + Sync,
for<'b> &'b E: Event + Send,
{ {
debug!("starting iterative auth check"); debug!("starting iterative auth check");
@ -552,7 +555,7 @@ where
let auth_events = &auth_events; let auth_events = &auth_events;
let mut resolved_state = unconflicted_state; let mut resolved_state = unconflicted_state;
for event in &events_to_check { for event in events_to_check {
let state_key = event let state_key = event
.state_key() .state_key()
.ok_or_else(|| Error::InvalidPdu("State event had no state key".to_owned()))?; .ok_or_else(|| Error::InvalidPdu("State event had no state key".to_owned()))?;
@ -607,11 +610,15 @@ where
}); });
let fetch_state = |ty: &StateEventType, key: &str| { let fetch_state = |ty: &StateEventType, key: &str| {
future::ready(auth_state.get(&ty.with_state_key(key))) future::ready(
auth_state
.get(&ty.with_state_key(key))
.map(ToOwned::to_owned),
)
}; };
debug!("running auth check on {:?}", event.event_id());
let auth_result = let auth_result =
auth_check(room_version, &event, current_third_party.as_ref(), fetch_state).await; auth_check(room_version, &event, current_third_party, fetch_state).await;
match auth_result { match auth_result {
| Ok(true) => { | Ok(true) => {
@ -726,8 +733,12 @@ where
Fut: Future<Output = Option<E>> + Send, Fut: Future<Output = Option<E>> + Send,
E: Event + Send + Sync, E: Event + Send + Sync,
{ {
let mut room_id = None;
while let Some(sort_ev) = event { while let Some(sort_ev) = event {
debug!(event_id = sort_ev.event_id().as_str(), "mainline"); trace!(event_id = sort_ev.event_id().as_str(), "mainline");
if room_id.is_none() {
room_id = Some(sort_ev.room_id().to_owned());
}
let id = sort_ev.event_id(); let id = sort_ev.event_id();
if let Some(depth) = mainline_map.get(id) { if let Some(depth) = mainline_map.get(id) {
@ -746,7 +757,7 @@ where
} }
} }
} }
// Did not find a power level event so we default to zero warn!("could not find a power event in the mainline map for {room_id:?}, defaulting to zero depth");
Ok(0) Ok(0)
} }
@ -794,11 +805,11 @@ where
} }
} }
fn is_type_and_key(ev: impl Event, ev_type: &TimelineEventType, state_key: &str) -> bool { fn is_type_and_key(ev: &impl Event, ev_type: &TimelineEventType, state_key: &str) -> bool {
ev.event_type() == ev_type && ev.state_key() == Some(state_key) ev.event_type() == ev_type && ev.state_key() == Some(state_key)
} }
fn is_power_event(event: impl Event) -> bool { fn is_power_event(event: &impl Event) -> bool {
match event.event_type() { match event.event_type() {
| TimelineEventType::RoomPowerLevels | TimelineEventType::RoomPowerLevels
| TimelineEventType::RoomJoinRules | TimelineEventType::RoomJoinRules
@ -859,15 +870,19 @@ mod tests {
use serde_json::{json, value::to_raw_value as to_raw_json_value}; use serde_json::{json, value::to_raw_value as to_raw_json_value};
use super::{ use super::{
Event, EventTypeExt, StateMap, is_power_event, StateMap, is_power_event,
room_version::RoomVersion, room_version::RoomVersion,
test_utils::{ test_utils::{
INITIAL_EVENTS, PduEvent, TestStore, alice, bob, charlie, do_check, ella, event_id, INITIAL_EVENTS, TestStore, alice, bob, charlie, do_check, ella, event_id,
member_content_ban, member_content_join, room_id, to_init_pdu_event, to_pdu_event, member_content_ban, member_content_join, room_id, to_init_pdu_event, to_pdu_event,
zara, zara,
}, },
}; };
use crate::{debug, utils::stream::IterStream}; use crate::{
debug,
matrix::{Event, EventTypeExt, Pdu as PduEvent},
utils::stream::IterStream,
};
async fn test_event_sort() { async fn test_event_sort() {
use futures::future::ready; use futures::future::ready;

View file

@ -10,7 +10,6 @@ use ruma::{
UserId, event_id, UserId, event_id,
events::{ events::{
TimelineEventType, TimelineEventType,
pdu::{EventHash, Pdu, RoomV3Pdu},
room::{ room::{
join_rules::{JoinRule, RoomJoinRulesEventContent}, join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent}, member::{MembershipState, RoomMemberEventContent},
@ -23,17 +22,16 @@ use serde_json::{
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value}, value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
}; };
pub(crate) use self::event::PduEvent;
use super::auth_types_for_event; use super::auth_types_for_event;
use crate::{ use crate::{
Result, info, Result, info,
matrix::{Event, EventTypeExt, StateMap}, matrix::{Event, EventTypeExt, Pdu, StateMap, pdu::EventHash},
}; };
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0); static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
pub(crate) async fn do_check( pub(crate) async fn do_check(
events: &[PduEvent], events: &[Pdu],
edges: Vec<Vec<OwnedEventId>>, edges: Vec<Vec<OwnedEventId>>,
expected_state_ids: Vec<OwnedEventId>, expected_state_ids: Vec<OwnedEventId>,
) { ) {
@ -81,8 +79,8 @@ pub(crate) async fn do_check(
} }
} }
// event_id -> PduEvent // event_id -> Pdu
let mut event_map: HashMap<OwnedEventId, PduEvent> = HashMap::new(); let mut event_map: HashMap<OwnedEventId, Pdu> = HashMap::new();
// event_id -> StateMap<OwnedEventId> // event_id -> StateMap<OwnedEventId>
let mut state_at_event: HashMap<OwnedEventId, StateMap<OwnedEventId>> = HashMap::new(); let mut state_at_event: HashMap<OwnedEventId, StateMap<OwnedEventId>> = HashMap::new();
@ -265,7 +263,7 @@ impl<E: Event + Clone> TestStore<E> {
// A StateStore implementation for testing // A StateStore implementation for testing
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
impl TestStore<PduEvent> { impl TestStore<Pdu> {
pub(crate) fn set_up( pub(crate) fn set_up(
&mut self, &mut self,
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) { ) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
@ -390,7 +388,7 @@ pub(crate) fn to_init_pdu_event(
ev_type: TimelineEventType, ev_type: TimelineEventType,
state_key: Option<&str>, state_key: Option<&str>,
content: Box<RawJsonValue>, content: Box<RawJsonValue>,
) -> PduEvent { ) -> Pdu {
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst); let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
let id = if id.contains('$') { let id = if id.contains('$') {
id.to_owned() id.to_owned()
@ -398,24 +396,22 @@ pub(crate) fn to_init_pdu_event(
format!("${id}:foo") format!("${id}:foo")
}; };
let state_key = state_key.map(ToOwned::to_owned); Pdu {
PduEvent {
event_id: id.try_into().unwrap(), event_id: id.try_into().unwrap(),
rest: Pdu::RoomV3Pdu(RoomV3Pdu { room_id: room_id().to_owned(),
room_id: room_id().to_owned(), sender: sender.to_owned(),
sender: sender.to_owned(), origin_server_ts: ts.try_into().unwrap(),
origin_server_ts: MilliSecondsSinceUnixEpoch(ts.try_into().unwrap()), state_key: state_key.map(Into::into),
state_key, kind: ev_type,
kind: ev_type, content,
content, origin: None,
redacts: None, redacts: None,
unsigned: BTreeMap::new(), unsigned: None,
auth_events: vec![], auth_events: vec![],
prev_events: vec![], prev_events: vec![],
depth: uint!(0), depth: uint!(0),
hashes: EventHash::new("".to_owned()), hashes: EventHash { sha256: "".to_owned() },
signatures: ServerSignatures::default(), signatures: None,
}),
} }
} }
@ -427,7 +423,7 @@ pub(crate) fn to_pdu_event<S>(
content: Box<RawJsonValue>, content: Box<RawJsonValue>,
auth_events: &[S], auth_events: &[S],
prev_events: &[S], prev_events: &[S],
) -> PduEvent ) -> Pdu
where where
S: AsRef<str>, S: AsRef<str>,
{ {
@ -448,30 +444,28 @@ where
.map(event_id) .map(event_id)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let state_key = state_key.map(ToOwned::to_owned); Pdu {
PduEvent {
event_id: id.try_into().unwrap(), event_id: id.try_into().unwrap(),
rest: Pdu::RoomV3Pdu(RoomV3Pdu { room_id: room_id().to_owned(),
room_id: room_id().to_owned(), sender: sender.to_owned(),
sender: sender.to_owned(), origin_server_ts: ts.try_into().unwrap(),
origin_server_ts: MilliSecondsSinceUnixEpoch(ts.try_into().unwrap()), state_key: state_key.map(Into::into),
state_key, kind: ev_type,
kind: ev_type, content,
content, origin: None,
redacts: None, redacts: None,
unsigned: BTreeMap::new(), unsigned: None,
auth_events, auth_events,
prev_events, prev_events,
depth: uint!(0), depth: uint!(0),
hashes: EventHash::new("".to_owned()), hashes: EventHash { sha256: "".to_owned() },
signatures: ServerSignatures::default(), signatures: None,
}),
} }
} }
// all graphs start with these input events // all graphs start with these input events
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> { pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
vec![ vec![
to_pdu_event::<&EventId>( to_pdu_event::<&EventId>(
"CREATE", "CREATE",
@ -553,7 +547,7 @@ pub(crate) fn INITIAL_EVENTS() -> HashMap<OwnedEventId, PduEvent> {
// all graphs start with these input events // all graphs start with these input events
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, PduEvent> { pub(crate) fn INITIAL_EVENTS_CREATE_ROOM() -> HashMap<OwnedEventId, Pdu> {
vec![to_pdu_event::<&EventId>( vec![to_pdu_event::<&EventId>(
"CREATE", "CREATE",
alice(), alice(),
@ -575,111 +569,3 @@ pub(crate) fn INITIAL_EDGES() -> Vec<OwnedEventId> {
.map(event_id) .map(event_id)
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
pub(crate) mod event {
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId,
events::{TimelineEventType, pdu::Pdu},
};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue as RawJsonValue;
use crate::Event;
impl Event for PduEvent {
fn event_id(&self) -> &EventId { &self.event_id }
fn room_id(&self) -> &RoomId {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.room_id,
| Pdu::RoomV3Pdu(ev) => &ev.room_id,
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn sender(&self) -> &UserId {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.sender,
| Pdu::RoomV3Pdu(ev) => &ev.sender,
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn event_type(&self) -> &TimelineEventType {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.kind,
| Pdu::RoomV3Pdu(ev) => &ev.kind,
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn content(&self) -> &RawJsonValue {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => &ev.content,
| Pdu::RoomV3Pdu(ev) => &ev.content,
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn origin_server_ts(&self) -> MilliSecondsSinceUnixEpoch {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.origin_server_ts,
| Pdu::RoomV3Pdu(ev) => ev.origin_server_ts,
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn state_key(&self) -> Option<&str> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.state_key.as_deref(),
| Pdu::RoomV3Pdu(ev) => ev.state_key.as_deref(),
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
#[allow(refining_impl_trait)]
fn prev_events(&self) -> Box<dyn DoubleEndedIterator<Item = &EventId> + Send + '_> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) =>
Box::new(ev.prev_events.iter().map(|(id, _)| id.as_ref())),
| Pdu::RoomV3Pdu(ev) => Box::new(ev.prev_events.iter().map(AsRef::as_ref)),
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
#[allow(refining_impl_trait)]
fn auth_events(&self) -> Box<dyn DoubleEndedIterator<Item = &EventId> + Send + '_> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) =>
Box::new(ev.auth_events.iter().map(|(id, _)| id.as_ref())),
| Pdu::RoomV3Pdu(ev) => Box::new(ev.auth_events.iter().map(AsRef::as_ref)),
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
fn redacts(&self) -> Option<&EventId> {
match &self.rest {
| Pdu::RoomV1Pdu(ev) => ev.redacts.as_deref(),
| Pdu::RoomV3Pdu(ev) => ev.redacts.as_deref(),
#[allow(unreachable_patterns)]
| _ => unreachable!("new PDU version"),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[allow(clippy::exhaustive_structs)]
pub(crate) struct PduEvent {
pub(crate) event_id: OwnedEventId,
#[serde(flatten)]
pub(crate) rest: Pdu,
}
}

View file

@ -25,7 +25,9 @@ pub use info::{
rustc_flags_capture, version, rustc_flags_capture, version,
version::{name, version}, version::{name, version},
}; };
pub use matrix::{Event, EventTypeExt, PduCount, PduEvent, PduId, RoomVersion, pdu, state_res}; pub use matrix::{
Event, EventTypeExt, Pdu, PduCount, PduEvent, PduId, RoomVersion, pdu, state_res,
};
pub use server::Server; pub use server::Server;
pub use utils::{ctor, dtor, implement, result, result::Result}; pub use utils::{ctor, dtor, implement, result, result::Result};

View file

@ -44,6 +44,7 @@ impl Module {
.handle .handle
.as_ref() .as_ref()
.expect("backing library loaded by this instance"); .expect("backing library loaded by this instance");
// SAFETY: Calls dlsym(3) on unix platforms. This might not have to be unsafe // SAFETY: Calls dlsym(3) on unix platforms. This might not have to be unsafe
// if wrapped in libloading with_dlerror(). // if wrapped in libloading with_dlerror().
let sym = unsafe { handle.get::<Prototype>(cname.as_bytes()) }; let sym = unsafe { handle.get::<Prototype>(cname.as_bytes()) };

View file

@ -27,6 +27,7 @@ pub fn to_name(path: &OsStr) -> Result<String> {
.expect("path file stem") .expect("path file stem")
.to_str() .to_str()
.expect("name string"); .expect("name string");
let name = name.strip_prefix("lib").unwrap_or(name).to_owned(); let name = name.strip_prefix("lib").unwrap_or(name).to_owned();
Ok(name) Ok(name)

View file

@ -23,8 +23,10 @@ impl fmt::Display for Escape<'_> {
| '"' => "&quot;", | '"' => "&quot;",
| _ => continue, | _ => continue,
}; };
fmt.write_str(&pile_o_bits[last..i])?; fmt.write_str(&pile_o_bits[last..i])?;
fmt.write_str(s)?; fmt.write_str(s)?;
// NOTE: we only expect single byte characters here - which is fine as long as // NOTE: we only expect single byte characters here - which is fine as long as
// we only match single byte characters // we only match single byte characters
last = i.saturating_add(1); last = i.saturating_add(1);

View file

@ -1,4 +1,4 @@
use std::{fmt, str::FromStr}; use std::{fmt, marker::PhantomData, str::FromStr};
use ruma::{CanonicalJsonError, CanonicalJsonObject, canonical_json::try_from_json_map}; use ruma::{CanonicalJsonError, CanonicalJsonObject, canonical_json::try_from_json_map};
@ -11,25 +11,28 @@ use crate::Result;
pub fn to_canonical_object<T: serde::Serialize>( pub fn to_canonical_object<T: serde::Serialize>(
value: T, value: T,
) -> Result<CanonicalJsonObject, CanonicalJsonError> { ) -> Result<CanonicalJsonObject, CanonicalJsonError> {
use CanonicalJsonError::SerDe;
use serde::ser::Error; use serde::ser::Error;
match serde_json::to_value(value).map_err(CanonicalJsonError::SerDe)? { match serde_json::to_value(value).map_err(SerDe)? {
| serde_json::Value::Object(map) => try_from_json_map(map), | serde_json::Value::Object(map) => try_from_json_map(map),
| _ => | _ => Err(SerDe(serde_json::Error::custom("Value must be an object"))),
Err(CanonicalJsonError::SerDe(serde_json::Error::custom("Value must be an object"))),
} }
} }
pub fn deserialize_from_str< pub fn deserialize_from_str<'de, D, T, E>(deserializer: D) -> Result<T, D::Error>
'de, where
D: serde::de::Deserializer<'de>, D: serde::de::Deserializer<'de>,
T: FromStr<Err = E>, T: FromStr<Err = E>,
E: fmt::Display, E: fmt::Display,
>( {
deserializer: D, struct Visitor<T: FromStr<Err = E>, E>(PhantomData<T>);
) -> Result<T, D::Error> {
struct Visitor<T: FromStr<Err = E>, E>(std::marker::PhantomData<T>); impl<T, Err> serde::de::Visitor<'_> for Visitor<T, Err>
impl<T: FromStr<Err = Err>, Err: fmt::Display> serde::de::Visitor<'_> for Visitor<T, Err> { where
T: FromStr<Err = Err>,
Err: fmt::Display,
{
type Value = T; type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -43,5 +46,6 @@ pub fn deserialize_from_str<
v.parse().map_err(serde::de::Error::custom) v.parse().map_err(serde::de::Error::custom)
} }
} }
deserializer.deserialize_str(Visitor(std::marker::PhantomData))
deserializer.deserialize_str(Visitor(PhantomData))
} }

View file

@ -105,14 +105,11 @@ pub fn whole_unit(d: Duration) -> Unit {
| 86_400.. => Days(d.as_secs() / 86_400), | 86_400.. => Days(d.as_secs() / 86_400),
| 3_600..=86_399 => Hours(d.as_secs() / 3_600), | 3_600..=86_399 => Hours(d.as_secs() / 3_600),
| 60..=3_599 => Mins(d.as_secs() / 60), | 60..=3_599 => Mins(d.as_secs() / 60),
| _ => match d.as_micros() { | _ => match d.as_micros() {
| 1_000_000.. => Secs(d.as_secs()), | 1_000_000.. => Secs(d.as_secs()),
| 1_000..=999_999 => Millis(d.subsec_millis().into()), | 1_000..=999_999 => Millis(d.subsec_millis().into()),
| _ => match d.as_nanos() { | _ => match d.as_nanos() {
| 1_000.. => Micros(d.subsec_micros().into()), | 1_000.. => Micros(d.subsec_micros().into()),
| _ => Nanos(d.subsec_nanos().into()), | _ => Nanos(d.subsec_nanos().into()),
}, },
}, },

View file

@ -29,7 +29,7 @@ fn descriptor_cf_options(
set_table_options(&mut opts, &desc, cache)?; set_table_options(&mut opts, &desc, cache)?;
opts.set_min_write_buffer_number(1); opts.set_min_write_buffer_number(1);
opts.set_max_write_buffer_number(2); opts.set_max_write_buffer_number(3);
opts.set_write_buffer_size(desc.write_size); opts.set_write_buffer_size(desc.write_size);
opts.set_target_file_size_base(desc.file_size); opts.set_target_file_size_base(desc.file_size);

View file

@ -1,8 +1,6 @@
use std::{cmp, convert::TryFrom}; use conduwuit::{Config, Result};
use conduwuit::{Config, Result, utils};
use rocksdb::{Cache, DBRecoveryMode, Env, LogLevel, Options, statistics::StatsLevel}; use rocksdb::{Cache, DBRecoveryMode, Env, LogLevel, Options, statistics::StatsLevel};
use conduwuit::config::{parallelism_scaled_i32, parallelism_scaled_u32};
use super::{cf_opts::cache_size_f64, logger::handle as handle_log}; use super::{cf_opts::cache_size_f64, logger::handle as handle_log};
/// Create database-wide options suitable for opening the database. This also /// Create database-wide options suitable for opening the database. This also
@ -23,8 +21,8 @@ pub(crate) fn db_options(config: &Config, env: &Env, row_cache: &Cache) -> Resul
set_logging_defaults(&mut opts, config); set_logging_defaults(&mut opts, config);
// Processing // Processing
opts.set_max_background_jobs(num_threads::<i32>(config)?); opts.set_max_background_jobs(parallelism_scaled_i32(1));
opts.set_max_subcompactions(num_threads::<u32>(config)?); opts.set_max_subcompactions(parallelism_scaled_u32(1));
opts.set_avoid_unnecessary_blocking_io(true); opts.set_avoid_unnecessary_blocking_io(true);
opts.set_max_file_opening_threads(0); opts.set_max_file_opening_threads(0);
@ -126,15 +124,3 @@ fn set_logging_defaults(opts: &mut Options, config: &Config) {
opts.set_callback_logger(rocksdb_log_level, &handle_log); opts.set_callback_logger(rocksdb_log_level, &handle_log);
} }
} }
fn num_threads<T: TryFrom<usize>>(config: &Config) -> Result<T> {
const MIN_PARALLELISM: usize = 2;
let requested = if config.rocksdb_parallelism_threads != 0 {
config.rocksdb_parallelism_threads
} else {
utils::available_parallelism()
};
utils::math::try_into::<T, usize>(cmp::max(MIN_PARALLELISM, requested))
}

View file

@ -37,7 +37,6 @@ impl Watchers {
pub(crate) fn wake(&self, key: &[u8]) { pub(crate) fn wake(&self, key: &[u8]) {
let watchers = self.watchers.read().unwrap(); let watchers = self.watchers.read().unwrap();
let mut triggered = Vec::new(); let mut triggered = Vec::new();
for length in 0..=key.len() { for length in 0..=key.len() {
if watchers.contains_key(&key[..length]) { if watchers.contains_key(&key[..length]) {
triggered.push(&key[..length]); triggered.push(&key[..length]);

View file

@ -22,10 +22,12 @@ pub(crate) fn init(
let reload_handles = LogLevelReloadHandles::default(); let reload_handles = LogLevelReloadHandles::default();
let console_span_events = fmt_span::from_str(&config.log_span_events).unwrap_or_err(); let console_span_events = fmt_span::from_str(&config.log_span_events).unwrap_or_err();
let console_filter = EnvFilter::builder() let console_filter = EnvFilter::builder()
.with_regex(config.log_filter_regex) .with_regex(config.log_filter_regex)
.parse(&config.log) .parse(&config.log)
.map_err(|e| err!(Config("log", "{e}.")))?; .map_err(|e| err!(Config("log", "{e}.")))?;
let console_layer = fmt::Layer::new() let console_layer = fmt::Layer::new()
.with_span_events(console_span_events) .with_span_events(console_span_events)
.event_format(ConsoleFormat::new(config)) .event_format(ConsoleFormat::new(config))
@ -34,6 +36,7 @@ pub(crate) fn init(
let (console_reload_filter, console_reload_handle) = let (console_reload_filter, console_reload_handle) =
reload::Layer::new(console_filter.clone()); reload::Layer::new(console_filter.clone());
reload_handles.add("console", Box::new(console_reload_handle)); reload_handles.add("console", Box::new(console_reload_handle));
let cap_state = Arc::new(capture::State::new()); let cap_state = Arc::new(capture::State::new());
@ -47,8 +50,10 @@ pub(crate) fn init(
let subscriber = { let subscriber = {
let sentry_filter = EnvFilter::try_new(&config.sentry_filter) let sentry_filter = EnvFilter::try_new(&config.sentry_filter)
.map_err(|e| err!(Config("sentry_filter", "{e}.")))?; .map_err(|e| err!(Config("sentry_filter", "{e}.")))?;
let sentry_layer = sentry_tracing::layer(); let sentry_layer = sentry_tracing::layer();
let (sentry_reload_filter, sentry_reload_handle) = reload::Layer::new(sentry_filter); let (sentry_reload_filter, sentry_reload_handle) = reload::Layer::new(sentry_filter);
reload_handles.add("sentry", Box::new(sentry_reload_handle)); reload_handles.add("sentry", Box::new(sentry_reload_handle));
subscriber.with(sentry_layer.with_filter(sentry_reload_filter)) subscriber.with(sentry_layer.with_filter(sentry_reload_filter))
}; };
@ -58,12 +63,15 @@ pub(crate) fn init(
let (flame_layer, flame_guard) = if config.tracing_flame { let (flame_layer, flame_guard) = if config.tracing_flame {
let flame_filter = EnvFilter::try_new(&config.tracing_flame_filter) let flame_filter = EnvFilter::try_new(&config.tracing_flame_filter)
.map_err(|e| err!(Config("tracing_flame_filter", "{e}.")))?; .map_err(|e| err!(Config("tracing_flame_filter", "{e}.")))?;
let (flame_layer, flame_guard) = let (flame_layer, flame_guard) =
tracing_flame::FlameLayer::with_file(&config.tracing_flame_output_path) tracing_flame::FlameLayer::with_file(&config.tracing_flame_output_path)
.map_err(|e| err!(Config("tracing_flame_output_path", "{e}.")))?; .map_err(|e| err!(Config("tracing_flame_output_path", "{e}.")))?;
let flame_layer = flame_layer let flame_layer = flame_layer
.with_empty_samples(false) .with_empty_samples(false)
.with_filter(flame_filter); .with_filter(flame_filter);
(Some(flame_layer), Some(flame_guard)) (Some(flame_layer), Some(flame_guard))
} else { } else {
(None, None) (None, None)
@ -71,19 +79,24 @@ pub(crate) fn init(
let jaeger_filter = EnvFilter::try_new(&config.jaeger_filter) let jaeger_filter = EnvFilter::try_new(&config.jaeger_filter)
.map_err(|e| err!(Config("jaeger_filter", "{e}.")))?; .map_err(|e| err!(Config("jaeger_filter", "{e}.")))?;
let jaeger_layer = config.allow_jaeger.then(|| { let jaeger_layer = config.allow_jaeger.then(|| {
opentelemetry::global::set_text_map_propagator( opentelemetry::global::set_text_map_propagator(
opentelemetry_jaeger::Propagator::new(), opentelemetry_jaeger::Propagator::new(),
); );
let tracer = opentelemetry_jaeger::new_agent_pipeline() let tracer = opentelemetry_jaeger::new_agent_pipeline()
.with_auto_split_batch(true) .with_auto_split_batch(true)
.with_service_name(conduwuit_core::name()) .with_service_name(conduwuit_core::name())
.install_batch(opentelemetry_sdk::runtime::Tokio) .install_batch(opentelemetry_sdk::runtime::Tokio)
.expect("jaeger agent pipeline"); .expect("jaeger agent pipeline");
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
let (jaeger_reload_filter, jaeger_reload_handle) = let (jaeger_reload_filter, jaeger_reload_handle) =
reload::Layer::new(jaeger_filter.clone()); reload::Layer::new(jaeger_filter.clone());
reload_handles.add("jaeger", Box::new(jaeger_reload_handle)); reload_handles.add("jaeger", Box::new(jaeger_reload_handle));
Some(telemetry.with_filter(jaeger_reload_filter)) Some(telemetry.with_filter(jaeger_reload_filter))
}); });

View file

@ -51,7 +51,9 @@ pub(crate) async fn run(server: &Arc<Server>, starts: bool) -> Result<(bool, boo
}, },
}; };
} }
server.server.stopping.store(false, Ordering::Release); server.server.stopping.store(false, Ordering::Release);
let run = main_mod.get::<RunFuncProto>("run")?; let run = main_mod.get::<RunFuncProto>("run")?;
if let Err(error) = run(server if let Err(error) = run(server
.services .services
@ -64,7 +66,9 @@ pub(crate) async fn run(server: &Arc<Server>, starts: bool) -> Result<(bool, boo
error!("Running server: {error}"); error!("Running server: {error}");
return Err(error); return Err(error);
} }
let reloads = server.server.reloading.swap(false, Ordering::AcqRel); let reloads = server.server.reloading.swap(false, Ordering::AcqRel);
let stops = !reloads || stale(server).await? <= restart_thresh(); let stops = !reloads || stale(server).await? <= restart_thresh();
let starts = reloads && stops; let starts = reloads && stops;
if stops { if stops {

View file

@ -35,11 +35,13 @@ fn options(config: &Config) -> ClientOptions {
.expect("init_sentry should only be called if sentry is enabled and this is not None") .expect("init_sentry should only be called if sentry is enabled and this is not None")
.as_str(); .as_str();
let server_name = config
.sentry_send_server_name
.then(|| config.server_name.to_string().into());
ClientOptions { ClientOptions {
dsn: Some(Dsn::from_str(dsn).expect("sentry_endpoint must be a valid URL")), dsn: Some(Dsn::from_str(dsn).expect("sentry_endpoint must be a valid URL")),
server_name: config server_name,
.sentry_send_server_name
.then(|| config.server_name.to_string().into()),
traces_sample_rate: config.sentry_traces_sample_rate, traces_sample_rate: config.sentry_traces_sample_rate,
debug: cfg!(debug_assertions), debug: cfg!(debug_assertions),
release: sentry::release_name!(), release: sentry::release_name!(),

View file

@ -98,8 +98,8 @@ async fn execute(
fn handle_result(method: &Method, uri: &Uri, result: Response) -> Result<Response, StatusCode> { fn handle_result(method: &Method, uri: &Uri, result: Response) -> Result<Response, StatusCode> {
let status = result.status(); let status = result.status();
let reason = status.canonical_reason().unwrap_or("Unknown Reason");
let code = status.as_u16(); let code = status.as_u16();
let reason = status.canonical_reason().unwrap_or("Unknown Reason");
if status.is_server_error() { if status.is_server_error() {
error!(method = ?method, uri = ?uri, "{code} {reason}"); error!(method = ?method, uri = ?uri, "{code} {reason}");

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