mirror of
https://gitlab.com/famedly/conduit.git
synced 2025-08-11 17:50:59 +00:00
refactor: use RoomVersionRules instead of matching against RoomVersionId
This commit is contained in:
parent
b631621f8c
commit
a8fa237fad
7 changed files with 135 additions and 219 deletions
|
@ -23,7 +23,7 @@ use ruma::{
|
||||||
},
|
},
|
||||||
int,
|
int,
|
||||||
serde::JsonObject,
|
serde::JsonObject,
|
||||||
CanonicalJsonObject, OwnedRoomAliasId, RoomAliasId, RoomId, RoomVersionId,
|
CanonicalJsonObject, OwnedRoomAliasId, RoomAliasId, RoomId,
|
||||||
};
|
};
|
||||||
use serde_json::{json, value::to_raw_value};
|
use serde_json::{json, value::to_raw_value};
|
||||||
use std::{cmp::max, collections::BTreeMap, sync::Arc};
|
use std::{cmp::max, collections::BTreeMap, sync::Arc};
|
||||||
|
@ -137,6 +137,10 @@ pub async fn create_room_route(
|
||||||
}
|
}
|
||||||
None => services().globals.default_room_version(),
|
None => services().globals.default_room_version(),
|
||||||
};
|
};
|
||||||
|
let rules = room_version
|
||||||
|
.rules()
|
||||||
|
.expect("Supported room version must have rules.")
|
||||||
|
.authorization;
|
||||||
|
|
||||||
let content = match &body.creation_content {
|
let content = match &body.creation_content {
|
||||||
Some(content) => {
|
Some(content) => {
|
||||||
|
@ -144,17 +148,7 @@ pub async fn create_room_route(
|
||||||
.deserialize_as::<CanonicalJsonObject>()
|
.deserialize_as::<CanonicalJsonObject>()
|
||||||
.expect("Invalid creation content");
|
.expect("Invalid creation content");
|
||||||
|
|
||||||
match room_version {
|
if !rules.use_room_create_sender {
|
||||||
RoomVersionId::V1
|
|
||||||
| RoomVersionId::V2
|
|
||||||
| RoomVersionId::V3
|
|
||||||
| RoomVersionId::V4
|
|
||||||
| RoomVersionId::V5
|
|
||||||
| RoomVersionId::V6
|
|
||||||
| RoomVersionId::V7
|
|
||||||
| RoomVersionId::V8
|
|
||||||
| RoomVersionId::V9
|
|
||||||
| RoomVersionId::V10 => {
|
|
||||||
content.insert(
|
content.insert(
|
||||||
"creator".into(),
|
"creator".into(),
|
||||||
json!(&sender_user).try_into().map_err(|_| {
|
json!(&sender_user).try_into().map_err(|_| {
|
||||||
|
@ -162,9 +156,6 @@ pub async fn create_room_route(
|
||||||
})?,
|
})?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
RoomVersionId::V11 => {} // V11 removed the "creator" key
|
|
||||||
_ => unreachable!("Validity of room version already checked"),
|
|
||||||
}
|
|
||||||
|
|
||||||
content.insert(
|
content.insert(
|
||||||
"room_version".into(),
|
"room_version".into(),
|
||||||
|
@ -175,19 +166,10 @@ pub async fn create_room_route(
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let content = match room_version {
|
let content = if rules.use_room_create_sender {
|
||||||
RoomVersionId::V1
|
RoomCreateEventContent::new_v11()
|
||||||
| RoomVersionId::V2
|
} else {
|
||||||
| RoomVersionId::V3
|
RoomCreateEventContent::new_v1(sender_user.clone())
|
||||||
| RoomVersionId::V4
|
|
||||||
| RoomVersionId::V5
|
|
||||||
| RoomVersionId::V6
|
|
||||||
| RoomVersionId::V7
|
|
||||||
| RoomVersionId::V8
|
|
||||||
| RoomVersionId::V9
|
|
||||||
| RoomVersionId::V10 => RoomCreateEventContent::new_v1(sender_user.clone()),
|
|
||||||
RoomVersionId::V11 => RoomCreateEventContent::new_v11(),
|
|
||||||
_ => unreachable!("Validity of room version already checked"),
|
|
||||||
};
|
};
|
||||||
let mut content = serde_json::from_str::<CanonicalJsonObject>(
|
let mut content = serde_json::from_str::<CanonicalJsonObject>(
|
||||||
to_raw_value(&content)
|
to_raw_value(&content)
|
||||||
|
@ -602,6 +584,11 @@ pub async fn upgrade_room_route(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rules = body
|
||||||
|
.new_version
|
||||||
|
.rules()
|
||||||
|
.expect("Supported room version must have rules.");
|
||||||
|
|
||||||
// Create a replacement room
|
// Create a replacement room
|
||||||
let replacement_room = RoomId::new(services().globals.server_name());
|
let replacement_room = RoomId::new(services().globals.server_name());
|
||||||
services()
|
services()
|
||||||
|
@ -676,17 +663,9 @@ pub async fn upgrade_room_route(
|
||||||
));
|
));
|
||||||
|
|
||||||
// Send a m.room.create event containing a predecessor field and the applicable room_version
|
// Send a m.room.create event containing a predecessor field and the applicable room_version
|
||||||
match body.new_version {
|
if rules.authorization.use_room_create_sender {
|
||||||
RoomVersionId::V1
|
create_event_content.remove("creator");
|
||||||
| RoomVersionId::V2
|
} else {
|
||||||
| RoomVersionId::V3
|
|
||||||
| RoomVersionId::V4
|
|
||||||
| RoomVersionId::V5
|
|
||||||
| RoomVersionId::V6
|
|
||||||
| RoomVersionId::V7
|
|
||||||
| RoomVersionId::V8
|
|
||||||
| RoomVersionId::V9
|
|
||||||
| RoomVersionId::V10 => {
|
|
||||||
create_event_content.insert(
|
create_event_content.insert(
|
||||||
"creator".into(),
|
"creator".into(),
|
||||||
json!(&sender_user).try_into().map_err(|_| {
|
json!(&sender_user).try_into().map_err(|_| {
|
||||||
|
@ -694,12 +673,7 @@ pub async fn upgrade_room_route(
|
||||||
})?,
|
})?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
RoomVersionId::V11 => {
|
|
||||||
// "creator" key no longer exists in V11 rooms
|
|
||||||
create_event_content.remove("creator");
|
|
||||||
}
|
|
||||||
_ => unreachable!("Validity of room version already checked"),
|
|
||||||
}
|
|
||||||
create_event_content.insert(
|
create_event_content.insert(
|
||||||
"room_version".into(),
|
"room_version".into(),
|
||||||
json!(&body.new_version)
|
json!(&body.new_version)
|
||||||
|
|
|
@ -2021,16 +2021,11 @@ fn user_can_perform_restricted_join(
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
if matches!(
|
let rules = room_version_id
|
||||||
room_version_id,
|
.rules()
|
||||||
RoomVersionId::V1
|
.expect("Supported room version must have rules.")
|
||||||
| RoomVersionId::V2
|
.authorization;
|
||||||
| RoomVersionId::V3
|
if !rules.restricted_join_rule {
|
||||||
| RoomVersionId::V4
|
|
||||||
| RoomVersionId::V5
|
|
||||||
| RoomVersionId::V6
|
|
||||||
| RoomVersionId::V7
|
|
||||||
) {
|
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1693,19 +1693,14 @@ impl Service {
|
||||||
services().users.create(conduit_user, None)?;
|
services().users.create(conduit_user, None)?;
|
||||||
|
|
||||||
let room_version = services().globals.default_room_version();
|
let room_version = services().globals.default_room_version();
|
||||||
let mut content = match room_version {
|
let rules = room_version
|
||||||
RoomVersionId::V1
|
.rules()
|
||||||
| RoomVersionId::V2
|
.expect("Supported room version must have rules.")
|
||||||
| RoomVersionId::V3
|
.authorization;
|
||||||
| RoomVersionId::V4
|
let mut content = if rules.use_room_create_sender {
|
||||||
| RoomVersionId::V5
|
RoomCreateEventContent::new_v11()
|
||||||
| RoomVersionId::V6
|
} else {
|
||||||
| RoomVersionId::V7
|
RoomCreateEventContent::new_v1(conduit_user.to_owned())
|
||||||
| RoomVersionId::V8
|
|
||||||
| RoomVersionId::V9
|
|
||||||
| RoomVersionId::V10 => RoomCreateEventContent::new_v1(conduit_user.to_owned()),
|
|
||||||
RoomVersionId::V11 => RoomCreateEventContent::new_v11(),
|
|
||||||
_ => unreachable!("Validity of room version already checked"),
|
|
||||||
};
|
};
|
||||||
content.federate = true;
|
content.federate = true;
|
||||||
content.predecessor = None;
|
content.predecessor = None;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
mod data;
|
mod data;
|
||||||
pub use data::{Data, SigningKeys};
|
pub use data::{Data, SigningKeys};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
serde::Base64, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedEventId, OwnedRoomAliasId,
|
room_version_rules::RoomVersionRules, serde::Base64, MilliSecondsSinceUnixEpoch, OwnedDeviceId,
|
||||||
OwnedRoomId, OwnedServerName, OwnedUserId, RoomAliasId,
|
OwnedEventId, OwnedRoomAliasId, OwnedRoomId, OwnedServerName, OwnedUserId, RoomAliasId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::api::server_server::DestinationResponse;
|
use crate::api::server_server::DestinationResponse;
|
||||||
|
@ -427,11 +427,11 @@ impl Service {
|
||||||
&self,
|
&self,
|
||||||
keys: BTreeMap<String, SigningKeys>,
|
keys: BTreeMap<String, SigningKeys>,
|
||||||
timestamp: MilliSecondsSinceUnixEpoch,
|
timestamp: MilliSecondsSinceUnixEpoch,
|
||||||
room_version_id: &RoomVersionId,
|
rules: &RoomVersionRules,
|
||||||
) -> BTreeMap<String, BTreeMap<String, Base64>> {
|
) -> BTreeMap<String, BTreeMap<String, Base64>> {
|
||||||
keys.into_iter()
|
keys.into_iter()
|
||||||
.filter_map(|(server, keys)| {
|
.filter_map(|(server, keys)| {
|
||||||
self.filter_keys_single_server(keys, timestamp, room_version_id)
|
self.filter_keys_single_server(keys, timestamp, rules)
|
||||||
.map(|keys| (server, keys))
|
.map(|keys| (server, keys))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
@ -443,15 +443,12 @@ impl Service {
|
||||||
&self,
|
&self,
|
||||||
keys: SigningKeys,
|
keys: SigningKeys,
|
||||||
timestamp: MilliSecondsSinceUnixEpoch,
|
timestamp: MilliSecondsSinceUnixEpoch,
|
||||||
room_version_id: &RoomVersionId,
|
rules: &RoomVersionRules,
|
||||||
) -> Option<BTreeMap<String, Base64>> {
|
) -> Option<BTreeMap<String, Base64>> {
|
||||||
if keys.valid_until_ts > timestamp
|
if keys.valid_until_ts > timestamp
|
||||||
// valid_until_ts MUST be ignored in room versions 1, 2, 3, and 4.
|
// valid_until_ts MUST be ignored in room versions 1, 2, 3, and 4.
|
||||||
// https://spec.matrix.org/v1.10/server-server-api/#get_matrixkeyv2server
|
// https://spec.matrix.org/v1.10/server-server-api/#get_matrixkeyv2server
|
||||||
|| matches!(room_version_id, RoomVersionId::V1
|
|| !rules.enforce_key_validity
|
||||||
| RoomVersionId::V2
|
|
||||||
| RoomVersionId::V4
|
|
||||||
| RoomVersionId::V3)
|
|
||||||
{
|
{
|
||||||
// Given that either the room version allows stale keys, or the valid_until_ts is
|
// Given that either the room version allows stale keys, or the valid_until_ts is
|
||||||
// in the future, all verify_keys are valid
|
// in the future, all verify_keys are valid
|
||||||
|
|
|
@ -34,7 +34,7 @@ use ruma::{
|
||||||
room_version_rules::{AuthorizationRules, RoomVersionRules},
|
room_version_rules::{AuthorizationRules, RoomVersionRules},
|
||||||
state_res::{self, StateMap},
|
state_res::{self, StateMap},
|
||||||
uint, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch,
|
uint, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch,
|
||||||
OwnedServerName, OwnedServerSigningKeyId, RoomId, RoomVersionId, ServerName,
|
OwnedServerName, OwnedServerSigningKeyId, RoomId, ServerName,
|
||||||
};
|
};
|
||||||
use serde_json::value::RawValue as RawJsonValue;
|
use serde_json::value::RawValue as RawJsonValue;
|
||||||
use tokio::sync::{RwLock, RwLockWriteGuard, Semaphore};
|
use tokio::sync::{RwLock, RwLockWriteGuard, Semaphore};
|
||||||
|
@ -373,7 +373,7 @@ impl Service {
|
||||||
let filtered_keys = services().globals.filter_keys_server_map(
|
let filtered_keys = services().globals.filter_keys_server_map(
|
||||||
pkey_map,
|
pkey_map,
|
||||||
origin_server_ts,
|
origin_server_ts,
|
||||||
room_version_id,
|
&room_version_rules,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut val =
|
let mut val =
|
||||||
|
@ -846,29 +846,7 @@ impl Service {
|
||||||
)
|
)
|
||||||
.is_err()
|
.is_err()
|
||||||
|| incoming_pdu.kind == TimelineEventType::RoomRedaction
|
|| incoming_pdu.kind == TimelineEventType::RoomRedaction
|
||||||
&& match room_version_id {
|
&& if room_version_rules.redaction.content_field_redacts {
|
||||||
RoomVersionId::V1
|
|
||||||
| RoomVersionId::V2
|
|
||||||
| RoomVersionId::V3
|
|
||||||
| RoomVersionId::V4
|
|
||||||
| RoomVersionId::V5
|
|
||||||
| RoomVersionId::V6
|
|
||||||
| RoomVersionId::V7
|
|
||||||
| RoomVersionId::V8
|
|
||||||
| RoomVersionId::V9
|
|
||||||
| RoomVersionId::V10 => {
|
|
||||||
if let Some(redact_id) = &incoming_pdu.redacts {
|
|
||||||
!services().rooms.state_accessor.user_can_redact(
|
|
||||||
redact_id,
|
|
||||||
&incoming_pdu.sender,
|
|
||||||
&incoming_pdu.room_id,
|
|
||||||
true,
|
|
||||||
)?
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RoomVersionId::V11 => {
|
|
||||||
let content = serde_json::from_str::<RoomRedactionEventContent>(
|
let content = serde_json::from_str::<RoomRedactionEventContent>(
|
||||||
incoming_pdu.content.get(),
|
incoming_pdu.content.get(),
|
||||||
)
|
)
|
||||||
|
@ -884,10 +862,15 @@ impl Service {
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
} else if let Some(redact_id) = &incoming_pdu.redacts {
|
||||||
_ => {
|
!services().rooms.state_accessor.user_can_redact(
|
||||||
unreachable!("Validity of room version already checked")
|
redact_id,
|
||||||
}
|
&incoming_pdu.sender,
|
||||||
|
&incoming_pdu.room_id,
|
||||||
|
true,
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
// 14. Use state resolution to find new room state
|
// 14. Use state resolution to find new room state
|
||||||
|
|
|
@ -722,10 +722,9 @@ async fn validate_and_add_event_id(
|
||||||
|
|
||||||
let unfiltered_keys = (*pub_key_map.read().await).clone();
|
let unfiltered_keys = (*pub_key_map.read().await).clone();
|
||||||
|
|
||||||
let keys =
|
let keys = services()
|
||||||
services()
|
|
||||||
.globals
|
.globals
|
||||||
.filter_keys_server_map(unfiltered_keys, origin_server_ts, room_version);
|
.filter_keys_server_map(unfiltered_keys, origin_server_ts, rules);
|
||||||
|
|
||||||
if let Err(e) = ruma::signatures::verify_event(&keys, &value, rules) {
|
if let Err(e) = ruma::signatures::verify_event(&keys, &value, rules) {
|
||||||
warn!("Event {} failed verification {:?} {}", event_id, pdu, e);
|
warn!("Event {} failed verification {:?} {}", event_id, pdu, e);
|
||||||
|
|
|
@ -23,7 +23,7 @@ use ruma::{
|
||||||
push::{Action, Ruleset, Tweak},
|
push::{Action, Ruleset, Tweak},
|
||||||
state_res::{self, Event},
|
state_res::{self, Event},
|
||||||
uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch,
|
uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch,
|
||||||
OwnedEventId, OwnedRoomId, OwnedServerName, RoomId, RoomVersionId, ServerName, UserId,
|
OwnedEventId, OwnedRoomId, OwnedServerName, RoomId, ServerName, UserId,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::value::{to_raw_value, RawValue as RawJsonValue};
|
use serde_json::value::{to_raw_value, RawValue as RawJsonValue};
|
||||||
|
@ -383,29 +383,12 @@ impl Service {
|
||||||
match pdu.kind {
|
match pdu.kind {
|
||||||
TimelineEventType::RoomRedaction => {
|
TimelineEventType::RoomRedaction => {
|
||||||
let room_version_id = services().rooms.state.get_room_version(&pdu.room_id)?;
|
let room_version_id = services().rooms.state.get_room_version(&pdu.room_id)?;
|
||||||
match room_version_id {
|
let rules = room_version_id
|
||||||
RoomVersionId::V1
|
.rules()
|
||||||
| RoomVersionId::V2
|
.expect("Supported room version must have rules.")
|
||||||
| RoomVersionId::V3
|
.redaction;
|
||||||
| RoomVersionId::V4
|
|
||||||
| RoomVersionId::V5
|
if rules.content_field_redacts {
|
||||||
| RoomVersionId::V6
|
|
||||||
| RoomVersionId::V7
|
|
||||||
| RoomVersionId::V8
|
|
||||||
| RoomVersionId::V9
|
|
||||||
| RoomVersionId::V10 => {
|
|
||||||
if let Some(redact_id) = &pdu.redacts {
|
|
||||||
if services().rooms.state_accessor.user_can_redact(
|
|
||||||
redact_id,
|
|
||||||
&pdu.sender,
|
|
||||||
&pdu.room_id,
|
|
||||||
false,
|
|
||||||
)? {
|
|
||||||
self.redact_pdu(redact_id, pdu, shortroomid)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RoomVersionId::V11 => {
|
|
||||||
let content =
|
let content =
|
||||||
serde_json::from_str::<RoomRedactionEventContent>(pdu.content.get())
|
serde_json::from_str::<RoomRedactionEventContent>(pdu.content.get())
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
|
@ -421,8 +404,15 @@ impl Service {
|
||||||
self.redact_pdu(redact_id, pdu, shortroomid)?;
|
self.redact_pdu(redact_id, pdu, shortroomid)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if let Some(redact_id) = &pdu.redacts {
|
||||||
|
if services().rooms.state_accessor.user_can_redact(
|
||||||
|
redact_id,
|
||||||
|
&pdu.sender,
|
||||||
|
&pdu.room_id,
|
||||||
|
false,
|
||||||
|
)? {
|
||||||
|
self.redact_pdu(redact_id, pdu, shortroomid)?;
|
||||||
}
|
}
|
||||||
_ => unreachable!("Validity of room version already checked"),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
TimelineEventType::SpaceChild => {
|
TimelineEventType::SpaceChild => {
|
||||||
|
@ -958,37 +948,15 @@ impl Service {
|
||||||
|
|
||||||
// If redaction event is not authorized, do not append it to the timeline
|
// If redaction event is not authorized, do not append it to the timeline
|
||||||
if pdu.kind == TimelineEventType::RoomRedaction {
|
if pdu.kind == TimelineEventType::RoomRedaction {
|
||||||
match services().rooms.state.get_room_version(&pdu.room_id)? {
|
let room_version_id = services().rooms.state.get_room_version(&pdu.room_id)?;
|
||||||
RoomVersionId::V1
|
let rules = room_version_id
|
||||||
| RoomVersionId::V2
|
.rules()
|
||||||
| RoomVersionId::V3
|
.expect("Supported room version must have rules.")
|
||||||
| RoomVersionId::V4
|
.redaction;
|
||||||
| RoomVersionId::V5
|
|
||||||
| RoomVersionId::V6
|
if rules.content_field_redacts {
|
||||||
| RoomVersionId::V7
|
let content = serde_json::from_str::<RoomRedactionEventContent>(pdu.content.get())
|
||||||
| RoomVersionId::V8
|
.map_err(|_| Error::bad_database("Invalid content in redaction pdu."))?;
|
||||||
| RoomVersionId::V9
|
|
||||||
| RoomVersionId::V10 => {
|
|
||||||
if let Some(redact_id) = &pdu.redacts {
|
|
||||||
if !services().rooms.state_accessor.user_can_redact(
|
|
||||||
redact_id,
|
|
||||||
&pdu.sender,
|
|
||||||
&pdu.room_id,
|
|
||||||
false,
|
|
||||||
)? {
|
|
||||||
return Err(Error::BadRequest(
|
|
||||||
ErrorKind::forbidden(),
|
|
||||||
"User cannot redact this event.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
RoomVersionId::V11 => {
|
|
||||||
let content =
|
|
||||||
serde_json::from_str::<RoomRedactionEventContent>(pdu.content.get())
|
|
||||||
.map_err(|_| {
|
|
||||||
Error::bad_database("Invalid content in redaction pdu.")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if let Some(redact_id) = &content.redacts {
|
if let Some(redact_id) = &content.redacts {
|
||||||
if !services().rooms.state_accessor.user_can_redact(
|
if !services().rooms.state_accessor.user_can_redact(
|
||||||
|
@ -1003,11 +971,16 @@ impl Service {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if let Some(redact_id) = &pdu.redacts {
|
||||||
_ => {
|
if !services().rooms.state_accessor.user_can_redact(
|
||||||
|
redact_id,
|
||||||
|
&pdu.sender,
|
||||||
|
&pdu.room_id,
|
||||||
|
false,
|
||||||
|
)? {
|
||||||
return Err(Error::BadRequest(
|
return Err(Error::BadRequest(
|
||||||
ErrorKind::UnsupportedRoomVersion,
|
ErrorKind::forbidden(),
|
||||||
"Unsupported room version",
|
"User cannot redact this event.",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue