use crate::error::MessengerError;
use crate::ids::{EventId, RoomId, UserId};
use crate::wire::events::{
DirectContent, HistoryVisibility, JoinRule, Membership, RawEvent, RoomCreateContent, RoomEncryptionContent,
RoomHistoryVisibilityContent, RoomJoinRulesContent, RoomMemberContent, RoomNameContent, RoomPinnedEventsContent,
RoomPowerLevelsContent, RoomPowerLevelsNotifications, RoomTopicContent,
};
use crate::wire::sync::{RoomSummary as SyncRoomSummary, UnreadNotifications};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
const EVENT_ROOM_CREATE: &str = "m.room.create";
const EVENT_ROOM_MEMBER: &str = "m.room.member";
const EVENT_ROOM_POWER_LEVELS: &str = "m.room.power_levels";
const EVENT_ROOM_JOIN_RULES: &str = "m.room.join_rules";
const EVENT_ROOM_HISTORY_VISIBILITY: &str = "m.room.history_visibility";
const EVENT_ROOM_NAME: &str = "m.room.name";
const EVENT_ROOM_TOPIC: &str = "m.room.topic";
const EVENT_ROOM_ENCRYPTION: &str = "m.room.encryption";
const EVENT_ROOM_PINNED_EVENTS: &str = "m.room.pinned_events";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MemberRole {
Owner,
Admin,
Member,
}
impl MemberRole {
pub fn from_power_level(power_level: i64) -> Self {
if power_level >= 100 {
MemberRole::Owner
} else if power_level >= 50 {
MemberRole::Admin
} else {
MemberRole::Member
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RoomKind {
Dm,
Group,
Channel,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemberState {
pub membership: Membership,
pub displayname: Option<String>,
pub is_direct: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RoomState {
pub create: Option<RoomCreateContent>,
pub members: BTreeMap<UserId, MemberState>,
pub power_levels: RoomPowerLevelsContent,
pub join_rules: Option<RoomJoinRulesContent>,
pub history_visibility: Option<HistoryVisibility>,
pub name: Option<String>,
pub topic: Option<String>,
pub encryption: Option<RoomEncryptionContent>,
pub encryption_tamper_attempts: u32,
pub pinned_events: Vec<EventId>,
pub heroes: Vec<UserId>,
pub joined_member_count: Option<u64>,
pub invited_member_count: Option<u64>,
pub unread_notifications: UnreadNotifications,
}
impl Default for RoomState {
fn default() -> Self {
Self {
create: None,
members: BTreeMap::new(),
power_levels: RoomPowerLevelsContent {
ban: 50,
events: BTreeMap::new(),
events_default: 0,
invite: 50,
kick: 50,
redact: 50,
state_default: 50,
users: BTreeMap::new(),
users_default: 0,
notifications: RoomPowerLevelsNotifications::default(),
},
join_rules: None,
history_visibility: None,
name: None,
topic: None,
encryption: None,
encryption_tamper_attempts: 0,
pinned_events: Vec::new(),
heroes: Vec::new(),
joined_member_count: None,
invited_member_count: None,
unread_notifications: UnreadNotifications::default(),
}
}
}
impl RoomState {
pub fn new() -> Self {
Self::default()
}
pub fn apply_state_event(&mut self, event: &RawEvent) -> Result<(), MessengerError> {
let Some(state_key) = event.state_key.as_deref() else { return Ok(()) };
match event.event_type.as_str() {
EVENT_ROOM_CREATE if self.create.is_none() => {
self.create = Some(serde_json::from_value(event.content.clone())?);
}
EVENT_ROOM_MEMBER => {
let Ok(user_id) = UserId::parse(state_key) else { return Ok(()) };
let content: RoomMemberContent = serde_json::from_value(event.content.clone())?;
self.members.insert(
user_id,
MemberState {
membership: content.membership,
displayname: content.displayname,
is_direct: content.is_direct.unwrap_or(false),
},
);
}
EVENT_ROOM_POWER_LEVELS => {
self.power_levels = serde_json::from_value(event.content.clone())?;
}
EVENT_ROOM_JOIN_RULES => {
self.join_rules = Some(serde_json::from_value(event.content.clone())?);
}
EVENT_ROOM_HISTORY_VISIBILITY => {
let content: RoomHistoryVisibilityContent = serde_json::from_value(event.content.clone())?;
self.history_visibility = Some(content.history_visibility);
}
EVENT_ROOM_NAME => {
let content: RoomNameContent = serde_json::from_value(event.content.clone())?;
self.name = Some(content.name);
}
EVENT_ROOM_TOPIC => {
let content: RoomTopicContent = serde_json::from_value(event.content.clone())?;
self.topic = Some(content.topic);
}
EVENT_ROOM_ENCRYPTION => {
let content: RoomEncryptionContent = serde_json::from_value(event.content.clone())?;
match &self.encryption {
None => self.encryption = Some(content),
Some(current) if current != &content => {
self.encryption_tamper_attempts += 1;
}
Some(_) => {} }
}
EVENT_ROOM_PINNED_EVENTS => {
let content: RoomPinnedEventsContent = serde_json::from_value(event.content.clone())?;
self.pinned_events = content.pinned;
}
_ => {}
}
Ok(())
}
pub fn apply_summary(&mut self, summary: &SyncRoomSummary, unread: &UnreadNotifications) {
self.heroes = summary.heroes.clone();
self.joined_member_count = summary.joined_member_count;
self.invited_member_count = summary.invited_member_count;
self.unread_notifications = unread.clone();
}
pub fn unread_count(&self) -> u64 {
self.unread_notifications.notification_count
}
pub fn power_level_for(&self, user_id: &UserId) -> i64 {
self.power_levels.users.get(user_id).copied().unwrap_or(self.power_levels.users_default)
}
pub fn member_role(&self, user_id: &UserId) -> MemberRole {
MemberRole::from_power_level(self.power_level_for(user_id))
}
pub fn active_member_count(&self) -> usize {
self.members.values().filter(|member| matches!(member.membership, Membership::Join | Membership::Invite)).count()
}
pub fn derive_room_kind(&self, room_id: &RoomId, direct_account_data: Option<&DirectContent>) -> RoomKind {
if self.is_direct(room_id, direct_account_data) {
RoomKind::Dm
} else if self.is_channel() {
RoomKind::Channel
} else {
RoomKind::Group
}
}
fn is_direct(&self, room_id: &RoomId, direct_account_data: Option<&DirectContent>) -> bool {
let listed_in_account_data =
direct_account_data.is_some_and(|direct| direct.0.values().any(|rooms| rooms.contains(room_id)));
let invite_flagged_and_still_a_pair =
self.active_member_count() == 2 && self.members.values().any(|member| member.is_direct);
listed_in_account_data || invite_flagged_and_still_a_pair
}
fn is_channel(&self) -> bool {
let is_public =
matches!(self.join_rules.as_ref().map(|join_rules| &join_rules.join_rule), Some(JoinRule::Public));
is_public && self.power_levels.events_default > self.power_levels.users_default
}
}
#[cfg(test)]
mod tests {
use super::*;
fn state_event(event_type: &str, state_key: &str, sender: &str, content: serde_json::Value) -> RawEvent {
serde_json::from_value(serde_json::json!({
"event_id": "$e:example.org",
"type": event_type,
"sender": sender,
"origin_server_ts": 1,
"state_key": state_key,
"content": content,
}))
.expect("valid state event")
}
#[test]
fn derive_room_kind_dm_vs_group_vs_channel() {
let room_id = RoomId::parse("!room:example.org").expect("valid room id");
let mut group = RoomState::new();
for user in ["@alice:example.org", "@bob:example.org", "@carol:example.org"] {
group
.apply_state_event(&state_event("m.room.member", user, user, serde_json::json!({ "membership": "join" })))
.expect("valid member event");
}
assert_eq!(group.derive_room_kind(&room_id, None), RoomKind::Group);
let mut dm = RoomState::new();
dm.apply_state_event(&state_event(
"m.room.member",
"@alice:example.org",
"@alice:example.org",
serde_json::json!({ "membership": "join" }),
))
.expect("valid member event");
dm.apply_state_event(&state_event(
"m.room.member",
"@bob:example.org",
"@alice:example.org",
serde_json::json!({ "membership": "join", "is_direct": true }),
))
.expect("valid member event");
assert_eq!(dm.derive_room_kind(&room_id, None), RoomKind::Dm);
let mut channel = RoomState::new();
channel
.apply_state_event(&state_event(
"m.room.join_rules",
"",
"@alice:example.org",
serde_json::json!({ "join_rule": "public" }),
))
.expect("valid join rules event");
channel
.apply_state_event(&state_event(
"m.room.power_levels",
"",
"@alice:example.org",
serde_json::json!({ "events_default": 50 }),
))
.expect("valid power levels event");
assert_eq!(channel.derive_room_kind(&room_id, None), RoomKind::Channel);
}
#[test]
fn derive_room_kind_dm_via_account_data_overrides_channel_shape() {
let room_id = RoomId::parse("!room:example.org").expect("valid room id");
let alice = UserId::parse("@alice:example.org").expect("valid user id");
let mut room = RoomState::new();
room.apply_state_event(&state_event(
"m.room.join_rules",
"",
"@alice:example.org",
serde_json::json!({ "join_rule": "public" }),
))
.expect("valid join rules event");
room.apply_state_event(&state_event(
"m.room.power_levels",
"",
"@alice:example.org",
serde_json::json!({ "events_default": 50 }),
))
.expect("valid power levels event");
let mut direct_rooms: BTreeMap<UserId, Vec<RoomId>> = BTreeMap::new();
direct_rooms.insert(alice, vec![room_id.clone()]);
let direct_content = DirectContent(direct_rooms);
assert_eq!(room.derive_room_kind(&room_id, Some(&direct_content)), RoomKind::Dm);
assert_eq!(room.derive_room_kind(&room_id, None), RoomKind::Channel);
}
#[test]
fn encryption_state_cannot_be_unset() {
let mut room = RoomState::new();
let megolm = serde_json::json!({ "algorithm": "m.megolm.v1.aes-sha2" });
room.apply_state_event(&state_event("m.room.encryption", "", "@alice:example.org", megolm.clone()))
.expect("valid encryption event");
let original = room.encryption.clone().expect("encryption set");
assert_eq!(room.encryption_tamper_attempts, 0);
let tampered = serde_json::json!({ "algorithm": "m.megolm.v1.aes-sha2", "rotation_period_msgs": 1 });
room.apply_state_event(&state_event("m.room.encryption", "", "@mallory:example.org", tampered))
.expect("valid encryption event");
assert_eq!(room.encryption, Some(original));
assert_eq!(room.encryption_tamper_attempts, 1);
room.apply_state_event(&state_event("m.room.encryption", "", "@alice:example.org", megolm))
.expect("valid encryption event");
assert_eq!(room.encryption_tamper_attempts, 1);
}
#[test]
fn member_role_uses_the_matrix_thresholds() {
let mut room = RoomState::new();
let owner = UserId::parse("@owner:example.org").expect("valid user id");
let admin = UserId::parse("@admin:example.org").expect("valid user id");
let plain = UserId::parse("@plain:example.org").expect("valid user id");
room.apply_state_event(&state_event(
"m.room.power_levels",
"",
"@owner:example.org",
serde_json::json!({ "users": { "@owner:example.org": 100, "@admin:example.org": 50 } }),
))
.expect("valid power levels event");
assert_eq!(room.member_role(&owner), MemberRole::Owner);
assert_eq!(room.member_role(&admin), MemberRole::Admin);
assert_eq!(room.member_role(&plain), MemberRole::Member);
}
}