mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-11-13 12:16:54 +00:00
refactor: Use a shared helper function for submitting state events at room creation
On room creation, A list of initial state events may be provided that should be submitted to the room after creation. These events should be treated as any other state event and submitted to the same checks. With this change, state events submitted on room creation will be submitted through the same helper as those through the usual endpoint. The submission helper is moved to the timeline service to make it available everywhere. This will be useful for implementing MSC4140 (issue #903).
This commit is contained in:
1 parent
8cde2ad0cc
commit
7f8c2aedc8
5 files changed
+388
-304
No files matched your search
@@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Result, debug, debug_info, err, info,
|
||||
Err, Error, Result, debug, debug_info, err, info,
|
||||
matrix::{StateKey, pdu::PartialPdu},
|
||||
trace, warn,
|
||||
};
|
||||
@@ -11,10 +11,13 @@ use futures::FutureExt;
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, Int, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId,
|
||||
OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId,
|
||||
api::client::room::{self, create_room},
|
||||
api::{
|
||||
client::room::{self, create_room},
|
||||
error::ErrorKind::Forbidden,
|
||||
},
|
||||
assign,
|
||||
events::{
|
||||
TimelineEventType,
|
||||
AnyStateEventContent, StateEventType, TimelineEventType,
|
||||
room::{
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
create::RoomCreateEventContent,
|
||||
@@ -512,30 +515,42 @@ pub(crate) async fn create_room_route(
|
||||
|
||||
// 7. Events listed in initial_state
|
||||
for event in &body.initial_state {
|
||||
let mut partial_pdu = event
|
||||
.deserialize_as_unchecked::<PartialPdu>()
|
||||
.map_err(|e| {
|
||||
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
|
||||
let event_type: StateEventType = event.get_field("type")?.ok_or_else(|| {
|
||||
err!(Request(InvalidParam(warn!(
|
||||
r#"Invalid initial state event: missing "type" field "#
|
||||
))))
|
||||
})?;
|
||||
|
||||
let content: Raw<AnyStateEventContent> =
|
||||
event.get_field("content")?.ok_or_else(|| {
|
||||
err!(Request(InvalidParam(warn!(
|
||||
r#"Invalid initial state event: missing "content" field "#
|
||||
))))
|
||||
})?;
|
||||
|
||||
debug_info!("Room creation initial state event: {event:?}");
|
||||
let state_key: String = event.get_field("state_key")?.unwrap_or_default();
|
||||
|
||||
// Implicit state key defaults to ""
|
||||
partial_pdu.state_key.get_or_insert_with(StateKey::new);
|
||||
|
||||
// Silently skip encryption events if they are not allowed
|
||||
if partial_pdu.event_type == TimelineEventType::RoomEncryption
|
||||
&& !services.config.allow_encryption
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
services
|
||||
match services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(partial_pdu, sender_user, Some(&room_id), &state_lock)
|
||||
.send_state_event_for_key(
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
&event_type,
|
||||
&content,
|
||||
&state_key,
|
||||
None,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
| Err(Error::Request(Forbidden, ..)) => {
|
||||
// Silently skip forbidden events
|
||||
continue;
|
||||
},
|
||||
| r => r,
|
||||
}?;
|
||||
}
|
||||
|
||||
// 8. Events implied by name and topic
|
||||
|
||||
+26
-281
@@ -8,22 +8,11 @@ use conduwuit::{
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, TryStreamExt};
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId,
|
||||
RoomId,
|
||||
api::client::state::{
|
||||
get_state_event_for_key::{self, v3::StateEventFormat},
|
||||
get_state_events, send_state_event,
|
||||
},
|
||||
events::{
|
||||
AnyStateEventContent, StateEventType,
|
||||
room::{
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
server_acl::RoomServerAclEventContent,
|
||||
},
|
||||
},
|
||||
serde::Raw,
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
|
||||
@@ -47,21 +36,31 @@ pub(crate) async fn send_state_event_for_key_route(
|
||||
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
||||
}
|
||||
|
||||
let event_id = send_state_event_for_key_helper(
|
||||
&services,
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&body.event_type,
|
||||
&body.body.body,
|
||||
&body.state_key,
|
||||
if body.identity.is_appservice() {
|
||||
body.timestamp
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
let state_lock = services
|
||||
.rooms
|
||||
.state
|
||||
.mutex
|
||||
.lock::<RoomId>(&body.room_id)
|
||||
.await;
|
||||
|
||||
let event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.send_state_event_for_key(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&state_lock,
|
||||
&body.event_type,
|
||||
&body.body.body,
|
||||
&body.state_key,
|
||||
if body.identity.is_appservice() {
|
||||
body.timestamp
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
Ok(send_state_event::v3::Response::new(event_id))
|
||||
}
|
||||
@@ -185,257 +184,3 @@ pub(crate) async fn get_state_events_for_empty_key_route(
|
||||
.await
|
||||
.map(RumaResponse)
|
||||
}
|
||||
|
||||
async fn send_state_event_for_key_helper(
|
||||
services: &Services,
|
||||
sender: &UserId,
|
||||
room_id: &RoomId,
|
||||
event_type: &StateEventType,
|
||||
json: &Raw<AnyStateEventContent>,
|
||||
state_key: &str,
|
||||
timestamp: Option<MilliSecondsSinceUnixEpoch>,
|
||||
) -> Result<OwnedEventId> {
|
||||
let json: &mut Raw<AnyStateEventContent> = &mut json.clone();
|
||||
allowed_to_send_state_event(services, room_id, event_type, state_key, json).await?;
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id).await;
|
||||
let event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PartialPdu {
|
||||
event_type: event_type.to_string().into(),
|
||||
content: serde_json::from_str(json.json().get())?,
|
||||
state_key: Some(state_key.into()),
|
||||
timestamp,
|
||||
..Default::default()
|
||||
},
|
||||
sender,
|
||||
Some(room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(event_id)
|
||||
}
|
||||
|
||||
async fn allowed_to_send_state_event(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
event_type: &StateEventType,
|
||||
state_key: &str,
|
||||
json: &mut Raw<AnyStateEventContent>,
|
||||
) -> Result {
|
||||
match event_type {
|
||||
| StateEventType::RoomCreate => {
|
||||
return Err!(Request(BadJson(debug_warn!(
|
||||
%room_id,
|
||||
"You cannot update m.room.create after a room has been created."
|
||||
))));
|
||||
},
|
||||
| StateEventType::RoomServerAcl => {
|
||||
// prevents common ACL paw-guns as ACL management is difficult and prone to
|
||||
// irreversible mistakes
|
||||
match json.deserialize_as_unchecked::<RoomServerAclEventContent>() {
|
||||
| Ok(acl_content) => {
|
||||
let allow_has_wildcard = acl_content.allow.iter().any(|entry| entry == "*");
|
||||
let deny_has_wildcard = acl_content.deny.iter().any(|entry| entry == "*");
|
||||
let allow_has_server = acl_content
|
||||
.allow
|
||||
.iter()
|
||||
.any(|entry| entry == services.globals.server_name().as_str());
|
||||
|
||||
if acl_content.allow.is_empty() {
|
||||
return Err!(Request(BadJson(debug_warn!(
|
||||
%room_id,
|
||||
"Sending an ACL event with an empty allow key will permanently \
|
||||
brick the room for non-conduwuit's as this equates to no servers \
|
||||
being allowed to participate in this room."
|
||||
))));
|
||||
}
|
||||
|
||||
if allow_has_wildcard && deny_has_wildcard {
|
||||
return Err!(Request(BadJson(debug_warn!(
|
||||
%room_id,
|
||||
"Sending an ACL event with a deny and allow key value of \"*\" will \
|
||||
permanently brick the room for non-conduwuit's as this equates to \
|
||||
no servers being allowed to participate in this room."
|
||||
))));
|
||||
}
|
||||
|
||||
if deny_has_wildcard
|
||||
&& !acl_content.is_allowed(services.globals.server_name())
|
||||
&& !allow_has_server
|
||||
{
|
||||
return Err!(Request(BadJson(debug_warn!(
|
||||
%room_id,
|
||||
"Sending an ACL event with a deny key value of \"*\" and without \
|
||||
your own server name in the allow key will result in you being \
|
||||
unable to participate in this room."
|
||||
))));
|
||||
}
|
||||
|
||||
if !allow_has_wildcard
|
||||
&& !acl_content.is_allowed(services.globals.server_name())
|
||||
&& !allow_has_server
|
||||
{
|
||||
return Err!(Request(BadJson(debug_warn!(
|
||||
%room_id,
|
||||
"Sending an ACL event for an allow key without \"*\" and without \
|
||||
your own server name in the allow key will result in you being \
|
||||
unable to participate in this room."
|
||||
))));
|
||||
}
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(BadJson(debug_warn!(
|
||||
"Room server ACL event is invalid: {e}"
|
||||
))));
|
||||
},
|
||||
}
|
||||
},
|
||||
| StateEventType::RoomEncryption =>
|
||||
// Forbid m.room.encryption if encryption is disabled
|
||||
if !services.config.allow_encryption {
|
||||
return Err!(Request(Forbidden("Encryption is disabled on this homeserver.")));
|
||||
},
|
||||
| StateEventType::RoomJoinRules => {
|
||||
// admin room is a sensitive room, it should not ever be made public
|
||||
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
|
||||
if admin_room_id == room_id {
|
||||
match json.deserialize_as_unchecked::<RoomJoinRulesEventContent>() {
|
||||
| Ok(join_rule) =>
|
||||
if join_rule.join_rule == JoinRule::Public {
|
||||
return Err!(Request(Forbidden(
|
||||
"Admin room is a sensitive room, it cannot be made public"
|
||||
)));
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(BadJson(debug_warn!(
|
||||
"Room join rules event is invalid: {e}"
|
||||
))));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
| StateEventType::RoomHistoryVisibility => {
|
||||
// admin room is a sensitive room, it should not ever be made world readable
|
||||
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
|
||||
match json.deserialize_as_unchecked::<RoomHistoryVisibilityEventContent>() {
|
||||
| Ok(visibility_content) => {
|
||||
if admin_room_id == room_id
|
||||
&& visibility_content.history_visibility
|
||||
== HistoryVisibility::WorldReadable
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"Admin room is a sensitive room, it cannot be made world \
|
||||
readable (public room history)."
|
||||
)));
|
||||
}
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(BadJson(debug_warn!(
|
||||
"Room history visibility event is invalid: {e}"
|
||||
))));
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
| StateEventType::RoomCanonicalAlias => {
|
||||
match json.deserialize_as_unchecked::<RoomCanonicalAliasEventContent>() {
|
||||
| Ok(canonical_alias_content) => {
|
||||
let mut aliases = canonical_alias_content.alt_aliases.clone();
|
||||
|
||||
if let Some(alias) = canonical_alias_content.alias {
|
||||
aliases.push(alias);
|
||||
}
|
||||
|
||||
for alias in aliases {
|
||||
let (alias_room_id, _) = services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_alias(&alias)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
err!(Request(Unknown("Failed resolving alias \"{alias}\": {e}")))
|
||||
})?;
|
||||
|
||||
if alias_room_id != room_id {
|
||||
return Err!(Request(BadAlias(
|
||||
"Room alias {alias} does not belong to room {room_id}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(InvalidParam(debug_warn!(
|
||||
"Room canonical alias event is invalid: {e}"
|
||||
))));
|
||||
},
|
||||
}
|
||||
},
|
||||
| StateEventType::RoomMember =>
|
||||
match json.deserialize_as_unchecked::<RoomMemberEventContent>() {
|
||||
| Ok(mut membership_content) => {
|
||||
let Ok(state_key) = UserId::parse(state_key) else {
|
||||
return Err!(Request(BadJson(
|
||||
"Membership event has invalid or non-existent state key"
|
||||
)));
|
||||
};
|
||||
|
||||
if let Some(authorising_user) =
|
||||
membership_content.join_authorized_via_users_server
|
||||
{
|
||||
// join_authorized_via_users_server must be thrown away, if user is
|
||||
// already a member of the room.
|
||||
if services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(&state_key, room_id)
|
||||
.await
|
||||
{
|
||||
membership_content.join_authorized_via_users_server = None;
|
||||
*json = Raw::<AnyStateEventContent>::from_json_string(
|
||||
serde_json::to_string(&membership_content)?,
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if membership_content.membership != MembershipState::Join {
|
||||
return Err!(Request(BadJson(
|
||||
"join_authorised_via_users_server is only for member joins"
|
||||
)));
|
||||
}
|
||||
|
||||
if !services.globals.user_is_local(&authorising_user) {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Authorising user {authorising_user} does not belong to this \
|
||||
homeserver"
|
||||
)));
|
||||
}
|
||||
|
||||
if !services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(&authorising_user, room_id)
|
||||
.await
|
||||
{
|
||||
return Err!(Request(InvalidParam(
|
||||
"Authorising user {authorising_user} is not in the room, they \
|
||||
cannot authorise the join."
|
||||
)));
|
||||
}
|
||||
}
|
||||
},
|
||||
| Err(e) => {
|
||||
return Err!(Request(BadJson(
|
||||
"Membership content must have a valid JSON body with at least a valid \
|
||||
membership state: {e}"
|
||||
)));
|
||||
},
|
||||
},
|
||||
| _ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
use ruma::{
|
||||
events::{AnyStateEventContent, room::member::RoomMemberEventContent},
|
||||
serde::Raw,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user