1use futures::{TryFutureExt, future::join4};
2use ruma::{
3 UserId,
4 events::{
5 GlobalAccountDataEventType,
6 ignored_user_list::{IgnoredUserListEvent, IgnoredUserListEventContent},
7 invite_permission_config::{
8 InvitePermission, InvitePermissionAction, InvitePermissionConfigEvent,
9 InvitePermissionConfigEventContent, UnstableInvitePermissionConfigEvent,
10 UnstableInvitePermissionConfigEventContent,
11 },
12 },
13};
14use serde::Deserialize;
15use tuwunel_core::{implement, utils::future::TryExtExt};
16
17pub struct InviteFilter {
24 ignored: Option<IgnoredUserListEventContent>,
25 stable: Option<InvitePermissionConfigEventContent>,
26 unstable_blanket: Option<InvitePermissionConfigEventContent>,
27 unstable_lists: Option<InvitePermissionConfigEventContent>,
28}
29
30const BLANKET_TYPE: &str = "org.matrix.msc4380.invite_permission_config";
37
38const LISTS_TYPE: &str = "org.matrix.msc4155.invite_permission_config";
43
44#[implement(super::Service)]
49pub async fn invite_filter(&self, recipient: &UserId) -> InviteFilter {
50 let ignored = self
51 .services
52 .account_data
53 .get_global(recipient, GlobalAccountDataEventType::IgnoredUserList)
54 .map_ok(|event: IgnoredUserListEvent| event.content)
55 .ok();
56
57 let stable = self.stable_config(recipient);
58
59 let unstable_blanket = self
60 .services
61 .account_data
62 .get_global(recipient, BLANKET_TYPE.into())
63 .map_ok(|event: UnstableInvitePermissionConfigEvent| event.content.into())
64 .ok();
65
66 let unstable_lists = self
67 .services
68 .account_data
69 .get_global(recipient, LISTS_TYPE.into())
70 .map_ok(|event: InvitePermissionConfigEvent| event.content)
71 .ok();
72
73 let (ignored, stable, unstable_blanket, unstable_lists) =
74 join4(ignored, stable, unstable_blanket, unstable_lists).await;
75
76 InviteFilter {
77 ignored,
78 stable,
79 unstable_blanket,
80 unstable_lists,
81 }
82}
83
84#[implement(super::Service)]
90async fn stable_config(&self, recipient: &UserId) -> Option<InvitePermissionConfigEventContent> {
91 let kind = GlobalAccountDataEventType::InvitePermissionConfig;
92
93 let config = self
94 .services
95 .account_data
96 .get_global(recipient, kind.clone())
97 .map_ok(|event: InvitePermissionConfigEvent| event.content)
98 .await;
99
100 match config {
101 | Ok(content) => Some(content),
102 | Err(_) => self
103 .services
104 .account_data
105 .get_global(recipient, kind)
106 .map_ok(|event: BlanketEvent| event.content.default_action)
107 .await
108 .is_ok_and(|action| matches!(action, Some(InvitePermissionAction::Block)))
109 .then(|| UnstableInvitePermissionConfigEventContent::new(true).into()),
110 }
111}
112
113#[derive(Deserialize)]
115struct BlanketEvent {
116 content: BlanketContent,
117}
118
119#[derive(Deserialize)]
120struct BlanketContent {
121 #[serde(default, deserialize_with = "ruma::serde::default_on_error")]
122 default_action: Option<InvitePermissionAction>,
123}
124
125#[implement(super::Service)]
130pub async fn invite_permission(&self, sender: &UserId, recipient: &UserId) -> InvitePermission {
131 self.invite_filter(recipient)
132 .await
133 .permission(sender)
134}
135
136#[implement(InviteFilter)]
144#[must_use]
145pub fn permits(&self, sender: Option<&UserId>) -> bool {
146 match sender {
147 | None => !self.blocks_all(),
148 | Some(sender) => self
149 .permission(sender)
150 .eq(&InvitePermission::Allow),
151 }
152}
153
154#[implement(InviteFilter)]
161#[must_use]
162pub fn permission(&self, sender: &UserId) -> InvitePermission {
163 match self {
164 | _ if self.blocks_all() => InvitePermission::Block,
165 | _ if self.ignores(sender) => InvitePermission::Ignore,
166 | _ => self
167 .configs()
168 .map(|config| config.permission(sender))
169 .find(|permission| permission.ne(&InvitePermission::Allow))
170 .unwrap_or(InvitePermission::Allow),
171 }
172}
173
174#[implement(InviteFilter)]
179#[inline]
180fn blocks_all(&self) -> bool {
181 self.configs()
182 .any(|config| config.default_action == Some(InvitePermissionAction::Block))
183}
184
185#[implement(InviteFilter)]
190#[inline]
191fn ignores(&self, sender: &UserId) -> bool {
192 self.ignored
193 .as_ref()
194 .is_some_and(|content| content.ignored_users.contains_key(sender))
195}
196
197#[implement(InviteFilter)]
202#[inline]
203#[must_use]
204pub fn is_permissive(&self) -> bool {
205 self.ignored
206 .as_ref()
207 .is_none_or(|ignored| ignored.ignored_users.is_empty())
208 && self
209 .configs()
210 .all(InvitePermissionConfigEventContent::is_inert)
211}
212
213#[implement(InviteFilter)]
215#[inline]
216fn configs(&self) -> impl Iterator<Item = &InvitePermissionConfigEventContent> + Send {
217 self.stable
218 .iter()
219 .chain(self.unstable_blanket.iter())
220 .chain(self.unstable_lists.iter())
221}
222
223#[cfg(test)]
224mod tests {
225 use ruma::{
226 events::invite_permission_config::UnstableInvitePermissionConfigEventContent, user_id,
227 };
228 use serde_json::{from_value, json};
229
230 use super::{InviteFilter, InvitePermission};
231
232 fn filter(
233 ignored: Option<serde_json::Value>,
234 stable: Option<serde_json::Value>,
235 unstable_lists: Option<serde_json::Value>,
236 ) -> InviteFilter {
237 InviteFilter {
238 ignored: ignored.map(|content| from_value(content).unwrap()),
239 stable: stable.map(|content| from_value(content).unwrap()),
240 unstable_blanket: None,
241 unstable_lists: unstable_lists.map(|content| from_value(content).unwrap()),
242 }
243 }
244
245 fn blanket_filter(block_all: bool) -> InviteFilter {
246 InviteFilter {
247 ignored: None,
248 stable: None,
249 unstable_blanket: Some(
250 UnstableInvitePermissionConfigEventContent::new(block_all).into(),
251 ),
252 unstable_lists: None,
253 }
254 }
255
256 #[test]
257 fn blanket_block_outranks_ignored_list() {
258 let sender = user_id!("@alice:example.org");
259 let ignored = json!({"ignored_users": {"@alice:example.org": {}}});
260
261 let both = filter(Some(ignored.clone()), Some(json!({"default_action": "block"})), None);
262
263 assert_eq!(both.permission(sender), InvitePermission::Block);
264
265 let ignored_only = filter(Some(ignored), None, None);
266
267 assert_eq!(ignored_only.permission(sender), InvitePermission::Ignore);
268 }
269
270 #[test]
271 fn senderless_invite_is_served() {
272 let sender = user_id!("@alice:example.org");
273 let blocked = filter(None, None, Some(json!({"blocked_servers": ["*"]})));
274
275 assert!(!blocked.is_permissive());
276 assert_eq!(blocked.permission(sender), InvitePermission::Block);
277 assert!(blocked.permits(None));
278 assert!(!blocked.permits(Some(sender)));
279
280 let ignored = json!({"ignored_users": {"@alice:example.org": {}}});
281 let ignoring = filter(Some(ignored), None, None);
282
283 assert!(ignoring.permits(None));
284 assert!(!ignoring.permits(Some(sender)));
285
286 let blanket = blanket_filter(true);
287
288 assert!(!blanket.permits(None));
289 assert!(!blanket.permits(Some(sender)));
290 }
291
292 #[test]
293 fn permissive_filter_permits_every_sender() {
294 let permissive = filter(None, None, Some(json!({})));
295
296 assert!(permissive.is_permissive());
297 assert!(permissive.permits(None));
298 assert!(permissive.permits(Some(user_id!("@alice:example.org"))));
299 }
300
301 #[test]
302 fn vacuous_configuration_is_permissive() {
303 let empty_list = filter(Some(json!({"ignored_users": {}})), None, None);
304
305 assert!(empty_list.is_permissive());
306
307 let empty_config = filter(None, Some(json!({})), Some(json!({"allowed_users": []})));
308
309 assert!(empty_config.is_permissive());
310
311 let disabled =
312 filter(None, None, Some(json!({"enabled": false, "blocked_servers": ["*"]})));
313
314 assert!(disabled.is_permissive());
315
316 let filtering = filter(None, None, Some(json!({"blocked_servers": ["*"]})));
317
318 assert!(!filtering.is_permissive());
319 assert!(!blanket_filter(true).is_permissive());
320 }
321
322 #[test]
323 fn unstable_blanket_slot_blocks() {
324 let sender = user_id!("@alice:example.org");
325
326 assert_eq!(blanket_filter(true).permission(sender), InvitePermission::Block);
327 assert_eq!(blanket_filter(false).permission(sender), InvitePermission::Allow);
328 assert!(blanket_filter(false).is_permissive());
329 }
330
331 #[test]
332 fn first_non_allow_verdict_spans_both_slots() {
333 let sender = user_id!("@alice:example.org");
334
335 let cross = filter(
336 None,
337 Some(json!({"allowed_users": ["@alice:example.org"]})),
338 Some(json!({"blocked_servers": ["*"]})),
339 );
340
341 assert_eq!(cross.permission(sender), InvitePermission::Block);
342
343 let unstable_only = filter(None, None, Some(json!({"ignored_users": ["@alice:*"]})));
344
345 assert_eq!(unstable_only.permission(sender), InvitePermission::Ignore);
346 }
347}