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}
92
93#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
101pub struct RoomState {
102 pub create: Option<RoomCreateContent>,
107 pub members: BTreeMap<UserId, MemberState>,
113 pub power_levels: RoomPowerLevelsContent,
117 pub join_rules: Option<RoomJoinRulesContent>,
119 pub history_visibility: Option<HistoryVisibility>,
121 pub name: Option<String>,
123 pub topic: Option<String>,
125 pub encryption: Option<RoomEncryptionContent>,
129 pub encryption_tamper_attempts: u32,
136 pub pinned_events: Vec<EventId>,
138 pub heroes: Vec<UserId>,
143 pub joined_member_count: Option<u64>,
145 pub invited_member_count: Option<u64>,
147 pub unread_notifications: UnreadNotifications,
153}
154
155impl Default for RoomState {
156 fn default() -> Self {
157 Self {
158 create: None,
159 members: BTreeMap::new(),
160 power_levels: RoomPowerLevelsContent {
161 ban: 50,
162 events: BTreeMap::new(),
163 events_default: 0,
164 invite: 50,
165 kick: 50,
166 redact: 50,
167 state_default: 50,
168 users: BTreeMap::new(),
169 users_default: 0,
170 notifications: RoomPowerLevelsNotifications::default(),
171 },
172 join_rules: None,
173 history_visibility: None,
174 name: None,
175 topic: None,
176 encryption: None,
177 encryption_tamper_attempts: 0,
178 pinned_events: Vec::new(),
179 heroes: Vec::new(),
180 joined_member_count: None,
181 invited_member_count: None,
182 unread_notifications: UnreadNotifications::default(),
183 }
184 }
185}
186
187impl RoomState {
188 pub fn new() -> Self {
192 Self::default()
193 }
194
195 pub fn apply_state_event(&mut self, event: &RawEvent) -> Result<(), MessengerError> {
201 let Some(state_key) = event.state_key.as_deref() else { return Ok(()) };
202 match event.event_type.as_str() {
203 EVENT_ROOM_CREATE if self.create.is_none() => {
204 self.create = Some(serde_json::from_value(event.content.clone())?);
205 }
206 EVENT_ROOM_MEMBER => {
207 let Ok(user_id) = UserId::parse(state_key) else { return Ok(()) };
208 let content: RoomMemberContent = serde_json::from_value(event.content.clone())?;
209 self.members.insert(
210 user_id,
211 MemberState {
212 membership: content.membership,
213 displayname: content.displayname,
214 is_direct: content.is_direct.unwrap_or(false),
215 },
216 );
217 }
218 EVENT_ROOM_POWER_LEVELS => {
219 self.power_levels = serde_json::from_value(event.content.clone())?;
220 }
221 EVENT_ROOM_JOIN_RULES => {
222 self.join_rules = Some(serde_json::from_value(event.content.clone())?);
223 }
224 EVENT_ROOM_HISTORY_VISIBILITY => {
225 let content: RoomHistoryVisibilityContent = serde_json::from_value(event.content.clone())?;
226 self.history_visibility = Some(content.history_visibility);
227 }
228 EVENT_ROOM_NAME => {
229 let content: RoomNameContent = serde_json::from_value(event.content.clone())?;
230 self.name = Some(content.name);
231 }
232 EVENT_ROOM_TOPIC => {
233 let content: RoomTopicContent = serde_json::from_value(event.content.clone())?;
234 self.topic = Some(content.topic);
235 }
236 EVENT_ROOM_ENCRYPTION => {
237 let content: RoomEncryptionContent = serde_json::from_value(event.content.clone())?;
238 match &self.encryption {
239 None => self.encryption = Some(content),
240 Some(current) if current != &content => {
241 self.encryption_tamper_attempts += 1;
249 }
250 Some(_) => {} }
252 }
253 EVENT_ROOM_PINNED_EVENTS => {
254 let content: RoomPinnedEventsContent = serde_json::from_value(event.content.clone())?;
255 self.pinned_events = content.pinned;
256 }
257 _ => {}
258 }
259 Ok(())
260 }
261
262 pub fn apply_summary(&mut self, summary: &SyncRoomSummary, unread: &UnreadNotifications) {
268 self.heroes = summary.heroes.clone();
269 self.joined_member_count = summary.joined_member_count;
270 self.invited_member_count = summary.invited_member_count;
271 self.unread_notifications = unread.clone();
272 }
273
274 pub fn unread_count(&self) -> u64 {
278 self.unread_notifications.notification_count
279 }
280
281 pub fn power_level_for(&self, user_id: &UserId) -> i64 {
285 self.power_levels.users.get(user_id).copied().unwrap_or(self.power_levels.users_default)
286 }
287
288 pub fn member_role(&self, user_id: &UserId) -> MemberRole {
291 MemberRole::from_power_level(self.power_level_for(user_id))
292 }
293
294 pub fn active_member_count(&self) -> usize {
298 self.members.values().filter(|member| matches!(member.membership, Membership::Join | Membership::Invite)).count()
299 }
300
301 pub fn derive_room_kind(&self, room_id: &RoomId, direct_account_data: Option<&DirectContent>) -> RoomKind {
318 if self.is_direct(room_id, direct_account_data) {
319 RoomKind::Dm
320 } else if self.is_channel() {
321 RoomKind::Channel
322 } else {
323 RoomKind::Group
324 }
325 }
326
327 fn is_direct(&self, room_id: &RoomId, direct_account_data: Option<&DirectContent>) -> bool {
328 let listed_in_account_data =
329 direct_account_data.is_some_and(|direct| direct.0.values().any(|rooms| rooms.contains(room_id)));
330 let invite_flagged_and_still_a_pair =
331 self.active_member_count() == 2 && self.members.values().any(|member| member.is_direct);
332 listed_in_account_data || invite_flagged_and_still_a_pair
333 }
334
335 fn is_channel(&self) -> bool {
336 let is_public =
337 matches!(self.join_rules.as_ref().map(|join_rules| &join_rules.join_rule), Some(JoinRule::Public));
338 is_public && self.power_levels.events_default > self.power_levels.users_default
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 fn state_event(event_type: &str, state_key: &str, sender: &str, content: serde_json::Value) -> RawEvent {
347 serde_json::from_value(serde_json::json!({
348 "event_id": "$e:example.org",
349 "type": event_type,
350 "sender": sender,
351 "origin_server_ts": 1,
352 "state_key": state_key,
353 "content": content,
354 }))
355 .expect("valid state event")
356 }
357
358 #[test]
359 fn derive_room_kind_dm_vs_group_vs_channel() {
360 let room_id = RoomId::parse("!room:example.org").expect("valid room id");
361
362 let mut group = RoomState::new();
365 for user in ["@alice:example.org", "@bob:example.org", "@carol:example.org"] {
366 group
367 .apply_state_event(&state_event("m.room.member", user, user, serde_json::json!({ "membership": "join" })))
368 .expect("valid member event");
369 }
370 assert_eq!(group.derive_room_kind(&room_id, None), RoomKind::Group);
371
372 let mut dm = RoomState::new();
374 dm.apply_state_event(&state_event(
375 "m.room.member",
376 "@alice:example.org",
377 "@alice:example.org",
378 serde_json::json!({ "membership": "join" }),
379 ))
380 .expect("valid member event");
381 dm.apply_state_event(&state_event(
382 "m.room.member",
383 "@bob:example.org",
384 "@alice:example.org",
385 serde_json::json!({ "membership": "join", "is_direct": true }),
386 ))
387 .expect("valid member event");
388 assert_eq!(dm.derive_room_kind(&room_id, None), RoomKind::Dm);
389
390 let mut channel = RoomState::new();
393 channel
394 .apply_state_event(&state_event(
395 "m.room.join_rules",
396 "",
397 "@alice:example.org",
398 serde_json::json!({ "join_rule": "public" }),
399 ))
400 .expect("valid join rules event");
401 channel
402 .apply_state_event(&state_event(
403 "m.room.power_levels",
404 "",
405 "@alice:example.org",
406 serde_json::json!({ "events_default": 50 }),
407 ))
408 .expect("valid power levels event");
409 assert_eq!(channel.derive_room_kind(&room_id, None), RoomKind::Channel);
410 }
411
412 #[test]
413 fn derive_room_kind_dm_via_account_data_overrides_channel_shape() {
414 let room_id = RoomId::parse("!room:example.org").expect("valid room id");
415 let alice = UserId::parse("@alice:example.org").expect("valid user id");
416
417 let mut room = RoomState::new();
418 room.apply_state_event(&state_event(
419 "m.room.join_rules",
420 "",
421 "@alice:example.org",
422 serde_json::json!({ "join_rule": "public" }),
423 ))
424 .expect("valid join rules event");
425 room.apply_state_event(&state_event(
426 "m.room.power_levels",
427 "",
428 "@alice:example.org",
429 serde_json::json!({ "events_default": 50 }),
430 ))
431 .expect("valid power levels event");
432
433 let mut direct_rooms: BTreeMap<UserId, Vec<RoomId>> = BTreeMap::new();
437 direct_rooms.insert(alice, vec![room_id.clone()]);
438 let direct_content = DirectContent(direct_rooms);
439
440 assert_eq!(room.derive_room_kind(&room_id, Some(&direct_content)), RoomKind::Dm);
441 assert_eq!(room.derive_room_kind(&room_id, None), RoomKind::Channel);
442 }
443
444 #[test]
445 fn encryption_state_cannot_be_unset() {
446 let mut room = RoomState::new();
447 let megolm = serde_json::json!({ "algorithm": "m.megolm.v1.aes-sha2" });
448 room.apply_state_event(&state_event("m.room.encryption", "", "@alice:example.org", megolm.clone()))
449 .expect("valid encryption event");
450 let original = room.encryption.clone().expect("encryption set");
451 assert_eq!(room.encryption_tamper_attempts, 0);
452
453 let tampered = serde_json::json!({ "algorithm": "m.megolm.v1.aes-sha2", "rotation_period_msgs": 1 });
455 room.apply_state_event(&state_event("m.room.encryption", "", "@mallory:example.org", tampered))
456 .expect("valid encryption event");
457 assert_eq!(room.encryption, Some(original));
458 assert_eq!(room.encryption_tamper_attempts, 1);
459
460 room.apply_state_event(&state_event("m.room.encryption", "", "@alice:example.org", megolm))
463 .expect("valid encryption event");
464 assert_eq!(room.encryption_tamper_attempts, 1);
465 }
466
467 #[test]
468 fn member_role_uses_the_matrix_thresholds() {
469 let mut room = RoomState::new();
470 let owner = UserId::parse("@owner:example.org").expect("valid user id");
471 let admin = UserId::parse("@admin:example.org").expect("valid user id");
472 let plain = UserId::parse("@plain:example.org").expect("valid user id");
473 room.apply_state_event(&state_event(
474 "m.room.power_levels",
475 "",
476 "@owner:example.org",
477 serde_json::json!({ "users": { "@owner:example.org": 100, "@admin:example.org": 50 } }),
478 ))
479 .expect("valid power levels event");
480
481 assert_eq!(room.member_role(&owner), MemberRole::Owner);
482 assert_eq!(room.member_role(&admin), MemberRole::Admin);
483 assert_eq!(room.member_role(&plain), MemberRole::Member);
484 }
485}