tuwunel_service/rooms/state_accessor/
user_can.rs1use futures::pin_mut;
8use ruma::{
9 EventId, RoomId, UserId,
10 events::{
11 StateEventType, TimelineEventType,
12 room::{
13 history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
14 member::{MembershipState, RoomMemberEventContent},
15 tombstone::RoomTombstoneEventContent,
16 },
17 },
18};
19use tuwunel_core::{
20 Err, Result, implement,
21 matrix::{Event, PduCount, StateKey},
22 pdu::PduBuilder,
23 utils::FutureBoolExt,
24};
25
26use crate::rooms::{short::ShortStateHash, state::RoomMutexGuard};
27
28#[implement(super::Service)]
35pub async fn user_can_redact(
36 &self,
37 redacts: &EventId,
38 sender: &UserId,
39 room_id: &RoomId,
40 federation: bool,
41) -> Result<bool> {
42 let redacting_event = self.services.timeline.get_pdu(redacts).await;
43
44 if redacting_event
45 .as_ref()
46 .is_ok_and(|pdu| pdu.room_id() != room_id)
47 {
48 return Ok(false);
49 }
50
51 if redacting_event
52 .as_ref()
53 .is_ok_and(|pdu| *pdu.kind() == TimelineEventType::RoomCreate)
54 {
55 return Err!(Request(Forbidden("Redacting m.room.create is not safe, forbidding.")));
56 }
57
58 if redacting_event
59 .as_ref()
60 .is_ok_and(|pdu| *pdu.kind() == TimelineEventType::RoomServerAcl)
61 {
62 return Err!(Request(Forbidden(
63 "Redacting m.room.server_acl will result in the room being inaccessible for \
64 everyone (empty allow key), forbidding."
65 )));
66 }
67
68 match self.get_power_levels(room_id).await {
69 | Ok(power_levels) => Ok(power_levels.user_can_redact_event_of_other(sender)
70 || power_levels.user_can_redact_own_event(sender)
71 && match redacting_event {
72 | Ok(redacting_event) =>
73 if federation {
74 redacting_event.sender().server_name() == sender.server_name()
75 } else {
76 redacting_event.sender() == sender
77 },
78 | _ => false,
79 }),
80 | _ => {
81 match self
83 .room_state_get(room_id, &StateEventType::RoomCreate, "")
84 .await
85 {
86 | Ok(room_create) => Ok(room_create.sender() == sender
87 || redacting_event
88 .as_ref()
89 .is_ok_and(|redacting_event| redacting_event.sender() == sender)),
90 | _ => Err!(Database(
91 "No m.room.power_levels or m.room.create events in database for room"
92 )),
93 }
94 },
95 }
96}
97
98#[implement(super::Service)]
106#[tracing::instrument(skip_all, level = "trace")]
107pub async fn user_can_see_event<Pdu>(&self, user_id: &UserId, pdu: &Pdu) -> bool
108where
109 Pdu: Event,
110{
111 let Some((shortstatehash, history_visibility)) =
112 self.history_visibility_at(pdu.event_id()).await
113 else {
114 return true;
115 };
116
117 match history_visibility {
118 | HistoryVisibility::WorldReadable => true,
119
120 | HistoryVisibility::Invited =>
122 matches!(
123 pdu.membership_for(user_id),
124 Some(MembershipState::Join | MembershipState::Invite)
125 ) || self
126 .user_was_invited(shortstatehash, user_id)
127 .await,
128
129 | HistoryVisibility::Joined =>
131 matches!(pdu.membership_for(user_id), Some(MembershipState::Join))
132 || self
133 .user_was_joined(shortstatehash, user_id)
134 .await,
135
136 | HistoryVisibility::Shared | _ =>
138 self.user_shared_history(shortstatehash, pdu.room_id(), pdu.event_id(), user_id)
139 .await,
140 }
141}
142
143#[implement(super::Service)]
148#[tracing::instrument(skip_all, level = "trace")]
149async fn history_visibility_at(
150 &self,
151 event_id: &EventId,
152) -> Option<(ShortStateHash, HistoryVisibility)> {
153 let shortstatehash = self
154 .services
155 .state
156 .pdu_shortstatehash(event_id)
157 .await
158 .ok()?;
159
160 let history_visibility = self
161 .state_get_content(shortstatehash, &StateEventType::RoomHistoryVisibility, "")
162 .await
163 .map_or(HistoryVisibility::Shared, |c: RoomHistoryVisibilityEventContent| {
164 c.history_visibility
165 });
166
167 Some((shortstatehash, history_visibility))
168}
169
170#[implement(super::Service)]
176async fn user_shared_history(
177 &self,
178 shortstatehash: ShortStateHash,
179 room_id: &RoomId,
180 event_id: &EventId,
181 user_id: &UserId,
182) -> bool {
183 let state_cache = &self.services.state_cache;
184
185 if state_cache.is_joined(user_id, room_id).await
186 || self
187 .user_was_joined(shortstatehash, user_id)
188 .await
189 {
190 return true;
191 }
192
193 if !state_cache.once_joined(user_id, room_id).await {
194 return false;
195 }
196
197 let Ok(left_count) = state_cache.get_left_count(room_id, user_id).await else {
198 return false;
199 };
200
201 let Ok(event_count) = self
202 .services
203 .timeline
204 .get_pdu_count(event_id)
205 .await
206 else {
207 return false;
208 };
209
210 event_count <= PduCount::from_unsigned(left_count)
211}
212
213#[implement(super::Service)]
220#[tracing::instrument(skip_all, level = "trace")]
221pub async fn user_can_see_state_events(&self, user_id: &UserId, room_id: &RoomId) -> bool {
222 if self
223 .services
224 .state_cache
225 .is_joined(user_id, room_id)
226 .await
227 {
228 return true;
229 }
230
231 let history_visibility = self
232 .room_state_get_content(room_id, &StateEventType::RoomHistoryVisibility, "")
233 .await
234 .map_or(HistoryVisibility::Shared, |c: RoomHistoryVisibilityEventContent| {
235 c.history_visibility
236 });
237
238 match history_visibility {
239 | HistoryVisibility::WorldReadable => true,
240
241 | HistoryVisibility::Invited =>
242 self.services
243 .state_cache
244 .is_invited(user_id, room_id)
245 .await,
246
247 | HistoryVisibility::Shared =>
248 self.services
249 .state_cache
250 .once_joined(user_id, room_id)
251 .await,
252
253 | _ => false,
254 }
255}
256
257#[implement(super::Service)]
263pub async fn user_can_see_room(&self, user_id: &UserId, room_id: &RoomId) -> bool {
264 let state_cache = &self.services.state_cache;
265 let joined = state_cache.is_joined(user_id, room_id);
266 let invited = state_cache.is_invited(user_id, room_id);
267 let left = state_cache.is_left(user_id, room_id);
268 let world_readable = self.is_world_readable(room_id);
269
270 pin_mut!(joined, invited, left, world_readable);
271 joined
272 .or(invited)
273 .or(left)
274 .or(world_readable)
275 .await
276}
277
278#[implement(super::Service)]
284#[tracing::instrument(skip_all, level = "trace")]
285pub async fn user_can_peek(&self, user_id: &UserId, room_id: &RoomId) -> bool {
286 self.services
288 .state_cache
289 .is_joined(user_id, room_id)
290 .await
291 || self.is_world_readable(room_id).await
292}
293
294#[implement(super::Service)]
300#[tracing::instrument(skip_all, level = "trace")]
301pub async fn is_world_readable_at<Pdu>(&self, pdu: &Pdu) -> bool
302where
303 Pdu: Event,
304{
305 let opens_history = pdu.is_type_and_state_key(&TimelineEventType::RoomHistoryVisibility, "")
306 && pdu
307 .get_content()
308 .is_ok_and(|c: RoomHistoryVisibilityEventContent| {
309 c.history_visibility == HistoryVisibility::WorldReadable
310 });
311
312 opens_history
313 || self
314 .history_visibility_at(pdu.event_id())
315 .await
316 .is_some_and(|(_, history_visibility)| {
317 history_visibility == HistoryVisibility::WorldReadable
318 })
319}
320
321#[implement(super::Service)]
327pub async fn user_can_invite(
328 &self,
329 room_id: &RoomId,
330 sender: &UserId,
331 target_user: &UserId,
332 state_lock: &RoomMutexGuard,
333) -> bool {
334 self.services
335 .timeline
336 .create_hash_and_sign_event(
337 PduBuilder::state(
338 target_user.as_str(),
339 &RoomMemberEventContent::new(MembershipState::Invite),
340 ),
341 sender,
342 room_id,
343 state_lock,
344 )
345 .await
346 .is_ok()
347}
348
349#[implement(super::Service)]
355pub async fn user_can_tombstone(
356 &self,
357 room_id: &RoomId,
358 user_id: &UserId,
359 state_lock: &RoomMutexGuard,
360) -> bool {
361 if !self
362 .services
363 .state_cache
364 .is_joined(user_id, room_id)
365 .await
366 {
367 return false;
368 }
369
370 self.services
371 .timeline
372 .create_hash_and_sign_event(
373 PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
374 replacement_room: room_id.into(), body: "Not a valid m.room.tombstone.".into(),
376 }),
377 user_id,
378 room_id,
379 state_lock,
380 )
381 .await
382 .is_ok()
383}
384
385#[cfg(test)]
386mod tests;