#![allow(clippy::assign_op_pattern)] mod members;
pub(crate) mod normal;
use std::{
collections::{BTreeMap, HashSet},
fmt,
hash::Hash,
};
use bitflags::bitflags;
pub use members::RoomMember;
pub use normal::{Room, RoomInfo, RoomState, RoomStateFilter};
use ruma::{
assign,
events::{
call::member::CallMemberEventContent,
macros::EventContent,
room::{
avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
create::{PreviousRoom, RoomCreateEventContent},
encryption::RoomEncryptionEventContent,
guest_access::RoomGuestAccessEventContent,
history_visibility::RoomHistoryVisibilityEventContent,
join_rules::RoomJoinRulesEventContent,
member::MembershipState,
name::RoomNameEventContent,
tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent,
},
AnyStrippedStateEvent, AnySyncStateEvent, EmptyStateKey, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncStateEvent,
},
room::RoomType,
EventId, OwnedUserId, RoomVersionId,
};
use serde::{Deserialize, Serialize};
use crate::MinimalStateEvent;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum DisplayName {
Named(String),
Aliased(String),
Calculated(String),
EmptyWas(String),
Empty,
}
impl fmt::Display for DisplayName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DisplayName::Named(s) | DisplayName::Calculated(s) | DisplayName::Aliased(s) => {
write!(f, "{s}")
}
DisplayName::EmptyWas(s) => write!(f, "Empty Room (was {s})"),
DisplayName::Empty => write!(f, "Empty Room"),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BaseRoomInfo {
pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
pub(crate) dm_targets: HashSet<OwnedUserId>,
pub(crate) encryption: Option<RoomEncryptionEventContent>,
pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
pub(crate) max_power_level: i64,
pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
pub(crate) rtc_member: BTreeMap<OwnedUserId, MinimalStateEvent<CallMemberEventContent>>,
}
impl BaseRoomInfo {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn calculate_room_name(
&self,
joined_member_count: u64,
invited_member_count: u64,
heroes: Vec<RoomMember>,
) -> DisplayName {
calculate_room_name(
joined_member_count,
invited_member_count,
heroes.iter().take(3).map(|mem| mem.name()).collect::<Vec<&str>>(),
)
}
pub fn room_version(&self) -> Option<&RoomVersionId> {
match self.create.as_ref()? {
MinimalStateEvent::Original(ev) => Some(&ev.content.room_version),
MinimalStateEvent::Redacted(ev) => Some(&ev.content.room_version),
}
}
pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
match ev {
AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
self.encryption = Some(encryption.content.clone());
}
AnySyncStateEvent::RoomAvatar(a) => {
self.avatar = Some(a.into());
}
AnySyncStateEvent::RoomName(n) => {
self.name = Some(n.into());
}
AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
self.create = Some(c.into());
}
AnySyncStateEvent::RoomHistoryVisibility(h) => {
self.history_visibility = Some(h.into());
}
AnySyncStateEvent::RoomGuestAccess(g) => {
self.guest_access = Some(g.into());
}
AnySyncStateEvent::RoomJoinRules(c) => {
self.join_rules = Some(c.into());
}
AnySyncStateEvent::RoomCanonicalAlias(a) => {
self.canonical_alias = Some(a.into());
}
AnySyncStateEvent::RoomTopic(t) => {
self.topic = Some(t.into());
}
AnySyncStateEvent::RoomTombstone(t) => {
self.tombstone = Some(t.into());
}
AnySyncStateEvent::RoomPowerLevels(p) => {
self.max_power_level = p.power_levels().max().into();
}
AnySyncStateEvent::CallMember(m) => {
let Some(o_ev) = m.as_original() else {
return false;
};
let mut o_ev = o_ev.clone();
o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
self.rtc_member
.insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
self.rtc_member.retain(|_, ev| {
ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
});
}
_ => return false,
}
true
}
pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
match ev {
AnyStrippedStateEvent::RoomEncryption(encryption) => {
if let Some(algorithm) = &encryption.content.algorithm {
let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
rotation_period_ms: encryption.content.rotation_period_ms,
rotation_period_msgs: encryption.content.rotation_period_msgs,
});
self.encryption = Some(content);
}
}
AnyStrippedStateEvent::RoomAvatar(a) => {
self.avatar = Some(a.into());
}
AnyStrippedStateEvent::RoomName(n) => {
self.name = Some(n.into());
}
AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
self.create = Some(c.into());
}
AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
self.history_visibility = Some(h.into());
}
AnyStrippedStateEvent::RoomGuestAccess(g) => {
self.guest_access = Some(g.into());
}
AnyStrippedStateEvent::RoomJoinRules(c) => {
self.join_rules = Some(c.into());
}
AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
self.canonical_alias = Some(a.into());
}
AnyStrippedStateEvent::RoomTopic(t) => {
self.topic = Some(t.into());
}
AnyStrippedStateEvent::RoomTombstone(t) => {
self.tombstone = Some(t.into());
}
AnyStrippedStateEvent::RoomPowerLevels(p) => {
self.max_power_level = p.power_levels().max().into();
}
AnyStrippedStateEvent::CallMember(_) => {
return false;
}
_ => return false,
}
true
}
fn handle_redaction(&mut self, redacts: &EventId) {
let room_version = self.room_version().unwrap_or(&RoomVersionId::V1).to_owned();
if self.avatar.has_event_id(redacts) {
self.avatar.as_mut().unwrap().redact(&room_version);
} else if self.canonical_alias.has_event_id(redacts) {
self.canonical_alias.as_mut().unwrap().redact(&room_version);
} else if self.create.has_event_id(redacts) {
self.create.as_mut().unwrap().redact(&room_version);
} else if self.guest_access.has_event_id(redacts) {
self.guest_access.as_mut().unwrap().redact(&room_version);
} else if self.history_visibility.has_event_id(redacts) {
self.history_visibility.as_mut().unwrap().redact(&room_version);
} else if self.join_rules.has_event_id(redacts) {
self.join_rules.as_mut().unwrap().redact(&room_version);
} else if self.name.has_event_id(redacts) {
self.name.as_mut().unwrap().redact(&room_version);
} else if self.tombstone.has_event_id(redacts) {
self.tombstone.as_mut().unwrap().redact(&room_version);
} else if self.topic.has_event_id(redacts) {
self.topic.as_mut().unwrap().redact(&room_version);
} else {
self.rtc_member.retain(|_, member_event| member_event.event_id() != Some(redacts));
}
}
}
trait OptionExt {
fn has_event_id(&self, ev_id: &EventId) -> bool;
}
impl<C> OptionExt for Option<MinimalStateEvent<C>>
where
C: StaticStateEventContent + RedactContent,
C::Redacted: RedactedStateEventContent,
{
fn has_event_id(&self, ev_id: &EventId) -> bool {
self.as_ref().is_some_and(|ev| ev.event_id() == Some(ev_id))
}
}
impl Default for BaseRoomInfo {
fn default() -> Self {
Self {
avatar: None,
canonical_alias: None,
create: None,
dm_targets: Default::default(),
encryption: None,
guest_access: None,
history_visibility: None,
join_rules: None,
max_power_level: 100,
name: None,
tombstone: None,
topic: None,
rtc_member: BTreeMap::new(),
}
}
}
fn calculate_room_name(
joined_member_count: u64,
invited_member_count: u64,
heroes: Vec<&str>,
) -> DisplayName {
let heroes_count = heroes.len() as u64;
let invited_joined = invited_member_count + joined_member_count;
let invited_joined_minus_one = invited_joined.saturating_sub(1);
let names = if heroes_count >= invited_joined_minus_one {
let mut names = heroes;
names.sort_unstable();
names.join(", ")
} else if heroes_count < invited_joined_minus_one && invited_joined > 1 {
let mut names = heroes;
names.sort_unstable();
format!("{}, and {} others", names.join(", "), (invited_joined - heroes_count))
} else {
"".to_owned()
};
if invited_joined <= 1 {
if names.is_empty() {
DisplayName::Empty
} else {
DisplayName::EmptyWas(names)
}
} else {
DisplayName::Calculated(names)
}
}
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "m.room.create", kind = State, state_key_type = EmptyStateKey, custom_redacted)]
pub struct RoomCreateWithCreatorEventContent {
pub creator: OwnedUserId,
#[serde(
rename = "m.federate",
default = "ruma::serde::default_true",
skip_serializing_if = "ruma::serde::is_true"
)]
pub federate: bool,
#[serde(default = "default_create_room_version_id")]
pub room_version: RoomVersionId,
#[serde(skip_serializing_if = "Option::is_none")]
pub predecessor: Option<PreviousRoom>,
#[serde(skip_serializing_if = "Option::is_none", rename = "type")]
pub room_type: Option<RoomType>,
}
impl RoomCreateWithCreatorEventContent {
pub fn from_event_content(content: RoomCreateEventContent, sender: OwnedUserId) -> Self {
let RoomCreateEventContent { federate, room_version, predecessor, room_type, .. } = content;
Self { creator: sender, federate, room_version, predecessor, room_type }
}
fn into_event_content(self) -> (RoomCreateEventContent, OwnedUserId) {
let Self { creator, federate, room_version, predecessor, room_type } = self;
#[allow(deprecated)]
let content = assign!(RoomCreateEventContent::new_v11(), {
creator: Some(creator.clone()),
federate,
room_version,
predecessor,
room_type,
});
(content, creator)
}
}
pub type RedactedRoomCreateWithCreatorEventContent = RoomCreateWithCreatorEventContent;
impl RedactedStateEventContent for RedactedRoomCreateWithCreatorEventContent {
type StateKey = EmptyStateKey;
}
impl RedactContent for RoomCreateWithCreatorEventContent {
type Redacted = RedactedRoomCreateWithCreatorEventContent;
fn redact(self, version: &RoomVersionId) -> Self::Redacted {
let (content, sender) = self.into_event_content();
let content = content.redact(version);
Self::from_event_content(content, sender)
}
}
fn default_create_room_version_id() -> RoomVersionId {
RoomVersionId::V1
}
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct RoomMemberships: u16 {
const JOIN = 0b00000001;
const INVITE = 0b00000010;
const KNOCK = 0b00000100;
const LEAVE = 0b00001000;
const BAN = 0b00010000;
const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
}
}
impl RoomMemberships {
pub fn matches(&self, membership: &MembershipState) -> bool {
if self.is_empty() {
return true;
}
let membership = match membership {
MembershipState::Ban => Self::BAN,
MembershipState::Invite => Self::INVITE,
MembershipState::Join => Self::JOIN,
MembershipState::Knock => Self::KNOCK,
MembershipState::Leave => Self::LEAVE,
_ => return false,
};
self.contains(membership)
}
pub fn as_vec(&self) -> Vec<MembershipState> {
let mut memberships = Vec::new();
if self.contains(Self::JOIN) {
memberships.push(MembershipState::Join);
}
if self.contains(Self::INVITE) {
memberships.push(MembershipState::Invite);
}
if self.contains(Self::KNOCK) {
memberships.push(MembershipState::Knock);
}
if self.contains(Self::LEAVE) {
memberships.push(MembershipState::Leave);
}
if self.contains(Self::BAN) {
memberships.push(MembershipState::Ban);
}
memberships
}
}
#[cfg(test)]
mod tests {
use super::{calculate_room_name, DisplayName};
#[test]
fn test_calculate_room_name() {
let mut actual = calculate_room_name(2, 0, vec!["a"]);
assert_eq!(DisplayName::Calculated("a".to_owned()), actual);
actual = calculate_room_name(3, 0, vec!["a", "b"]);
assert_eq!(DisplayName::Calculated("a, b".to_owned()), actual);
actual = calculate_room_name(4, 0, vec!["a", "b", "c"]);
assert_eq!(DisplayName::Calculated("a, b, c".to_owned()), actual);
actual = calculate_room_name(5, 0, vec!["a", "b", "c"]);
assert_eq!(DisplayName::Calculated("a, b, c, and 2 others".to_owned()), actual);
actual = calculate_room_name(0, 0, vec![]);
assert_eq!(DisplayName::Empty, actual);
actual = calculate_room_name(1, 0, vec![]);
assert_eq!(DisplayName::Empty, actual);
actual = calculate_room_name(0, 1, vec![]);
assert_eq!(DisplayName::Empty, actual);
actual = calculate_room_name(1, 0, vec!["a"]);
assert_eq!(DisplayName::EmptyWas("a".to_owned()), actual);
actual = calculate_room_name(1, 0, vec!["a", "b"]);
assert_eq!(DisplayName::EmptyWas("a, b".to_owned()), actual);
actual = calculate_room_name(1, 0, vec!["a", "b", "c"]);
assert_eq!(DisplayName::EmptyWas("a, b, c".to_owned()), actual);
}
}