1use std::{borrow::Cow, collections::BTreeMap};
6
7use ruma_common::{serde::from_raw_json_value, space::SpaceRoomJoinRule, OwnedRoomId};
8use ruma_macros::EventContent;
9use serde::{
10 de::{Deserializer, Error},
11 Deserialize, Serialize,
12};
13use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue};
14
15use crate::{EmptyStateKey, PrivOwnedStr};
16
17#[derive(Clone, Debug, Serialize, EventContent)]
21#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
22#[ruma_event(type = "m.room.join_rules", kind = State, state_key_type = EmptyStateKey)]
23pub struct RoomJoinRulesEventContent {
24 #[ruma_event(skip_redaction)]
26 #[serde(flatten)]
27 pub join_rule: JoinRule,
28}
29
30impl RoomJoinRulesEventContent {
31 pub fn new(join_rule: JoinRule) -> Self {
33 Self { join_rule }
34 }
35
36 pub fn restricted(allow: Vec<AllowRule>) -> Self {
39 Self { join_rule: JoinRule::Restricted(Restricted::new(allow)) }
40 }
41
42 pub fn knock_restricted(allow: Vec<AllowRule>) -> Self {
45 Self { join_rule: JoinRule::KnockRestricted(Restricted::new(allow)) }
46 }
47}
48
49impl<'de> Deserialize<'de> for RoomJoinRulesEventContent {
50 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51 where
52 D: Deserializer<'de>,
53 {
54 let join_rule = JoinRule::deserialize(deserializer)?;
55 Ok(RoomJoinRulesEventContent { join_rule })
56 }
57}
58
59impl RoomJoinRulesEvent {
60 pub fn join_rule(&self) -> &JoinRule {
62 match self {
63 Self::Original(ev) => &ev.content.join_rule,
64 Self::Redacted(ev) => &ev.content.join_rule,
65 }
66 }
67}
68
69impl SyncRoomJoinRulesEvent {
70 pub fn join_rule(&self) -> &JoinRule {
72 match self {
73 Self::Original(ev) => &ev.content.join_rule,
74 Self::Redacted(ev) => &ev.content.join_rule,
75 }
76 }
77}
78
79#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
84#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
85#[serde(tag = "join_rule", rename_all = "snake_case")]
86pub enum JoinRule {
87 Invite,
90
91 Knock,
95
96 Private,
98
99 Restricted(Restricted),
102
103 KnockRestricted(Restricted),
106
107 Public,
109
110 #[doc(hidden)]
111 #[serde(skip_serializing)]
112 _Custom(PrivOwnedStr),
113}
114
115impl JoinRule {
116 pub fn as_str(&self) -> &str {
118 match self {
119 JoinRule::Invite => "invite",
120 JoinRule::Knock => "knock",
121 JoinRule::Private => "private",
122 JoinRule::Restricted(_) => "restricted",
123 JoinRule::KnockRestricted(_) => "knock_restricted",
124 JoinRule::Public => "public",
125 JoinRule::_Custom(rule) => &rule.0,
126 }
127 }
128}
129
130impl<'de> Deserialize<'de> for JoinRule {
131 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132 where
133 D: Deserializer<'de>,
134 {
135 let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
136
137 #[derive(Deserialize)]
138 struct ExtractType<'a> {
139 #[serde(borrow)]
140 join_rule: Option<Cow<'a, str>>,
141 }
142
143 let join_rule = serde_json::from_str::<ExtractType<'_>>(json.get())
144 .map_err(Error::custom)?
145 .join_rule
146 .ok_or_else(|| D::Error::missing_field("join_rule"))?;
147
148 match join_rule.as_ref() {
149 "invite" => Ok(Self::Invite),
150 "knock" => Ok(Self::Knock),
151 "private" => Ok(Self::Private),
152 "restricted" => from_raw_json_value(&json).map(Self::Restricted),
153 "knock_restricted" => from_raw_json_value(&json).map(Self::KnockRestricted),
154 "public" => Ok(Self::Public),
155 _ => Ok(Self::_Custom(PrivOwnedStr(join_rule.into()))),
156 }
157 }
158}
159
160impl From<JoinRule> for SpaceRoomJoinRule {
161 fn from(value: JoinRule) -> Self {
162 value.as_str().into()
163 }
164}
165
166#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
168#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
169pub struct Restricted {
170 #[serde(default)]
172 pub allow: Vec<AllowRule>,
173}
174
175impl Restricted {
176 pub fn new(allow: Vec<AllowRule>) -> Self {
178 Self { allow }
179 }
180}
181
182#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
184#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
185#[serde(untagged)]
186pub enum AllowRule {
187 RoomMembership(RoomMembership),
189
190 #[doc(hidden)]
191 _Custom(Box<CustomAllowRule>),
192}
193
194impl AllowRule {
195 pub fn room_membership(room_id: OwnedRoomId) -> Self {
197 Self::RoomMembership(RoomMembership::new(room_id))
198 }
199}
200
201#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
203#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
204#[serde(tag = "type", rename = "m.room_membership")]
205pub struct RoomMembership {
206 pub room_id: OwnedRoomId,
208}
209
210impl RoomMembership {
211 pub fn new(room_id: OwnedRoomId) -> Self {
213 Self { room_id }
214 }
215}
216
217#[doc(hidden)]
218#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
219#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
220pub struct CustomAllowRule {
221 #[serde(rename = "type")]
222 rule_type: String,
223 #[serde(flatten)]
224 extra: BTreeMap<String, JsonValue>,
225}
226
227impl<'de> Deserialize<'de> for AllowRule {
228 fn deserialize<D>(deserializer: D) -> Result<AllowRule, D::Error>
229 where
230 D: Deserializer<'de>,
231 {
232 let json: Box<RawJsonValue> = Box::deserialize(deserializer)?;
233
234 #[derive(Deserialize)]
236 struct ExtractType<'a> {
237 #[serde(borrow, rename = "type")]
238 rule_type: Option<Cow<'a, str>>,
239 }
240
241 let rule_type =
243 serde_json::from_str::<ExtractType<'_>>(json.get()).map_err(Error::custom)?.rule_type;
244
245 match rule_type.as_deref() {
246 Some("m.room_membership") => from_raw_json_value(&json).map(Self::RoomMembership),
247 Some(_) => from_raw_json_value(&json).map(Self::_Custom),
248 None => Err(D::Error::missing_field("type")),
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use assert_matches2::assert_matches;
256 use ruma_common::owned_room_id;
257
258 use super::{
259 AllowRule, JoinRule, OriginalSyncRoomJoinRulesEvent, Restricted, RoomJoinRulesEventContent,
260 SpaceRoomJoinRule,
261 };
262
263 #[test]
264 fn deserialize() {
265 let json = r#"{"join_rule": "public"}"#;
266 let event: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap();
267 assert_matches!(event, RoomJoinRulesEventContent { join_rule: JoinRule::Public });
268 }
269
270 #[test]
271 fn deserialize_restricted() {
272 let json = r#"{
273 "join_rule": "restricted",
274 "allow": [
275 {
276 "type": "m.room_membership",
277 "room_id": "!mods:example.org"
278 },
279 {
280 "type": "m.room_membership",
281 "room_id": "!users:example.org"
282 }
283 ]
284 }"#;
285 let event: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap();
286 match event.join_rule {
287 JoinRule::Restricted(restricted) => assert_eq!(
288 restricted.allow,
289 &[
290 AllowRule::room_membership(owned_room_id!("!mods:example.org")),
291 AllowRule::room_membership(owned_room_id!("!users:example.org"))
292 ]
293 ),
294 rule => panic!("Deserialized to wrong variant: {rule:?}"),
295 }
296 }
297
298 #[test]
299 fn deserialize_restricted_event() {
300 let json = r#"{
301 "type": "m.room.join_rules",
302 "sender": "@admin:community.rs",
303 "content": {
304 "join_rule": "restricted",
305 "allow": [
306 { "type": "m.room_membership","room_id": "!KqeUnzmXPIhHRaWMTs:mccarty.io" }
307 ]
308 },
309 "state_key": "",
310 "origin_server_ts":1630508835342,
311 "unsigned": {
312 "age":4165521871
313 },
314 "event_id": "$0ACb9KSPlT3al3kikyRYvFhMqXPP9ZcQOBrsdIuh58U"
315 }"#;
316
317 assert_matches!(serde_json::from_str::<OriginalSyncRoomJoinRulesEvent>(json), Ok(_));
318 }
319
320 #[test]
321 fn roundtrip_custom_allow_rule() {
322 let json = r#"{"type":"org.msc9000.something","foo":"bar"}"#;
323 let allow_rule: AllowRule = serde_json::from_str(json).unwrap();
324 assert_matches!(&allow_rule, AllowRule::_Custom(_));
325 assert_eq!(serde_json::to_string(&allow_rule).unwrap(), json);
326 }
327
328 #[test]
329 fn restricted_room_no_allow_field() {
330 let json = r#"{"join_rule":"restricted"}"#;
331 let join_rules: RoomJoinRulesEventContent = serde_json::from_str(json).unwrap();
332 assert_matches!(
333 join_rules,
334 RoomJoinRulesEventContent { join_rule: JoinRule::Restricted(_) }
335 );
336 }
337
338 #[test]
339 fn join_rule_to_space_room_join_rule() {
340 assert_eq!(SpaceRoomJoinRule::Invite, JoinRule::Invite.into());
341 assert_eq!(SpaceRoomJoinRule::Knock, JoinRule::Knock.into());
342 assert_eq!(
343 SpaceRoomJoinRule::KnockRestricted,
344 JoinRule::KnockRestricted(Restricted::default()).into()
345 );
346 assert_eq!(SpaceRoomJoinRule::Public, JoinRule::Public.into());
347 assert_eq!(SpaceRoomJoinRule::Private, JoinRule::Private.into());
348 assert_eq!(
349 SpaceRoomJoinRule::Restricted,
350 JoinRule::Restricted(Restricted::default()).into()
351 );
352 }
353}