Skip to main content

tuwunel_api_client/
notice.rs

1use futures::{FutureExt, TryFutureExt, future::ready};
2use ruma::{
3	RoomId, UserId,
4	events::{StateEventType, tag::TagName},
5};
6use tuwunel_core::{Result, utils::BoolExt};
7use tuwunel_matrix::Event;
8use tuwunel_service::Services;
9
10/// Tests whether a room is the target user's server-notice room.
11///
12/// The configured tag and global server identity are applied consistently with
13/// notice-room lookup and creation.
14#[tracing::instrument(level = "trace", skip_all)]
15pub(crate) async fn is_notice_room(
16	services: &Services,
17	target: &UserId,
18	room_id: &RoomId,
19) -> Result<bool> {
20	let server_user = services.globals.server_user.as_ref();
21	let tag = notice_tag(&services.config.admin.room.tag);
22
23	if services
24		.admin
25		.get_admin_room()
26		.map(|admin| admin.is_ok_and(|admin| admin == room_id))
27		.await
28	{
29		return Ok(false);
30	}
31
32	room_is_notice(services, server_user, target, &tag, room_id).await
33}
34
35/// Checks server membership, the target's tag, and the immutable room creator.
36///
37/// An absent tag is an ordinary non-notice room; other lookup failures propagate.
38#[tracing::instrument(level = "trace", skip_all)]
39pub async fn room_is_notice(
40	services: &Services,
41	server_user: &UserId,
42	target: &UserId,
43	tag: &TagName,
44	room_id: &RoomId,
45) -> Result<bool> {
46	if !services
47		.state_cache
48		.is_joined(server_user, room_id)
49		.await
50	{
51		return Ok(false);
52	}
53
54	let tagged = services
55		.account_data
56		.get_room_tags(target, room_id)
57		.map_ok(|tags| tags.contains_key(tag))
58		.or_else(|error| ready(error.is_not_found().then_ok_or(false, error)))
59		.await?;
60
61	if !tagged {
62		return Ok(false);
63	}
64
65	services
66		.state_accessor
67		.room_state_get(room_id, &StateEventType::RoomCreate, "")
68		.map_ok(|create| is_notice_creator(create.sender(), server_user))
69		.await
70}
71
72/// Selects the configured notice tag, falling back when it is empty.
73///
74/// The fallback matches the tag used when creating server-notice rooms.
75pub fn notice_tag(tag: &str) -> TagName {
76	Some(tag)
77		.filter(|tag| !tag.is_empty())
78		.map_or(TagName::ServerNotice, Into::into)
79}
80
81/// Matches the immutable create-event sender to the configured server identity.
82///
83/// Membership alone cannot distinguish notice rooms from ordinary shared rooms.
84fn is_notice_creator(create_sender: &UserId, server_user: &UserId) -> bool {
85	create_sender == server_user
86}
87
88#[cfg(test)]
89mod tests {
90	use ruma::{events::tag::TagName, user_id};
91
92	use super::{is_notice_creator, notice_tag};
93
94	#[test]
95	fn only_the_server_user_creates_a_notice_room() {
96		let server = user_id!("@server:example.com");
97		let other = user_id!("@other:example.com");
98
99		assert!(is_notice_creator(server, server));
100		assert!(!is_notice_creator(other, server));
101	}
102
103	#[test]
104	fn empty_config_tag_falls_back_to_server_notice() {
105		assert_eq!(notice_tag(""), TagName::ServerNotice);
106		assert_eq!(notice_tag("m.server_notice"), TagName::ServerNotice);
107		assert_eq!(notice_tag("u.custom"), TagName::from("u.custom"));
108	}
109}