diff --git a/examples/login.rs b/examples/login.rs index a623016d..088895e6 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -25,10 +25,10 @@ async fn async_cb(room: Arc>, event: Arc>) { .. }) = event { - let user = room.members.get(&sender.to_string()).unwrap(); + let member = room.members.get(&sender.to_string()).unwrap(); println!( "{}: {}", - user.display_name.as_ref().unwrap_or(&sender.to_string()), + member.user.display_name.as_ref().unwrap_or(&sender.to_string()), msg_body ); } diff --git a/src/async_client.rs b/src/async_client.rs index 6ca98e88..8fe5d916 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -37,7 +37,8 @@ use ruma_identifiers::RoomId; use crate::api; use crate::base_client::Client as BaseClient; -use crate::base_client::Room; +use crate::models::Room; +use crate::error::{Error, InnerError}; use crate::session::Session; use crate::VERSION; use crate::{Error, Result}; @@ -265,7 +266,7 @@ impl AsyncClient { pub fn base_client(&self) -> Arc> { Arc::clone(&self.base_client) } - + /// Calculate the room name from a `RoomId`, returning a string. pub async fn get_room_name(&self, room_id: &str) -> Option { self.base_client.read().await.calculate_room_name(room_id) @@ -309,10 +310,10 @@ impl AsyncClient { /// .. /// }) = event /// { - /// let user = room.members.get(&sender.to_string()).unwrap(); + /// let member = room.members.get(&sender.to_string()).unwrap(); /// println!( /// "{}: {}", - /// user.display_name.as_ref().unwrap_or(&sender.to_string()), + /// member.user.display_name.as_ref().unwrap_or(&sender.to_string()), /// msg_body /// ); /// } @@ -423,6 +424,15 @@ impl AsyncClient { if let Some(e) = decrypted_event { *event = e; } + + for presence in &response.presence.events { + let mut client = self.base_client.write().await; + if let EventResult::Ok(e) = presence { + client.receive_presence_event(&room_id, e); + } + } + + let event = Arc::new(event.clone()); let callbacks = { let mut cb_futures = self.event_callbacks.lock().unwrap(); diff --git a/src/base_client.rs b/src/base_client.rs index 43952fd0..93b8b6b7 100644 --- a/src/base_client.rs +++ b/src/base_client.rs @@ -27,9 +27,11 @@ use crate::events::room::{ member::{MemberEvent, MembershipState}, name::NameEvent, }; +use crate::events::presence::PresenceEvent; use crate::events::EventResult; use crate::identifiers::RoomAliasId; use crate::session::Session; +use crate::models::{Room}; use std::sync::{Arc, RwLock}; #[cfg(feature = "encryption")] @@ -55,250 +57,6 @@ pub struct RoomName { aliases: Vec, } -#[derive(Debug)] -/// A Matrix room member. -pub struct RoomMember { - /// The unique mxid of the user. - pub user_id: UserId, - /// The human readable name of the user. - pub display_name: Option, - /// The matrix url of the users avatar. - pub avatar_url: Option, - /// The users power level. - pub power_level: u8, -} - -#[derive(Debug)] -/// A Matrix rooom. -pub struct Room { - /// The unique id of the room. - pub room_id: String, - /// The name of the room, clients use this to represent a room. - pub room_name: RoomName, - /// The mxid of our own user. - pub own_user_id: UserId, - /// The mxid of the room creator. - pub creator: Option, - /// The map of room members. - pub members: HashMap, - /// A list of users that are currently typing. - pub typing_users: Vec, - /// A flag indicating if the room is encrypted. - pub encrypted: bool, -} - -impl RoomName { - pub fn push_alias(&mut self, alias: RoomAliasId) -> bool { - self.aliases.push(alias); - true - } - - pub fn set_canonical(&mut self, alias: RoomAliasId) -> bool { - self.canonical_alias = Some(alias); - true - } - - pub fn set_name(&mut self, name: &str) -> bool { - self.name = Some(name.to_string()); - true - } - - pub fn calculate_name(&self, room_id: &str, members: &HashMap) -> String { - // https://github.com/matrix-org/matrix-js-sdk/blob/33941eb37bffe41958ba9887fc8070dfb1a0ee76/src/models/room.js#L1823 - // the order in which we check for a name ^^ - if let Some(name) = &self.name { - name.clone() - } else if let Some(alias) = &self.canonical_alias { - alias.alias().to_string() - } else if !self.aliases.is_empty() { - self.aliases[0].alias().to_string() - } else { - // TODO - let mut names = members - .values() - .flat_map(|m| m.display_name.clone()) - .take(3) - .collect::>(); - - if names.is_empty() { - // TODO implement the rest of matrix-js-sdk handling of room names - format!("Room {}", room_id) - } else { - // stabilize order - names.sort(); - names.join(", ") - } - } - } -} - -impl Room { - /// Create a new room. - /// - /// # Arguments - /// - /// * `room_id` - The unique id of the room. - /// - /// * `own_user_id` - The mxid of our own user. - pub fn new(room_id: &str, own_user_id: &str) -> Self { - Room { - room_id: room_id.to_string(), - room_name: RoomName::default(), - own_user_id: own_user_id.to_owned(), - creator: None, - members: HashMap::new(), - typing_users: Vec::new(), - encrypted: false, - } - } - - fn add_member(&mut self, event: &MemberEvent) -> bool { - if self.members.contains_key(&event.state_key) { - return false; - } - - let member = RoomMember { - user_id: event.state_key.clone(), - display_name: event.content.displayname.clone(), - avatar_url: event.content.avatar_url.clone(), - power_level: 0, - }; - - self.members.insert(event.state_key.clone(), member); - - true - } - - fn remove_member(&mut self, event: &MemberEvent) -> bool { - if !self.members.contains_key(&event.state_key) { - return false; - } - - true - } - - fn update_joined_member(&mut self, event: &MemberEvent) -> bool { - if let Some(member) = self.members.get_mut(&event.state_key) { - member.display_name = event.content.displayname.clone(); - member.avatar_url = event.content.avatar_url.clone(); - } - - false - } - - fn handle_join(&mut self, event: &MemberEvent) -> bool { - match &event.prev_content { - Some(c) => match c.membership { - MembershipState::Join => self.update_joined_member(event), - MembershipState::Invite => self.add_member(event), - MembershipState::Leave => self.remove_member(event), - _ => false, - }, - None => self.add_member(event), - } - } - - fn handle_leave(&mut self, _event: &MemberEvent) -> bool { - false - } - - /// Handle a room.member updating the room state if necessary. - /// - /// Returns true if the joined member list changed, false otherwise. - pub fn handle_membership(&mut self, event: &MemberEvent) -> bool { - match event.content.membership { - MembershipState::Join => self.handle_join(event), - MembershipState::Leave => self.handle_leave(event), - MembershipState::Ban => self.handle_leave(event), - MembershipState::Invite => false, - MembershipState::Knock => false, - _ => false, - } - } - - /// Add to the list of `RoomAliasId`s. - fn room_aliases(&mut self, alias: &RoomAliasId) -> bool { - self.room_name.push_alias(alias.clone()); - true - } - - /// RoomAliasId is `#alias:hostname` and `port` - fn canonical_alias(&mut self, alias: &RoomAliasId) -> bool { - self.room_name.set_canonical(alias.clone()); - true - } - - fn name_room(&mut self, name: &str) -> bool { - self.room_name.set_name(name); - true - } - - /// Handle a room.aliases event, updating the room state if necessary. - /// - /// Returns true if the room name changed, false otherwise. - pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool { - match event.content.aliases.as_slice() { - [alias] => self.room_aliases(alias), - [alias, ..] => self.room_aliases(alias), - _ => false, - } - } - - /// Handle a room.canonical_alias event, updating the room state if necessary. - /// - /// Returns true if the room name changed, false otherwise. - pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool { - match &event.content.alias { - Some(name) => self.canonical_alias(&name), - _ => false, - } - } - - /// Handle a room.name event, updating the room state if necessary. - /// - /// Returns true if the room name changed, false otherwise. - pub fn handle_room_name(&mut self, event: &NameEvent) -> bool { - match event.content.name() { - Some(name) => self.name_room(name), - _ => false, - } - } - - /// Receive a timeline event for this room and update the room state. - /// - /// Returns true if the joined member list changed, false otherwise. - /// - /// # Arguments - /// - /// * `event` - The event of the room. - pub fn receive_timeline_event(&mut self, event: &RoomEvent) -> bool { - match event { - RoomEvent::RoomMember(m) => self.handle_membership(m), - RoomEvent::RoomName(n) => self.handle_room_name(n), - RoomEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), - RoomEvent::RoomAliases(a) => self.handle_room_aliases(a), - _ => false, - } - } - - /// Receive a state event for this room and update the room state. - /// - /// Returns true if the joined member list changed, false otherwise. - /// - /// # Arguments - /// - /// * `event` - The event of the room. - pub fn receive_state_event(&mut self, event: &StateEvent) -> bool { - match event { - StateEvent::RoomMember(m) => self.handle_membership(m), - StateEvent::RoomName(n) => self.handle_room_name(n), - StateEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), - StateEvent::RoomAliases(a) => self.handle_room_aliases(a), - _ => false, - } - } -} - #[derive(Debug)] /// A no IO Client implementation. /// @@ -467,6 +225,21 @@ impl Client { room.receive_state_event(event) } + /// Receive a presence event from an `IncomingResponse` and updates the client state. + /// + /// Returns true if the membership list of the room changed, false + /// otherwise. + /// + /// # Arguments + /// + /// * `room_id` - The unique id of the room the event belongs to. + /// + /// * `event` - The event that should be handled by the client. + pub fn receive_presence_event(&mut self, room_id: &str, event: &PresenceEvent) -> bool { + let mut room = self.get_or_create_room(room_id).write().unwrap(); + room.receive_presence_event(event) + } + /// Receive a response from a sync call. /// /// # Arguments diff --git a/src/event_emitter/mod.rs b/src/event_emitter/mod.rs new file mode 100644 index 00000000..7bce3fc1 --- /dev/null +++ b/src/event_emitter/mod.rs @@ -0,0 +1,44 @@ +// Copyright 2020 Damir Jelić +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::api::r0 as api; +use crate::events::collections::all::{Event, RoomEvent, StateEvent}; +use crate::events::room::{ + aliases::AliasesEvent, + canonical_alias::CanonicalAliasEvent, + member::{MemberEvent, MemberEventContent, MembershipState}, + name::NameEvent, +}; +use crate::events::EventResult; +use crate::identifiers::RoomAliasId; +use crate::session::Session; +use crate::models::Room; + +use js_int::{Int, UInt}; +#[cfg(feature = "encryption")] +use tokio::sync::Mutex; + +#[cfg(feature = "encryption")] +use crate::crypto::{OlmMachine, OneTimeKeys}; +#[cfg(feature = "encryption")] +use ruma_client_api::r0::keys::{upload_keys::Response as KeysUploadResponse, DeviceKeys}; + +pub trait EventEmitter { + fn on_room_name(&mut self, _: &Room) {} + fn on_room_member(&mut self, _: &Room) {} +} diff --git a/src/lib.rs b/src/lib.rs index 84b40335..8e3ffb0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,12 +35,15 @@ pub use ruma_identifiers as identifiers; mod async_client; mod base_client; mod error; +mod models; mod session; +mod event_emitter; #[cfg(feature = "encryption")] mod crypto; pub use async_client::{AsyncClient, AsyncClientConfig, SyncSettings}; -pub use base_client::{Client, Room}; +pub use base_client::Client; +pub use models::Room; pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 00000000..4e2d3307 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,197 @@ +use crate::events::{ + call::{ + answer::AnswerEvent, candidates::CandidatesEvent, hangup::HangupEvent, invite::InviteEvent, + }, + direct::DirectEvent, + dummy::DummyEvent, + forwarded_room_key::ForwardedRoomKeyEvent, + fully_read::FullyReadEvent, + ignored_user_list::IgnoredUserListEvent, + key::verification::{ + accept::AcceptEvent, cancel::CancelEvent, key::KeyEvent, mac::MacEvent, + request::RequestEvent, start::StartEvent, + }, + presence::PresenceEvent, + push_rules::PushRulesEvent, + receipt::ReceiptEvent, + room::{ + aliases::AliasesEvent, + avatar::AvatarEvent, + canonical_alias::CanonicalAliasEvent, + create::CreateEvent, + encrypted::EncryptedEvent, + encryption::EncryptionEvent, + guest_access::GuestAccessEvent, + history_visibility::HistoryVisibilityEvent, + join_rules::JoinRulesEvent, + member::MemberEvent, + message::{feedback::FeedbackEvent, MessageEvent}, + name::NameEvent, + pinned_events::PinnedEventsEvent, + power_levels::PowerLevelsEvent, + redaction::RedactionEvent, + server_acl::ServerAclEvent, + third_party_invite::ThirdPartyInviteEvent, + tombstone::TombstoneEvent, + topic::TopicEvent, + }, + room_key::RoomKeyEvent, + room_key_request::RoomKeyRequestEvent, + sticker::StickerEvent, + tag::TagEvent, + typing::TypingEvent, + CustomEvent, CustomRoomEvent, CustomStateEvent, +}; + +mod room_member; +mod room_state; +mod room; +mod user; + +pub use room::{Room, RoomName}; +pub use room_member::RoomMember; +pub use user::User; + +pub type Token = String; +pub type RoomId = String; +pub type UserId = String; + +pub enum EventWrapper<'ev> { + /// m.call.answer + CallAnswer(&'ev AnswerEvent), + + /// m.call.candidates + CallCandidates(&'ev CandidatesEvent), + + /// m.call.hangup + CallHangup(&'ev HangupEvent), + + /// m.call.invite + CallInvite(&'ev InviteEvent), + + /// m.direct + Direct(&'ev DirectEvent), + + /// m.dummy + Dummy(&'ev DummyEvent), + + /// m.forwarded_room_key + ForwardedRoomKey(&'ev ForwardedRoomKeyEvent), + + /// m.fully_read + FullyRead(&'ev FullyReadEvent), + + /// m.ignored_user_list + IgnoredUserList(&'ev IgnoredUserListEvent), + + /// m.key.verification.accept + KeyVerificationAccept(&'ev AcceptEvent), + + /// m.key.verification.cancel + KeyVerificationCancel(&'ev CancelEvent), + + /// m.key.verification.key + KeyVerificationKey(&'ev KeyEvent), + + /// m.key.verification.mac + KeyVerificationMac(&'ev MacEvent), + + /// m.key.verification.request + KeyVerificationRequest(&'ev RequestEvent), + + /// m.key.verification.start + KeyVerificationStart(&'ev StartEvent), + + /// m.presence + Presence(&'ev PresenceEvent), + + /// m.push_rules + PushRules(&'ev PushRulesEvent), + + /// m.receipt + Receipt(&'ev ReceiptEvent), + + /// m.room.aliases + RoomAliases(&'ev AliasesEvent), + + /// m.room.avatar + RoomAvatar(&'ev AvatarEvent), + + /// m.room.canonical_alias + RoomCanonicalAlias(&'ev CanonicalAliasEvent), + + /// m.room.create + RoomCreate(&'ev CreateEvent), + + /// m.room.encrypted + RoomEncrypted(&'ev EncryptedEvent), + + /// m.room.encryption + RoomEncryption(&'ev EncryptionEvent), + + /// m.room.guest_access + RoomGuestAccess(&'ev GuestAccessEvent), + + /// m.room.history_visibility + RoomHistoryVisibility(&'ev HistoryVisibilityEvent), + + /// m.room.join_rules + RoomJoinRules(&'ev JoinRulesEvent), + + /// m.room.member + RoomMember(&'ev MemberEvent), + + /// m.room.message + RoomMessage(&'ev MessageEvent), + + /// m.room.message.feedback + RoomMessageFeedback(&'ev FeedbackEvent), + + /// m.room.name + RoomName(&'ev NameEvent), + + /// m.room.pinned_events + RoomPinnedEvents(&'ev PinnedEventsEvent), + + /// m.room.power_levels + RoomPowerLevels(&'ev PowerLevelsEvent), + + /// m.room.redaction + RoomRedaction(&'ev RedactionEvent), + + /// m.room.server_acl + RoomServerAcl(&'ev ServerAclEvent), + + /// m.room.third_party_invite + RoomThirdPartyInvite(&'ev ThirdPartyInviteEvent), + + /// m.room.tombstone + RoomTombstone(&'ev TombstoneEvent), + + /// m.room.topic + RoomTopic(&'ev TopicEvent), + + /// m.room_key + RoomKey(&'ev RoomKeyEvent), + + /// m.room_key_request + RoomKeyRequest(&'ev RoomKeyRequestEvent), + + /// m.sticker + Sticker(&'ev StickerEvent), + + /// m.tag + Tag(&'ev TagEvent), + + /// m.typing + Typing(&'ev TypingEvent), + + /// Any basic event that is not part of the specification. + Custom(&'ev CustomEvent), + + /// Any room event that is not part of the specification. + CustomRoom(&'ev CustomRoomEvent), + + /// Any state event that is not part of the specification. + CustomState(&'ev CustomStateEvent), +} diff --git a/src/models/room.rs b/src/models/room.rs new file mode 100644 index 00000000..deea05ad --- /dev/null +++ b/src/models/room.rs @@ -0,0 +1,373 @@ +// Copyright 2020 Damir Jelić +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::api::r0 as api; +use crate::events::collections::all::{RoomEvent, StateEvent}; +use crate::events::room::{ + aliases::AliasesEvent, + canonical_alias::CanonicalAliasEvent, + member::{MemberEvent, MembershipState}, + name::NameEvent, +}; +use crate::events::{presence::{PresenceEvent, PresenceEventContent}, EventResult}; +use crate::identifiers::RoomAliasId; +use crate::session::Session; +use super::{RoomId, UserId, RoomMember, User}; + +#[cfg(feature = "encryption")] +use tokio::sync::Mutex; + +#[cfg(feature = "encryption")] +use crate::crypto::{OlmMachine, OneTimeKeys}; +#[cfg(feature = "encryption")] +use ruma_client_api::r0::keys::{upload_keys::Response as KeysUploadResponse, DeviceKeys}; + +#[derive(Debug, Default)] +/// `RoomName` allows the calculation of a text room name. +pub struct RoomName { + /// The displayed name of the room. + name: Option, + /// The canonical alias of the room ex. `#room-name:example.com` and port number. + canonical_alias: Option, + /// List of `RoomAliasId`s the room has been given. + aliases: Vec, +} + +#[derive(Debug)] +/// A Matrix rooom. +pub struct Room { + /// The unique id of the room. + pub room_id: RoomId, + /// The name of the room, clients use this to represent a room. + pub room_name: RoomName, + /// The mxid of our own user. + pub own_user_id: UserId, + /// The mxid of the room creator. + pub creator: Option, + /// The map of room members. + pub members: HashMap, + /// A list of users that are currently typing. + pub typing_users: Vec, + /// A flag indicating if the room is encrypted. + pub encrypted: bool, +} + +impl RoomName { + pub fn push_alias(&mut self, alias: RoomAliasId) -> bool { + self.aliases.push(alias); + true + } + + pub fn set_canonical(&mut self, alias: RoomAliasId) -> bool { + self.canonical_alias = Some(alias); + true + } + + pub fn set_name(&mut self, name: &str) -> bool { + self.name = Some(name.to_string()); + true + } + + pub fn calculate_name(&self, room_id: &str, members: &HashMap) -> String { + // https://github.com/matrix-org/matrix-js-sdk/blob/33941eb37bffe41958ba9887fc8070dfb1a0ee76/src/models/room.js#L1823 + // the order in which we check for a name ^^ + if let Some(name) = &self.name { + name.clone() + } else if let Some(alias) = &self.canonical_alias { + alias.alias().to_string() + } else if !self.aliases.is_empty() { + self.aliases[0].alias().to_string() + } else { + // TODO + let mut names = members + .values() + .flat_map(|m| m.user.display_name.clone()) + .take(3) + .collect::>(); + + if names.is_empty() { + // TODO implement the rest of matrix-js-sdk handling of room names + format!("Room {}", room_id) + } else { + // stabilize order + names.sort(); + names.join(", ") + } + } + } +} + +impl Room { + /// Create a new room. + /// + /// # Arguments + /// + /// * `room_id` - The unique id of the room. + /// + /// * `own_user_id` - The mxid of our own user. + pub fn new(room_id: &str, own_user_id: &str) -> Self { + Room { + room_id: room_id.to_string(), + room_name: RoomName::default(), + own_user_id: own_user_id.to_owned(), + creator: None, + members: HashMap::new(), + typing_users: Vec::new(), + encrypted: false, + } + } + + fn add_member(&mut self, event: &MemberEvent) -> bool { + if self.members.contains_key(&event.state_key) { + return false; + } + + let member = RoomMember::new(event); + + self.members.insert(event.state_key.clone(), member); + + true + } + + // fn remove_member(&mut self, event: &MemberEvent) -> bool { + // if let Some(member) = self.members.get_mut(&event.sender.to_string()) { + // let changed = member.membership == event.content.membership; + // member.membership = event.content.membership; + // changed + // } else { + // false + // } + // } + + // fn update_joined_member(&mut self, event: &MemberEvent) -> bool { + // if let Some(member) = self.members.get_mut(&event.state_key) { + // member.update(event); + // } + + // false + // } + + // fn handle_join(&mut self, event: &MemberEvent) -> bool { + // match &event.prev_content { + // Some(c) => match c.membership { + // MembershipState::Join => self.update_joined_member(event), + // MembershipState::Invite => self.add_member(event), + // MembershipState::Leave => self.remove_member(event), + // _ => false, + // }, + // None => self.add_member(event), + // } + // } + + // fn handle_leave(&mut self, event: &MemberEvent) -> bool { + + // } + + /// Handle a room.member updating the room state if necessary. + /// + /// Returns true if the joined member list changed, false otherwise. + pub fn handle_membership(&mut self, event: &MemberEvent) -> bool { + match &event.content.membership { + MembershipState::Invite | MembershipState::Join => self.add_member(event), + _ => { + if let Some(member) = self.members.get_mut(&event.sender.to_string()) { + let changed = member.membership == event.content.membership; + member.membership = event.content.membership; + changed + } else { + false + } + } + } + } + + /// Add to the list of `RoomAliasId`s. + fn room_aliases(&mut self, alias: &RoomAliasId) -> bool { + self.room_name.push_alias(alias.clone()); + true + } + + /// RoomAliasId is `#alias:hostname` and `port` + fn canonical_alias(&mut self, alias: &RoomAliasId) -> bool { + self.room_name.set_canonical(alias.clone()); + true + } + + fn name_room(&mut self, name: &str) -> bool { + self.room_name.set_name(name); + true + } + + /// Handle a room.aliases event, updating the room state if necessary. + /// + /// Returns true if the room name changed, false otherwise. + pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool { + match event.content.aliases.as_slice() { + [alias] => self.room_aliases(alias), + [alias, ..] => self.room_aliases(alias), + _ => false, + } + } + + /// Handle a room.canonical_alias event, updating the room state if necessary. + /// + /// Returns true if the room name changed, false otherwise. + pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool { + match &event.content.alias { + Some(name) => self.canonical_alias(&name), + _ => false, + } + } + + /// Handle a room.name event, updating the room state if necessary. + /// + /// Returns true if the room name changed, false otherwise. + pub fn handle_room_name(&mut self, event: &NameEvent) -> bool { + match event.content.name() { + Some(name) => self.name_room(name), + _ => false, + } + } + + /// Receive a timeline event for this room and update the room state. + /// + /// Returns true if the joined member list changed, false otherwise. + /// + /// # Arguments + /// + /// * `event` - The event of the room. + pub fn receive_timeline_event(&mut self, event: &RoomEvent) -> bool { + match event { + // update to the current members of the room + RoomEvent::RoomMember(m) => self.handle_membership(m), + // finds all events related to the name of the room for later calculation + RoomEvent::RoomName(n) => self.handle_room_name(n), + RoomEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), + RoomEvent::RoomAliases(a) => self.handle_room_aliases(a), + // power levels of the room members + // RoomEvent::RoomPowerLevels(p) => self.handle_power_level(p), + _ => false, + } + } + + /// Receive a state event for this room and update the room state. + /// + /// Returns true if the joined member list changed, false otherwise. + /// + /// # Arguments + /// + /// * `event` - The event of the room. + pub fn receive_state_event(&mut self, event: &StateEvent) -> bool { + match event { + StateEvent::RoomMember(m) => self.handle_membership(m), + StateEvent::RoomName(n) => self.handle_room_name(n), + StateEvent::RoomCanonicalAlias(ca) => self.handle_canonical(ca), + StateEvent::RoomAliases(a) => self.handle_room_aliases(a), + _ => false, + } + } + + /// Receive a presence event from an `IncomingResponse` and updates the client state. + /// + /// Returns true if the joined member list changed, false otherwise. + /// + /// # Arguments + /// + /// * `event` - The event of the room. + pub fn receive_presence_event(&mut self, event: &PresenceEvent) -> bool { + let PresenceEvent { + content: PresenceEventContent { + avatar_url, + currently_active, + displayname, + last_active_ago, + presence, + status_msg, + }, + sender, + } = event; + + if let Some(user) = self.members.get_mut(&sender.to_string()).map(|m| &mut m.user) { + if user.display_name == *displayname && user.avatar_url == *avatar_url + && user.presence.as_ref() == Some(presence) && user.status_msg == *status_msg + && user.last_active_ago == *last_active_ago && user.currently_active == *currently_active + { + false + } else { + user.presence_events.push(event.clone()); + *user = User { + display_name: displayname.clone(), + avatar_url: avatar_url.clone(), + presence: Some(presence.clone()), + status_msg: status_msg.clone(), + last_active_ago: *last_active_ago, + currently_active: *currently_active, + // TODO better way of moving vec over + events: user.events.clone(), + presence_events: user.presence_events.clone(), + }; + true + } + } else { + // this is probably an error as we have a `PresenceEvent` for a user + // we dont know about + false + } + } +} + +// pub struct User { +// /// The human readable name of the user. +// pub display_name: Option, +// /// The matrix url of the users avatar. +// pub avatar_url: Option, +// /// The presence of the user, if found. +// pub presence: Option, +// /// The presence status message, if found. +// pub status_msg: Option, +// /// The time, in ms, since the user interacted with the server. +// pub last_active_ago: Option, +// /// If the user should be considered active. +// pub currently_active: Option, +// /// The events that created the state of the current user. +// // TODO do we want to hold the whole state or just update our structures. +// pub events: Vec, +// /// The `PresenceEvent`s connected to this user. +// pub presence_events: Vec, +// } + +// pub struct RoomMember { +// /// The unique mxid of the user. +// pub user_id: UserId, +// /// The unique id of the room. +// pub room_id: Option, +// /// If the member is typing. +// pub typing: Option, +// /// The user data for this room member. +// pub user: User, +// /// The users power level. +// pub power_level: Option, +// /// The normalized power level of this `RoomMember` (0-100). +// pub power_level_norm: Option, +// /// The `MembershipState` of this `RoomMember`. +// pub membership: MembershipState, +// /// The human readable name of this room member. +// pub name: String, +// /// The events that created the state of this room member. +// pub events: Vec +// } diff --git a/src/models/room_member.rs b/src/models/room_member.rs new file mode 100644 index 00000000..c8f67ef8 --- /dev/null +++ b/src/models/room_member.rs @@ -0,0 +1,106 @@ +// Copyright 2020 Damir Jelić +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::api::r0 as api; +use crate::events::collections::all::{Event, RoomEvent, StateEvent}; +use crate::events::room::{ + aliases::AliasesEvent, + canonical_alias::CanonicalAliasEvent, + member::{MemberEvent, MemberEventContent, MembershipState}, + name::NameEvent, +}; +use crate::events::EventResult; +use crate::identifiers::RoomAliasId; +use crate::session::Session; +use super::{UserId, RoomId, User}; + +use js_int::{Int, UInt}; +#[cfg(feature = "encryption")] +use tokio::sync::Mutex; + +#[cfg(feature = "encryption")] +use crate::crypto::{OlmMachine, OneTimeKeys}; +#[cfg(feature = "encryption")] +use ruma_client_api::r0::keys::{upload_keys::Response as KeysUploadResponse, DeviceKeys}; + +#[derive(Debug)] +/// A Matrix room member. +pub struct RoomMember { + /// The unique mxid of the user. + pub user_id: UserId, + /// The unique id of the room. + pub room_id: Option, + /// If the member is typing. + pub typing: Option, + /// The user data for this room member. + pub user: User, + /// The users power level. + pub power_level: Option, + /// The normalized power level of this `RoomMember` (0-100). + pub power_level_norm: Option, + /// The `MembershipState` of this `RoomMember`. + pub membership: MembershipState, + /// The human readable name of this room member. + pub name: String, + /// The events that created the state of this room member. + pub events: Vec +} + +impl RoomMember { + pub fn new(event: &MemberEvent) -> Self { + let user = User::new(event); + Self { + room_id: event.room_id.as_ref().map(|id| id.to_string()), + user_id: event.state_key.clone(), + typing: None, + user, + power_level: None, + power_level_norm: None, + membership: event.content.membership, + name: event.state_key.clone(), + events: vec![Event::RoomMember(event.clone())] + } + } + + pub fn update(&mut self, event: &MemberEvent) { + let MemberEvent { + content: MemberEventContent { + membership, + .. + }, + room_id, + state_key, + .. + } = event; + + let mut events = Vec::new(); + events.extend(self.events.drain(..).chain(Some(Event::RoomMember(event.clone())))); + + *self = Self { + room_id: room_id.as_ref().map(|id| id.to_string()).or(self.room_id.take()), + user_id: state_key.clone(), + typing: None, + user: User::new(event), + power_level: None, + power_level_norm: None, + membership: membership.clone(), + name: state_key.clone(), + events, + } + } +} diff --git a/src/models/room_state.rs b/src/models/room_state.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 00000000..060ec88c --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,77 @@ +// Copyright 2020 Damir Jelić +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::api::r0 as api; +use crate::events::collections::all::{Event, RoomEvent, StateEvent}; +use crate::events::room::{ + aliases::AliasesEvent, + canonical_alias::CanonicalAliasEvent, + member::{MemberEvent, MembershipState}, + name::NameEvent, +}; +use crate::events::presence::{PresenceEvent, PresenceState}; +use crate::events::EventResult; +use crate::identifiers::RoomAliasId; +use crate::session::Session; +use super::{UserId, RoomId}; + +use js_int::UInt; +#[cfg(feature = "encryption")] +use tokio::sync::Mutex; + +#[cfg(feature = "encryption")] +use crate::crypto::{OlmMachine, OneTimeKeys}; +#[cfg(feature = "encryption")] +use ruma_client_api::r0::keys::{upload_keys::Response as KeysUploadResponse, DeviceKeys}; + +#[derive(Debug)] +/// A Matrix room member. +pub struct User { + /// The human readable name of the user. + pub display_name: Option, + /// The matrix url of the users avatar. + pub avatar_url: Option, + /// The presence of the user, if found. + pub presence: Option, + /// The presence status message, if found. + pub status_msg: Option, + /// The time, in ms, since the user interacted with the server. + pub last_active_ago: Option, + /// If the user should be considered active. + pub currently_active: Option, + /// The events that created the state of the current user. + // TODO do we want to hold the whole state or just update our structures. + pub events: Vec, + /// The `PresenceEvent`s connected to this user. + pub presence_events: Vec, +} + +impl User { + pub fn new(event: &MemberEvent) -> Self { + Self { + display_name: event.content.displayname.clone(), + avatar_url: event.content.avatar_url.clone(), + presence: None, + status_msg: None, + last_active_ago: None, + currently_active: None, + events: Vec::default(), + presence_events: Vec::default(), + } + } +} diff --git a/tests/async_client_tests.rs b/tests/async_client_tests.rs index 0c213d0c..4b942e23 100644 --- a/tests/async_client_tests.rs +++ b/tests/async_client_tests.rs @@ -90,3 +90,38 @@ async fn timeline() { println!("{:#?}", &client.base_client().read().await.joined_rooms); } + +#[test] +fn timeline() { + let mut rt = Runtime::new().unwrap(); + + let homeserver = Url::from_str(&mockito::server_url()).unwrap(); + + let session = Session { + access_token: "1234".to_owned(), + user_id: UserId::try_from("@example:example.com").unwrap(), + device_id: "DEVICEID".to_owned(), + }; + + let _m = mock( + "GET", + Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()), + ) + .with_status(200) + .with_body_from_file("tests/data/sync.json") + .create(); + + let mut client = AsyncClient::new(homeserver, Some(session)).unwrap(); + + let sync_settings = SyncSettings::new().timeout(3000).unwrap(); + + let _response = rt.block_on(client.sync(sync_settings)).unwrap(); + + assert_eq!(vec!["tutorial"], rt.block_on(client.get_room_names())); + assert_eq!( + Some("tutorial".into()), + rt.block_on(client.get_room_name("!SVkFJHzfwvuaIEawgC:localhost")) + ); + + rt.block_on(async { println!("{:#?}", &client.base_client().read().await.joined_rooms ) }); +} diff --git a/tests/data/sync.json b/tests/data/sync.json index 3b898e20..d873fb47 100644 --- a/tests/data/sync.json +++ b/tests/data/sync.json @@ -2,12 +2,11 @@ "device_one_time_keys_count": {}, "next_batch": "s526_47314_0_7_1_1_1_11444_1", "device_lists": { - "changed": [ - "@example:example.org" - ], - "left": [] + "changed": [ + "@example:example.org" + ], + "left": [] }, - "rooms": { "invite": {}, "join": { @@ -184,7 +183,31 @@ "prev_sender": "@example:localhost", "replaces_state": "$152034819067QWJxM:localhost" } - } + }, + { + "content": { + "membership": "leave", + "reason": "offline", + "avatar_url": "avatar.com", + "displayname": "example" + }, + "event_id": "$1585345508297748AIUBh:matrix.org", + "origin_server_ts": 1585345508223, + "sender": "@example:localhost", + "state_key": "@example:localhost", + "type": "m.room.member", + "unsigned": { + "replaces_state": "$1585345354296486IGZfp:localhost", + "prev_content": { + "avatar_url": "avatar.com", + "displayname": "example", + "membership": "join" + }, + "prev_sender": "@example2:localhost", + "age": 6992 + }, + "room_id": "!roomid:room.com" + } ] }, "timeline": { @@ -219,8 +242,19 @@ "to_device": { "events": [] }, - "presence": { - "events": [] + "events": [ + { + "content": { + "avatar_url": "mxc://localhost:wefuiwegh8742w", + "currently_active": false, + "last_active_ago": 2478593, + "presence": "online", + "status_msg": "Making cupcakes" + }, + "sender": "@example:localhost", + "type": "m.presence" + } + ] } }