Skip to main content

tuwunel_service/admin/
grant.rs

1use futures::{FutureExt, future::join3};
2use ruma::{
3	Int, OwnedUserId, RoomId, UserId,
4	events::{
5		StateEventType,
6		room::{
7			member::{MembershipState, RoomMemberEventContent},
8			message::RoomMessageEventContent,
9			power_levels::{RoomPowerLevelsEventContent, UserPowerLevel},
10		},
11	},
12};
13use tuwunel_core::{
14	Err, Result, debug_info, debug_warn, error, implement, matrix::pdu::PduBuilder,
15	utils::stream::ReadyExt,
16};
17
18use crate::rooms::state::RoomMutexGuard;
19
20/// Invite the user to the tuwunel admin room.
21///
22/// This is equivalent to granting server admin privileges.
23#[implement(super::Service)]
24pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
25	let Ok(room_id) = self.get_admin_room().await else {
26		debug_warn!(
27			"make_user_admin was called without an admin room being available or created"
28		);
29		return Ok(());
30	};
31
32	let state_lock = self.services.state.mutex.lock(&room_id).await;
33
34	let is_joined = self
35		.services
36		.state_cache
37		.is_joined(user_id, &room_id)
38		.await;
39
40	let is_invited = self
41		.services
42		.state_cache
43		.is_invited(user_id, &room_id)
44		.await;
45
46	let already_member = is_joined || is_invited;
47
48	if !already_member {
49		self.invite_new_admin(user_id, &room_id, &state_lock)
50			.await?;
51	}
52
53	let server_user: &UserId = self.services.globals.server_user.as_ref();
54
55	let mut room_power_levels = self
56		.services
57		.state_accessor
58		.room_state_get_content::<RoomPowerLevelsEventContent>(
59			&room_id,
60			&StateEventType::RoomPowerLevels,
61			"",
62		)
63		.await
64		.unwrap_or_default();
65
66	let server_level = 69420.into();
67	let admin_level = 100.into();
68	let already_granted = room_power_levels.users.get(server_user) == Some(&server_level)
69		&& room_power_levels.users.get(user_id) == Some(&admin_level);
70
71	if !already_granted {
72		room_power_levels
73			.users
74			.insert(server_user.into(), server_level);
75		room_power_levels
76			.users
77			.insert(user_id.into(), admin_level);
78
79		self.services
80			.timeline
81			.build_and_append_pdu(
82				PduBuilder::state(String::new(), &room_power_levels),
83				server_user,
84				&room_id,
85				&state_lock,
86			)
87			.await?;
88	}
89
90	// Set room tag
91	let room_tag = self
92		.services
93		.server
94		.config
95		.admin_room_tag
96		.as_str();
97
98	if !already_granted
99		&& !room_tag.is_empty()
100		&& let Err(e) = self
101			.services
102			.account_data
103			.set_room_tag(user_id, &room_id, room_tag.into(), None)
104			.await
105	{
106		error!(?room_id, ?user_id, ?room_tag, "Failed to set tag for admin grant: {e}");
107	}
108
109	if !already_member && self.services.server.config.admin_room_notices {
110		let welcome_message = String::from(
111			"## 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`",
112		);
113
114		// Send welcome message
115		self.services
116			.timeline
117			.build_and_append_pdu(
118				PduBuilder::timeline(&RoomMessageEventContent::text_markdown(welcome_message)),
119				server_user,
120				&room_id,
121				&state_lock,
122			)
123			.await?;
124	}
125
126	Ok(())
127}
128
129/// Grant room admin powers to a target by impersonating the highest-powered
130/// local member.
131///
132/// The target is granted the impersonated member's power level (100 when that
133/// member is a room-version 12 creator with infinite power), sent as a new
134/// `m.room.power_levels` from that member. When the room is non-public and the
135/// target is neither joined nor invited, the target is invited first.
136#[implement(super::Service)]
137pub async fn make_room_admin(&self, room_id: &RoomId, target: &UserId) -> Result {
138	let state_lock = self.services.state.mutex.lock(room_id).await;
139
140	let power_levels = self
141		.services
142		.state_accessor
143		.get_power_levels(room_id)
144		.await?;
145
146	let sender = self
147		.services
148		.state_cache
149		.local_users_in_room(room_id)
150		.ready_fold(None, |best: Option<(OwnedUserId, UserPowerLevel)>, user| {
151			let level = power_levels.for_user(user);
152			match best {
153				| Some((_, best_level)) if best_level >= level => best,
154				| _ => Some((user.to_owned(), level)),
155			}
156		})
157		.await;
158
159	let Some((sender, sender_level)) = sender else {
160		return Err!(Request(InvalidParam("Server not in room")));
161	};
162
163	if !power_levels.user_can_send_state(&sender, StateEventType::RoomPowerLevels) {
164		return Err!(Request(InvalidParam(
165			"No local admin in room with power to update power levels"
166		)));
167	}
168
169	let grant_level: Int = match sender_level {
170		| UserPowerLevel::Infinite => 100.into(),
171		| UserPowerLevel::Int(level) => level,
172	};
173
174	let is_joined = self
175		.services
176		.state_cache
177		.is_joined(target, room_id);
178
179	let is_invited = self
180		.services
181		.state_cache
182		.is_invited(target, room_id);
183
184	let is_public = self.services.metadata.is_public(room_id);
185	let (is_joined, is_invited, is_public) = join3(is_joined, is_invited, is_public).await;
186
187	if !is_joined && !is_invited && !is_public {
188		self.services
189			.membership
190			.invite(&sender, target, room_id, None, false)
191			.await?;
192	}
193
194	let mut content = self
195		.services
196		.state_accessor
197		.room_state_get_content::<RoomPowerLevelsEventContent>(
198			room_id,
199			&StateEventType::RoomPowerLevels,
200			"",
201		)
202		.await
203		.unwrap_or_default();
204
205	content.users.insert(target.into(), grant_level);
206
207	self.services
208		.timeline
209		.build_and_append_pdu(
210			PduBuilder::state(String::new(), &content),
211			&sender,
212			room_id,
213			&state_lock,
214		)
215		.await
216		.map(|_| ())
217}
218
219#[implement(super::Service)]
220async fn invite_new_admin(
221	&self,
222	user_id: &UserId,
223	room_id: &RoomId,
224	state_lock: &RoomMutexGuard,
225) -> Result {
226	let server_user = self.services.globals.server_user.as_ref();
227
228	// if this is our local user, just forcefully join them in the room. otherwise,
229	// invite the remote user.
230	if self.services.globals.user_is_local(user_id) {
231		debug_info!("Inviting local user {user_id} to admin room {room_id}");
232		self.services
233			.timeline
234			.build_and_append_pdu(
235				PduBuilder::state(
236					String::from(user_id),
237					&RoomMemberEventContent::new(MembershipState::Invite),
238				),
239				server_user,
240				room_id,
241				state_lock,
242			)
243			.await?;
244
245		debug_info!("Force joining local user {user_id} to admin room {room_id}");
246		self.services
247			.timeline
248			.build_and_append_pdu(
249				PduBuilder::state(
250					String::from(user_id),
251					&RoomMemberEventContent::new(MembershipState::Join),
252				),
253				user_id,
254				room_id,
255				state_lock,
256			)
257			.await?;
258	} else {
259		debug_info!("Inviting remote user {user_id} to admin room {room_id}");
260		self.services
261			.timeline
262			.build_and_append_pdu(
263				PduBuilder::state(
264					user_id.to_string(),
265					&RoomMemberEventContent::new(MembershipState::Invite),
266				),
267				server_user,
268				room_id,
269				state_lock,
270			)
271			.await?;
272	}
273
274	Ok(())
275}
276
277/// Demote an admin, removing its rights.
278#[implement(super::Service)]
279pub async fn revoke_admin(&self, user_id: &UserId) -> Result {
280	use MembershipState::{Invite, Join, Knock, Leave};
281
282	let Ok(room_id) = self.get_admin_room().await else {
283		return Err!(error!("No admin room available or created."));
284	};
285
286	let state_lock = self.services.state.mutex.lock(&room_id).await;
287
288	let event = match self
289		.services
290		.state_accessor
291		.get_member(&room_id, user_id)
292		.await
293	{
294		| Err(e) if e.is_not_found() => return Err!("{user_id} was never an admin."),
295
296		| Err(e) => return Err!(error!(?e, "Failure occurred while attempting revoke.")),
297
298		| Ok(event) if !matches!(event.membership, Invite | Knock | Join) => {
299			return Err!("Cannot revoke {user_id} in membership state {:?}.", event.membership);
300		},
301
302		| Ok(event) => {
303			assert!(
304				matches!(event.membership, Invite | Knock | Join),
305				"Incorrect membership state to remove user."
306			);
307
308			event
309		},
310	};
311
312	self.services
313		.timeline
314		.build_and_append_pdu(
315			PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
316				membership: Leave,
317				reason: Some("Admin Revoked".into()),
318				is_direct: false,
319				join_authorized_via_users_server: None,
320				third_party_invite: None,
321				..event
322			}),
323			self.services.globals.server_user.as_ref(),
324			&room_id,
325			&state_lock,
326		)
327		.boxed()
328		.await
329		.map(|_| ())
330}