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