1use crate::error::MessengerError;
15use crate::ids::{EventId, RoomId, UserId};
16use crate::wire::events::{
17 DirectContent, HistoryVisibility, JoinRule, Membership, RawEvent, RoomCreateContent, RoomEncryptionContent,
18 RoomHistoryVisibilityContent, RoomJoinRulesContent, RoomMemberContent, RoomNameContent, RoomPinnedEventsContent,
19 RoomPowerLevelsContent, RoomPowerLevelsNotifications, RoomTopicContent,
20};
21use crate::wire::sync::{RoomSummary as SyncRoomSummary, UnreadNotifications};
22use serde::{Deserialize, Serialize};
23use std::collections::BTreeMap;
24
25const EVENT_ROOM_CREATE: &str = "m.room.create";
26const EVENT_ROOM_MEMBER: &str = "m.room.member";
27const EVENT_ROOM_POWER_LEVELS: &str = "m.room.power_levels";
28const EVENT_ROOM_JOIN_RULES: &str = "m.room.join_rules";
29const EVENT_ROOM_HISTORY_VISIBILITY: &str = "m.room.history_visibility";
30const EVENT_ROOM_NAME: &str = "m.room.name";
31const EVENT_ROOM_TOPIC: &str = "m.room.topic";
32const EVENT_ROOM_ENCRYPTION: &str = "m.room.encryption";
33const EVENT_ROOM_PINNED_EVENTS: &str = "m.room.pinned_events";
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum MemberRole {
40 Owner,
42 Admin,
44 Member,
46}
47
48impl MemberRole {
49 pub fn from_power_level(power_level: i64) -> Self {
53 if power_level >= 100 {
54 MemberRole::Owner
55 } else if power_level >= 50 {
56 MemberRole::Admin
57 } else {
58 MemberRole::Member
59 }
60 }
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub enum RoomKind {
68 Dm,
70 Group,
73 Channel,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
82pub struct MemberState {
83 pub membership: Membership,
85 pub displayname: Option<String>,
87 pub is_direct: bool,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub invited_by: Option<UserId>,
95}
96
97#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
105pub struct RoomState {
106 pub create: Option<RoomCreateContent>,
111 pub members: BTreeMap<UserId, MemberState>,
117 pub power_levels: RoomPowerLevelsContent,
121 pub join_rules: Option<RoomJoinRulesContent>,
123 pub history_visibility: Option<HistoryVisibility>,
125 pub name: Option<String>,
127 pub topic: Option<String>,
129 pub encryption: Option<RoomEncryptionContent>,
133 pub encryption_tamper_attempts: u32,
140 pub pinned_events: Vec<EventId>,
142 pub heroes: Vec<UserId>,
147 pub joined_member_count: Option<u64>,
149 pub invited_member_count: Option<u64>,
151 pub unread_notifications: UnreadNotifications,
157}
158
159impl Default for RoomState {
160 fn default() -> Self {
161 Self {
162 create: None,
163 members: BTreeMap::new(),
164 power_levels: RoomPowerLevelsContent {
165 ban: 50,
166 events: BTreeMap::new(),
167 events_default: 0,
168 invite: 50,
169 kick: 50,
170 redact: 50,
171 state_default: 50,
172 users: BTreeMap::new(),
173 users_default: 0,
174 notifications: RoomPowerLevelsNotifications::default(),
175 },
176 join_rules: None,
177 history_visibility: None,
178 name: None,
179 topic: None,
180 encryption: None,
181 encryption_tamper_attempts: 0,
182 pinned_events: Vec::new(),
183 heroes: Vec::new(),
184 joined_member_count: None,
185 invited_member_count: None,
186 unread_notifications: UnreadNotifications::default(),
187 }
188 }
189}
190
191impl RoomState {
192 pub fn new() -> Self {
196 Self::default()
197 }
198
199 pub fn apply_state_event(&mut self, event: &RawEvent) -> Result<(), MessengerError> {
205 let Some(state_key) = event.state_key.as_deref() else { return Ok(()) };
206 match event.event_type.as_str() {
207 EVENT_ROOM_CREATE if self.create.is_none() => {
208 self.create = Some(serde_json::from_value(event.content.clone())?);
209 }
210 EVENT_ROOM_MEMBER => {
211 let Ok(user_id) = UserId::parse(state_key) else { return Ok(()) };
212 let content: RoomMemberContent = serde_json::from_value(event.content.clone())?;
213 let invited_by = (content.membership == Membership::Invite).then(|| event.sender.clone());
214 self.members.insert(
215 user_id,
216 MemberState {
217 membership: content.membership,
218 displayname: content.displayname,
219 is_direct: content.is_direct.unwrap_or(false),
220 invited_by,
221 },
222 );
223 }
224 EVENT_ROOM_POWER_LEVELS => {
225 self.power_levels = serde_json::from_value(event.content.clone())?;
226 }
227 EVENT_ROOM_JOIN_RULES => {
228 self.join_rules = Some(serde_json::from_value(event.content.clone())?);
229 }
230 EVENT_ROOM_HISTORY_VISIBILITY => {
231 let content: RoomHistoryVisibilityContent = serde_json::from_value(event.content.clone())?;
232 self.history_visibility = Some(content.history_visibility);
233 }
234 EVENT_ROOM_NAME => {
235 let content: RoomNameContent = serde_json::from_value(event.content.clone())?;
236 self.name = Some(content.name);
237 }
238 EVENT_ROOM_TOPIC => {
239 let content: RoomTopicContent = serde_json::from_value(event.content.clone())?;
240 self.topic = Some(content.topic);
241 }
242 EVENT_ROOM_ENCRYPTION => {
243 let content: RoomEncryptionContent = serde_json::from_value(event.content.clone())?;
244 match &self.encryption {
245 None => self.encryption = Some(content),
246 Some(current) if current != &content => {
247 self.encryption_tamper_attempts += 1;
255 }
256 Some(_) => {} }
258 }
259 EVENT_ROOM_PINNED_EVENTS => {
260 let content: RoomPinnedEventsContent = serde_json::from_value(event.content.clone())?;
261 self.pinned_events = content.pinned;
262 }
263 _ => {}
264 }
265 Ok(())
266 }
267
268 pub fn apply_summary(&mut self, summary: &SyncRoomSummary, unread: &UnreadNotifications) {
274 self.heroes = summary.heroes.clone();
275 self.joined_member_count = summary.joined_member_count;
276 self.invited_member_count = summary.invited_member_count;
277 self.unread_notifications = unread.clone();
278 }
279
280 pub fn unread_count(&self) -> u64 {
284 self.unread_notifications.notification_count
285 }
286
287 pub fn power_level_for(&self, user_id: &UserId) -> i64 {
291 self.power_levels.users.get(user_id).copied().unwrap_or(self.power_levels.users_default)
292 }
293
294 pub fn member_role(&self, user_id: &UserId) -> MemberRole {
297 MemberRole::from_power_level(self.power_level_for(user_id))
298 }
299
300 pub fn active_member_count(&self) -> usize {
304 self.members.values().filter(|member| matches!(member.membership, Membership::Join | Membership::Invite)).count()
305 }
306
307 pub fn derive_room_kind(&self, room_id: &RoomId, direct_account_data: Option<&DirectContent>) -> RoomKind {
324 if self.is_direct(room_id, direct_account_data) {
325 RoomKind::Dm
326 } else if self.is_channel() {
327 RoomKind::Channel
328 } else {
329 RoomKind::Group
330 }
331 }
332
333 fn is_direct(&self, room_id: &RoomId, direct_account_data: Option<&DirectContent>) -> bool {
334 let listed_in_account_data =
335 direct_account_data.is_some_and(|direct| direct.0.values().any(|rooms| rooms.contains(room_id)));
336 let invite_flagged_and_still_a_pair =
337 self.active_member_count() == 2 && self.members.values().any(|member| member.is_direct);
338 listed_in_account_data || invite_flagged_and_still_a_pair
339 }
340
341 fn is_channel(&self) -> bool {
342 let is_public =
343 matches!(self.join_rules.as_ref().map(|join_rules| &join_rules.join_rule), Some(JoinRule::Public));
344 is_public && self.power_levels.events_default > self.power_levels.users_default
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 fn state_event(event_type: &str, state_key: &str, sender: &str, content: serde_json::Value) -> RawEvent {
353 serde_json::from_value(serde_json::json!({
354 "event_id": "$e:example.org",
355 "type": event_type,
356 "sender": sender,
357 "origin_server_ts": 1,
358 "state_key": state_key,
359 "content": content,
360 }))
361 .expect("valid state event")
362 }
363
364 #[test]
365 fn derive_room_kind_dm_vs_group_vs_channel() {
366 let room_id = RoomId::parse("!room:example.org").expect("valid room id");
367
368 let mut group = RoomState::new();
371 for user in ["@alice:example.org", "@bob:example.org", "@carol:example.org"] {
372 group
373 .apply_state_event(&state_event("m.room.member", user, user, serde_json::json!({ "membership": "join" })))
374 .expect("valid member event");
375 }
376 assert_eq!(group.derive_room_kind(&room_id, None), RoomKind::Group);
377
378 let mut dm = RoomState::new();
380 dm.apply_state_event(&state_event(
381 "m.room.member",
382 "@alice:example.org",
383 "@alice:example.org",
384 serde_json::json!({ "membership": "join" }),
385 ))
386 .expect("valid member event");
387 dm.apply_state_event(&state_event(
388 "m.room.member",
389 "@bob:example.org",
390 "@alice:example.org",
391 serde_json::json!({ "membership": "join", "is_direct": true }),
392 ))
393 .expect("valid member event");
394 assert_eq!(dm.derive_room_kind(&room_id, None), RoomKind::Dm);
395
396 let mut channel = RoomState::new();
399 channel
400 .apply_state_event(&state_event(
401 "m.room.join_rules",
402 "",
403 "@alice:example.org",
404 serde_json::json!({ "join_rule": "public" }),
405 ))
406 .expect("valid join rules event");
407 channel
408 .apply_state_event(&state_event(
409 "m.room.power_levels",
410 "",
411 "@alice:example.org",
412 serde_json::json!({ "events_default": 50 }),
413 ))
414 .expect("valid power levels event");
415 assert_eq!(channel.derive_room_kind(&room_id, None), RoomKind::Channel);
416 }
417
418 #[test]
419 fn derive_room_kind_dm_via_account_data_overrides_channel_shape() {
420 let room_id = RoomId::parse("!room:example.org").expect("valid room id");
421 let alice = UserId::parse("@alice:example.org").expect("valid user id");
422
423 let mut room = RoomState::new();
424 room.apply_state_event(&state_event(
425 "m.room.join_rules",
426 "",
427 "@alice:example.org",
428 serde_json::json!({ "join_rule": "public" }),
429 ))
430 .expect("valid join rules event");
431 room.apply_state_event(&state_event(
432 "m.room.power_levels",
433 "",
434 "@alice:example.org",
435 serde_json::json!({ "events_default": 50 }),
436 ))
437 .expect("valid power levels event");
438
439 let mut direct_rooms: BTreeMap<UserId, Vec<RoomId>> = BTreeMap::new();
443 direct_rooms.insert(alice, vec![room_id.clone()]);
444 let direct_content = DirectContent(direct_rooms);
445
446 assert_eq!(room.derive_room_kind(&room_id, Some(&direct_content)), RoomKind::Dm);
447 assert_eq!(room.derive_room_kind(&room_id, None), RoomKind::Channel);
448 }
449
450 #[test]
451 fn encryption_state_cannot_be_unset() {
452 let mut room = RoomState::new();
453 let megolm = serde_json::json!({ "algorithm": "m.megolm.v1.aes-sha2" });
454 room.apply_state_event(&state_event("m.room.encryption", "", "@alice:example.org", megolm.clone()))
455 .expect("valid encryption event");
456 let original = room.encryption.clone().expect("encryption set");
457 assert_eq!(room.encryption_tamper_attempts, 0);
458
459 let tampered = serde_json::json!({ "algorithm": "m.megolm.v1.aes-sha2", "rotation_period_msgs": 1 });
461 room.apply_state_event(&state_event("m.room.encryption", "", "@mallory:example.org", tampered))
462 .expect("valid encryption event");
463 assert_eq!(room.encryption, Some(original));
464 assert_eq!(room.encryption_tamper_attempts, 1);
465
466 room.apply_state_event(&state_event("m.room.encryption", "", "@alice:example.org", megolm))
469 .expect("valid encryption event");
470 assert_eq!(room.encryption_tamper_attempts, 1);
471 }
472
473 #[test]
474 fn member_role_uses_the_matrix_thresholds() {
475 let mut room = RoomState::new();
476 let owner = UserId::parse("@owner:example.org").expect("valid user id");
477 let admin = UserId::parse("@admin:example.org").expect("valid user id");
478 let plain = UserId::parse("@plain:example.org").expect("valid user id");
479 room.apply_state_event(&state_event(
480 "m.room.power_levels",
481 "",
482 "@owner:example.org",
483 serde_json::json!({ "users": { "@owner:example.org": 100, "@admin:example.org": 50 } }),
484 ))
485 .expect("valid power levels event");
486
487 assert_eq!(room.member_role(&owner), MemberRole::Owner);
488 assert_eq!(room.member_role(&admin), MemberRole::Admin);
489 assert_eq!(room.member_role(&plain), MemberRole::Member);
490 }
491}