Skip to main content

tuwunel_service/admin/
grant.rs

1use futures::FutureExt;
2use ruma::{
3	RoomId, UserId,
4	events::{
5		StateEventType,
6		room::{
7			member::{MembershipState, RoomMemberEventContent},
8			message::RoomMessageEventContent,
9			power_levels::RoomPowerLevelsEventContent,
10		},
11	},
12};
13use tuwunel_core::{
14	Err, Result, debug_info, debug_warn, error, implement, matrix::pdu::PduBuilder,
15};
16
17use crate::rooms::state::RoomMutexGuard;
18
19/// Invite the user to the tuwunel admin room.
20///
21/// This is equivalent to granting server admin privileges.
22#[implement(super::Service)]
23pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
24	let Ok(room_id) = self.get_admin_room().await else {
25		debug_warn!(
26			"make_user_admin was called without an admin room being available or created"
27		);
28		return Ok(());
29	};
30
31	let state_lock = self.services.state.mutex.lock(&room_id).await;
32
33	let is_joined = self
34		.services
35		.state_cache
36		.is_joined(user_id, &room_id)
37		.await;
38
39	let is_invited = self
40		.services
41		.state_cache
42		.is_invited(user_id, &room_id)
43		.await;
44
45	let already_member = is_joined || is_invited;
46
47	if !already_member {
48		self.invite_new_admin(user_id, &room_id, &state_lock)
49			.await?;
50	}
51
52	let server_user: &UserId = self.services.globals.server_user.as_ref();
53
54	let mut room_power_levels = self
55		.services
56		.state_accessor
57		.room_state_get_content::<RoomPowerLevelsEventContent>(
58			&room_id,
59			&StateEventType::RoomPowerLevels,
60			"",
61		)
62		.await
63		.unwrap_or_default();
64
65	let server_level = 69420.into();
66	let admin_level = 100.into();
67	let already_granted = room_power_levels.users.get(server_user) == Some(&server_level)
68		&& room_power_levels.users.get(user_id) == Some(&admin_level);
69
70	if !already_granted {
71		room_power_levels
72			.users
73			.insert(server_user.into(), server_level);
74		room_power_levels
75			.users
76			.insert(user_id.into(), admin_level);
77
78		self.services
79			.timeline
80			.build_and_append_pdu(
81				PduBuilder::state(String::new(), &room_power_levels),
82				server_user,
83				&room_id,
84				&state_lock,
85			)
86			.await?;
87	}
88
89	// Set room tag
90	let room_tag = self
91		.services
92		.server
93		.config
94		.admin_room_tag
95		.as_str();
96
97	if !already_granted
98		&& !room_tag.is_empty()
99		&& let Err(e) = self
100			.services
101			.account_data
102			.set_room_tag(user_id, &room_id, room_tag.into(), None)
103			.await
104	{
105		error!(?room_id, ?user_id, ?room_tag, "Failed to set tag for admin grant: {e}");
106	}
107
108	if !already_member && self.services.server.config.admin_room_notices {
109		let welcome_message = String::from(
110			"## Thank you for trying out tuwunel!\n\nTuwunel is a continuation of conduwuit which was technically a hard fork of Conduit.\n\nHelpful links:\n> GitHub Repo: https://github.com/matrix-construct/tuwunel\n> Documentation: https://matrix-construct.github.io/tuwunel\n> Report issues: https://github.com/matrix-construct/tuwunel/issues\n\nFor a list of available commands, send the following message in this room: `!admin --help`",
111		);
112
113		// Send welcome message
114		self.services
115			.timeline
116			.build_and_append_pdu(
117				PduBuilder::timeline(&RoomMessageEventContent::text_markdown(welcome_message)),
118				server_user,
119				&room_id,
120				&state_lock,
121			)
122			.await?;
123	}
124
125	Ok(())
126}
127
128#[implement(super::Service)]
129async fn invite_new_admin(
130	&self,
131	user_id: &UserId,
132	room_id: &RoomId,
133	state_lock: &RoomMutexGuard,
134) -> Result {
135	let server_user = self.services.globals.server_user.as_ref();
136
137	// if this is our local user, just forcefully join them in the room. otherwise,
138	// invite the remote user.
139	if self.services.globals.user_is_local(user_id) {
140		debug_info!("Inviting local user {user_id} to admin room {room_id}");
141		self.services
142			.timeline
143			.build_and_append_pdu(
144				PduBuilder::state(
145					String::from(user_id),
146					&RoomMemberEventContent::new(MembershipState::Invite),
147				),
148				server_user,
149				room_id,
150				state_lock,
151			)
152			.await?;
153
154		debug_info!("Force joining local user {user_id} to admin room {room_id}");
155		self.services
156			.timeline
157			.build_and_append_pdu(
158				PduBuilder::state(
159					String::from(user_id),
160					&RoomMemberEventContent::new(MembershipState::Join),
161				),
162				user_id,
163				room_id,
164				state_lock,
165			)
166			.await?;
167	} else {
168		debug_info!("Inviting remote user {user_id} to admin room {room_id}");
169		self.services
170			.timeline
171			.build_and_append_pdu(
172				PduBuilder::state(
173					user_id.to_string(),
174					&RoomMemberEventContent::new(MembershipState::Invite),
175				),
176				server_user,
177				room_id,
178				state_lock,
179			)
180			.await?;
181	}
182
183	Ok(())
184}
185
186/// Demote an admin, removing its rights.
187#[implement(super::Service)]
188pub async fn revoke_admin(&self, user_id: &UserId) -> Result {
189	use MembershipState::{Invite, Join, Knock, Leave};
190
191	let Ok(room_id) = self.get_admin_room().await else {
192		return Err!(error!("No admin room available or created."));
193	};
194
195	let state_lock = self.services.state.mutex.lock(&room_id).await;
196
197	let event = match self
198		.services
199		.state_accessor
200		.get_member(&room_id, user_id)
201		.await
202	{
203		| Err(e) if e.is_not_found() => return Err!("{user_id} was never an admin."),
204
205		| Err(e) => return Err!(error!(?e, "Failure occurred while attempting revoke.")),
206
207		| Ok(event) if !matches!(event.membership, Invite | Knock | Join) => {
208			return Err!("Cannot revoke {user_id} in membership state {:?}.", event.membership);
209		},
210
211		| Ok(event) => {
212			assert!(
213				matches!(event.membership, Invite | Knock | Join),
214				"Incorrect membership state to remove user."
215			);
216
217			event
218		},
219	};
220
221	self.services
222		.timeline
223		.build_and_append_pdu(
224			PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
225				membership: Leave,
226				reason: Some("Admin Revoked".into()),
227				is_direct: None,
228				join_authorized_via_users_server: None,
229				third_party_invite: None,
230				..event
231			}),
232			self.services.globals.server_user.as_ref(),
233			&room_id,
234			&state_lock,
235		)
236		.boxed()
237		.await
238		.map(|_| ())
239}