From 993986e6972d199de59f74ff9ee1a89c3e2a14f1 Mon Sep 17 00:00:00 2001 From: mb Date: Thu, 2 Nov 2023 23:33:19 +0100 Subject: [PATCH] feat: Add name to various objects return from Room Members, Occupants and Messages --- bindings/prose-sdk-ffi/src/types/message.rs | 2 +- bindings/prose-sdk-js/src/types/contact.rs | 1 + bindings/prose-sdk-js/src/types/message.rs | 29 +++- bindings/prose-sdk-js/src/types/mod.rs | 2 + bindings/prose-sdk-js/src/types/room.rs | 58 ++----- bindings/prose-sdk-js/src/types/user_info.rs | 84 +++++++++ .../prose-core-client/src/app/dtos/message.rs | 28 +++ crates/prose-core-client/src/app/dtos/mod.rs | 8 +- .../app/event_handlers/rooms_event_handler.rs | 10 +- .../src/app/services/room.rs | 136 +++++++++++---- .../src/domain/messaging/models/message.rs | 8 +- .../domain/messaging/models/message_like.rs | 32 ++-- .../src/domain/rooms/models/mod.rs | 6 +- .../src/domain/rooms/models/room_internals.rs | 60 +++---- .../src/domain/rooms/models/room_state.rs | 51 ++++-- .../services/impls/rooms_domain_service.rs | 64 ++++++- .../src/domain/shared/models/mod.rs | 2 + .../models/user_info.rs} | 12 +- .../shared/utils/contact_name_builder.rs | 2 +- .../src/infra/platform_dependencies.rs | 41 ++++- .../src/test/message_builder.rs | 65 +++++-- .../src/test/room_internals.rs | 37 +++- crates/prose-core-client/src/util/jid_ext.rs | 48 +++++- crates/prose-core-client/tests/room.rs | 113 +++++++++---- .../tests/rooms_event_handler.rs | 34 ++-- .../prose-core-client/tests/rooms_service.rs | 3 +- .../prose-xmpp/src/stanza/message/builder.rs | 6 + .../prose-xmpp/src/stanza/message/message.rs | 5 + crates/prose-xmpp/src/stanza/message/mod.rs | 2 + .../prose-xmpp/src/stanza/message/muc_user.rs | 160 ++++++++++++++++++ crates/prose-xmpp/src/stanza/muc/mod.rs | 5 + crates/prose-xmpp/src/stanza/muc/ns.rs | 5 + crates/prose-xmpp/src/stanza/muc/query.rs | 5 + examples/prose-core-client-cli/src/main.rs | 4 +- 34 files changed, 887 insertions(+), 241 deletions(-) create mode 100644 bindings/prose-sdk-js/src/types/user_info.rs create mode 100644 crates/prose-core-client/src/app/dtos/message.rs rename crates/prose-core-client/src/domain/{rooms/models/composing_user.rs => shared/models/user_info.rs} (53%) create mode 100644 crates/prose-xmpp/src/stanza/message/muc_user.rs diff --git a/bindings/prose-sdk-ffi/src/types/message.rs b/bindings/prose-sdk-ffi/src/types/message.rs index f5d8d3d0..ef820f53 100644 --- a/bindings/prose-sdk-ffi/src/types/message.rs +++ b/bindings/prose-sdk-ffi/src/types/message.rs @@ -35,7 +35,7 @@ impl From for Message { Message { id: value.id, stanza_id: value.stanza_id, - from: value.from.into(), + from: value.from.jid.to_bare().into(), body: value.body, timestamp: value.timestamp, is_read: value.is_read, diff --git a/bindings/prose-sdk-js/src/types/contact.rs b/bindings/prose-sdk-js/src/types/contact.rs index b06f0b31..2b2a8716 100644 --- a/bindings/prose-sdk-js/src/types/contact.rs +++ b/bindings/prose-sdk-js/src/types/contact.rs @@ -21,6 +21,7 @@ impl From for Contact { } #[wasm_bindgen] +#[derive(Clone)] pub enum Availability { Available = 0, Unavailable = 1, diff --git a/bindings/prose-sdk-js/src/types/message.rs b/bindings/prose-sdk-js/src/types/message.rs index d5ccd014..9e959d87 100644 --- a/bindings/prose-sdk-js/src/types/message.rs +++ b/bindings/prose-sdk-js/src/types/message.rs @@ -3,10 +3,12 @@ // Copyright: 2023, Marc Bauer // License: Mozilla Public License v2.0 (MPL v2.0) -use crate::types::IntoJSArray; -use prose_core_client::dtos; use wasm_bindgen::prelude::*; +use prose_core_client::dtos; + +use crate::types::IntoJSArray; + use super::{BareJid, BareJidArray, ReactionsArray}; #[wasm_bindgen] @@ -20,6 +22,9 @@ pub struct Reaction { pub from: Vec, } +#[wasm_bindgen] +pub struct MessageSender(dtos::MessageSender); + #[wasm_bindgen] impl Message { #[wasm_bindgen(getter)] @@ -39,7 +44,12 @@ impl Message { #[wasm_bindgen(getter, js_name = "from")] pub fn from_(&self) -> String { - self.0.from.to_string() + self.0.from.jid.to_string() + } + + #[wasm_bindgen(getter, js_name = "user")] + pub fn user(&self) -> MessageSender { + MessageSender(self.0.from.clone()) } #[wasm_bindgen(getter, js_name = "content")] @@ -100,6 +110,19 @@ impl Reaction { } } +#[wasm_bindgen] +impl MessageSender { + #[wasm_bindgen(getter, js_name = "jid")] + pub fn jid(&self) -> String { + self.0.jid.to_string() + } + + #[wasm_bindgen(getter, js_name = "name")] + pub fn name(&self) -> String { + self.0.name.clone() + } +} + impl From for Message { fn from(value: dtos::Message) -> Self { Message(value) diff --git a/bindings/prose-sdk-js/src/types/mod.rs b/bindings/prose-sdk-js/src/types/mod.rs index 925a42ae..002f42f2 100644 --- a/bindings/prose-sdk-js/src/types/mod.rs +++ b/bindings/prose-sdk-js/src/types/mod.rs @@ -9,6 +9,7 @@ pub use jid::BareJid; pub use js_array::*; pub use message::Message; pub use room::{RoomEnvelopeExt, RoomsArray}; +pub use user_info::{UserBasicInfo, UserBasicInfoArray, UserPresenceInfo, UserPresenceInfoArray}; pub use user_metadata::UserMetadata; pub use user_profile::UserProfile; @@ -18,5 +19,6 @@ mod jid; mod js_array; mod message; mod room; +mod user_info; mod user_metadata; mod user_profile; diff --git a/bindings/prose-sdk-js/src/types/room.rs b/bindings/prose-sdk-js/src/types/room.rs index ad15692d..54c6f113 100644 --- a/bindings/prose-sdk-js/src/types/room.rs +++ b/bindings/prose-sdk-js/src/types/room.rs @@ -8,14 +8,15 @@ use tracing::info; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsError, JsValue}; -use prose_core_client::dtos::{ComposingUser as CoreComposingUser, MessageId}; +use prose_core_client::dtos::MessageId; use prose_core_client::services::{ DirectMessage, Generic, Group, PrivateChannel, PublicChannel, Room as SdkRoom, RoomEnvelope, }; use crate::client::WasmError; use crate::types::{ - try_jid_vec_from_string_array, BareJid, BareJidArray, MessagesArray, StringArray, + try_jid_vec_from_string_array, MessagesArray, StringArray, UserBasicInfo, UserBasicInfoArray, + UserPresenceInfo, UserPresenceInfoArray, }; use super::IntoJSArray; @@ -31,9 +32,9 @@ export interface RoomBase { readonly id: RoomID; readonly name: string; /// The members of a room. Only available for DirectMessage and Group (member-only rooms) - readonly members: JID[]; + readonly members: UserPresenceInfo[]; /// The occupants of a room. - readonly occupants: JID[]; + readonly occupants: UserBasicInfo[]; sendMessage(body: string): Promise; updateMessage(messageID: string, body: string): Promise; @@ -44,7 +45,7 @@ export interface RoomBase { loadMessagesWithIDs(messageIDs: string[]): Promise; setUserIsComposing(isComposing: boolean): Promise; - loadComposingUsers(): Promise; + loadComposingUsers(): Promise; saveDraft(message?: string): Promise; loadDraft(): Promise; @@ -152,21 +153,21 @@ macro_rules! base_room_impl { } #[wasm_bindgen(getter)] - pub fn members(&self) -> BareJidArray { + pub fn members(&self) -> UserPresenceInfoArray { self.room .members() - .iter() - .map(BareJid::from) - .collect_into_js_array::() + .into_iter() + .map(UserPresenceInfo::from) + .collect_into_js_array::() } #[wasm_bindgen(getter)] - pub fn occupants(&self) -> BareJidArray { + pub fn occupants(&self) -> UserBasicInfoArray { self.room .occupants() - .iter() - .map(BareJid::from) - .collect_into_js_array::() + .into_iter() + .map(UserBasicInfo::from) + .collect_into_js_array::() } #[wasm_bindgen(js_name = "sendMessage")] @@ -247,15 +248,15 @@ macro_rules! base_room_impl { } #[wasm_bindgen(js_name = "loadComposingUsers")] - pub async fn load_composing_users(&self) -> Result { + pub async fn load_composing_users(&self) -> Result { Ok(self .room .load_composing_users() .await .map_err(WasmError::from)? .into_iter() - .map(ComposingUser::from) - .collect_into_js_array::()) + .map(UserBasicInfo::from) + .collect_into_js_array::()) } #[wasm_bindgen(js_name = "saveDraft")] @@ -343,9 +344,6 @@ channel_room_impl!(RoomPublicChannel); extern "C" { #[wasm_bindgen(typescript_type = "Room[]")] pub type RoomsArray; - - #[wasm_bindgen(typescript_type = "ComposingUser[]")] - pub type ComposingUsersArray; } pub trait RoomEnvelopeExt { @@ -387,25 +385,3 @@ impl From> for RoomsArray { .collect_into_js_array::() } } - -#[wasm_bindgen] -pub struct ComposingUser(CoreComposingUser); - -#[wasm_bindgen] -impl ComposingUser { - #[wasm_bindgen(getter)] - pub fn name(&self) -> String { - self.0.name.clone() - } - - #[wasm_bindgen(getter)] - pub fn jid(&self) -> BareJid { - self.0.jid.clone().into() - } -} - -impl From for ComposingUser { - fn from(value: CoreComposingUser) -> Self { - Self(value) - } -} diff --git a/bindings/prose-sdk-js/src/types/user_info.rs b/bindings/prose-sdk-js/src/types/user_info.rs new file mode 100644 index 00000000..0cbacc1b --- /dev/null +++ b/bindings/prose-sdk-js/src/types/user_info.rs @@ -0,0 +1,84 @@ +// prose-core-client/prose-sdk-js +// +// Copyright: 2023, Marc Bauer +// License: Mozilla Public License v2.0 (MPL v2.0) + +use wasm_bindgen::prelude::wasm_bindgen; + +use prose_core_client::dtos::{ + UserBasicInfo as SdkUserBasicInfo, UserPresenceInfo as SdkUserPresenceInfo, +}; + +use crate::types::{Availability, BareJid}; + +#[wasm_bindgen] +pub struct UserBasicInfo { + jid: BareJid, + name: String, +} + +#[wasm_bindgen] +impl UserBasicInfo { + #[wasm_bindgen(getter)] + pub fn name(&self) -> String { + self.name.clone() + } + + #[wasm_bindgen(getter)] + pub fn jid(&self) -> BareJid { + self.jid.clone().into() + } +} + +impl From for UserBasicInfo { + fn from(value: SdkUserBasicInfo) -> Self { + Self { + jid: value.jid.into(), + name: value.name, + } + } +} + +#[wasm_bindgen] +pub struct UserPresenceInfo { + jid: BareJid, + name: String, + availability: Availability, +} + +#[wasm_bindgen] +impl UserPresenceInfo { + #[wasm_bindgen(getter)] + pub fn name(&self) -> String { + self.name.clone() + } + + #[wasm_bindgen(getter)] + pub fn jid(&self) -> BareJid { + self.jid.clone().into() + } + + #[wasm_bindgen(getter)] + pub fn availability(&self) -> Availability { + self.availability.clone() + } +} + +impl From for UserPresenceInfo { + fn from(value: SdkUserPresenceInfo) -> Self { + Self { + jid: value.jid.into(), + name: value.name, + availability: value.availability.into(), + } + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "UserBasicInfo[]")] + pub type UserBasicInfoArray; + + #[wasm_bindgen(typescript_type = "UserPresenceInfo[]")] + pub type UserPresenceInfoArray; +} diff --git a/crates/prose-core-client/src/app/dtos/message.rs b/crates/prose-core-client/src/app/dtos/message.rs new file mode 100644 index 00000000..537fe1d4 --- /dev/null +++ b/crates/prose-core-client/src/app/dtos/message.rs @@ -0,0 +1,28 @@ +// prose-core-client/prose-core-client +// +// Copyright: 2023, Marc Bauer +// License: Mozilla Public License v2.0 (MPL v2.0) + +use chrono::{DateTime, Utc}; +use jid::Jid; + +use crate::dtos::{MessageId, Reaction, StanzaId}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Message { + pub id: Option, + pub stanza_id: Option, + pub from: MessageSender, + pub body: String, + pub timestamp: DateTime, + pub is_read: bool, + pub is_edited: bool, + pub is_delivered: bool, + pub reactions: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MessageSender { + pub jid: Jid, + pub name: String, +} diff --git a/crates/prose-core-client/src/app/dtos/mod.rs b/crates/prose-core-client/src/app/dtos/mod.rs index 7fb656f7..7afd7cee 100644 --- a/crates/prose-core-client/src/app/dtos/mod.rs +++ b/crates/prose-core-client/src/app/dtos/mod.rs @@ -6,15 +6,17 @@ pub use url::Url; pub use contact::Contact; +pub use message::{Message, MessageSender}; pub use crate::domain::{ contacts::models::Group, general::models::SoftwareVersion, - messaging::models::{Emoji, Message, MessageId, Reaction, StanzaId}, - rooms::models::{ComposingUser, Occupant}, - shared::models::Availability, + messaging::models::{Emoji, MessageId, Reaction, StanzaId}, + rooms::models::{Member, Occupant}, + shared::models::{Availability, UserBasicInfo, UserPresenceInfo}, user_info::models::{LastActivity, UserActivity, UserInfo, UserMetadata}, user_profiles::models::{Address, UserProfile}, }; mod contact; +mod message; diff --git a/crates/prose-core-client/src/app/event_handlers/rooms_event_handler.rs b/crates/prose-core-client/src/app/event_handlers/rooms_event_handler.rs index e2e1d829..02f660ed 100644 --- a/crates/prose-core-client/src/app/event_handlers/rooms_event_handler.rs +++ b/crates/prose-core-client/src/app/event_handlers/rooms_event_handler.rs @@ -18,7 +18,7 @@ use prose_xmpp::{ns, Event}; use crate::app::deps::{ DynAppContext, DynClientEventDispatcher, DynConnectedRoomsRepository, DynRoomFactory, - DynRoomsDomainService, DynTimeProvider, + DynRoomsDomainService, DynTimeProvider, DynUserProfileRepository, }; use crate::app::event_handlers::{XMPPEvent, XMPPEventHandler}; use crate::client_event::RoomEventType; @@ -40,6 +40,8 @@ pub struct RoomsEventHandler { room_factory: DynRoomFactory, #[inject] time_provider: DynTimeProvider, + #[inject] + user_profile_repo: DynUserProfileRepository, } #[cfg_attr(target_arch = "wasm32", async_trait(? Send))] @@ -161,9 +163,13 @@ impl RoomsEventHandler { }; info!("Received real jid for {}: {}", from, jid); + + let bare_jid = jid.into_bare(); + let name = self.user_profile_repo.get_display_name(&bare_jid).await?; + room.state .write() - .insert_occupant(&from, Some(&jid.into_bare()), &affiliation); + .insert_occupant(&from, Some(&bare_jid), name.as_deref(), &affiliation); Ok(()) } diff --git a/crates/prose-core-client/src/app/services/room.rs b/crates/prose-core-client/src/app/services/room.rs index 39a702ac..0407dc58 100644 --- a/crates/prose-core-client/src/app/services/room.rs +++ b/crates/prose-core-client/src/app/services/room.rs @@ -18,8 +18,12 @@ use crate::app::deps::{ DynRoomParticipationService, DynRoomTopicService, DynTimeProvider, DynUserProfileRepository, }; use crate::domain::messaging::models::{Emoji, Message, MessageId, MessageLike}; -use crate::domain::rooms::models::{ComposingUser, RoomInternals}; -use crate::util::jid_ext::JidExt; +use crate::domain::rooms::models::RoomInternals; +use crate::domain::shared::models::RoomType; +use crate::dtos::{ + Availability, Message as MessageDTO, MessageSender, UserBasicInfo, UserPresenceInfo, +}; +use crate::util::jid_ext::{BareJidExt, JidExt}; pub struct Room { inner: Arc, @@ -129,17 +133,35 @@ impl Room { self.data.state.read().subject.clone() } - pub fn members(&self) -> &[BareJid] { - self.data.info.members.as_slice() + pub fn members(&self) -> Vec { + self.data + .info + .members + .iter() + .map(|(jid, member)| UserPresenceInfo { + jid: jid.clone(), + name: member.name.clone(), + availability: Availability::Available, + }) + .collect() } - pub fn occupants(&self) -> Vec { + pub fn occupants(&self) -> Vec { self.data .state .read() .occupants .values() - .filter_map(|occupant| occupant.jid.clone()) + .filter_map(|occupant| { + let Some(jid) = occupant.jid.clone() else { + return None; + }; + let name = occupant + .name + .clone() + .unwrap_or_else(|| jid.to_display_name()); + Some(UserBasicInfo { jid, name }) + }) .collect() } } @@ -191,9 +213,9 @@ impl Room { .await } - pub async fn load_messages_with_ids(&self, ids: &[&MessageId]) -> Result> { + pub async fn load_messages_with_ids(&self, ids: &[&MessageId]) -> Result> { let messages = self.message_repo.get_all(&self.data.info.jid, ids).await?; - Ok(self.reduce_messages_and_lookup_real_jids(messages)) + Ok(self.reduce_messages_and_add_sender(messages).await) } pub async fn set_user_is_composing(&self, is_composing: bool) -> Result<()> { @@ -202,25 +224,11 @@ impl Room { .await } - pub async fn load_composing_users(&self) -> Result> { + pub async fn load_composing_users(&self) -> Result> { // If the chat state is 'composing' but older than 30 seconds we do not consider // the user as currently typing. let thirty_secs_ago = self.time_provider.now() - Duration::seconds(30); - let state = &*self.data.state.read(); - - let mut composing_users = vec![]; - for real_jid in state.composing_users(thirty_secs_ago) { - composing_users.push(ComposingUser { - name: self - .user_profile_repo - .get_display_name(&real_jid) - .await? - .unwrap_or_else(|| real_jid.to_display_name()), - jid: real_jid, - }) - } - - Ok(composing_users) + Ok(self.data.state.read().composing_users(thirty_secs_ago)) } pub async fn save_draft(&self, text: Option<&str>) -> Result<()> { @@ -231,7 +239,7 @@ impl Room { self.drafts_repo.get(&self.data.info.jid).await } - pub async fn load_latest_messages(&self) -> Result> { + pub async fn load_latest_messages(&self) -> Result> { debug!("Loading messages from server…"); let result = self @@ -253,19 +261,75 @@ impl Room { ) .await?; - Ok(self.reduce_messages_and_lookup_real_jids(messages)) + Ok(self.reduce_messages_and_add_sender(messages).await) } } impl Room { - fn reduce_messages_and_lookup_real_jids(&self, mut messages: Vec) -> Vec { - let state = &*self.data.state.read(); - for message in messages.iter_mut() { - if let Some(real_jid) = state.real_jid_for_occupant(&message.from) { - message.from = Jid::Bare(real_jid) + async fn reduce_messages_and_add_sender(&self, messages: Vec) -> Vec { + let messages = Message::reducing_messages(messages); + let mut message_dtos = Vec::with_capacity(messages.len()); + + for message in messages { + let name = self.resolve_user_name(&message.from).await; + + let from = MessageSender { + jid: message.from, + name, + }; + + message_dtos.push(MessageDTO { + id: message.id, + stanza_id: message.stanza_id, + from, + body: message.body, + timestamp: message.timestamp, + is_read: message.is_read, + is_edited: message.is_edited, + is_delivered: message.is_delivered, + reactions: message.reactions, + }); + } + + message_dtos + } + + async fn resolve_user_name(&self, jid: &Jid) -> String { + let name = { + let state = &*self.data.state.read(); + + match jid { + Jid::Bare(bare) => self + .data + .info + .members + .get(bare) + .map(|member| member.name.clone()) + .or_else(|| state.occupants.get(jid).and_then(|o| o.name.clone())), + Jid::Full(_) => state.occupants.get(jid).and_then(|o| o.name.clone()), } + }; + + if let Some(name) = name { + return name; + }; + + if let Jid::Bare(bare) = &jid { + if let Some(name) = self + .user_profile_repo + .get_display_name(bare) + .await + .unwrap_or_default() + { + return name; + }; + } + + if self.data.info.room_type == RoomType::DirectMessage { + jid.node_to_display_name() + } else { + jid.resource_to_display_name() } - Message::reducing_messages(messages) } } @@ -280,7 +344,13 @@ impl Room { pub async fn resend_invites_to_members(&self) -> Result<()> { info!("Sending invites to group members…"); - let member_jids = self.data.info.members.iter().collect::>(); + let member_jids = self + .data + .info + .members + .keys() + .map(|jid| jid) + .collect::>(); self.participation_service .invite_users_to_room(&self.data.info.jid, member_jids.as_slice()) .await?; diff --git a/crates/prose-core-client/src/domain/messaging/models/message.rs b/crates/prose-core-client/src/domain/messaging/models/message.rs index 25fb7956..5009d83e 100644 --- a/crates/prose-core-client/src/domain/messaging/models/message.rs +++ b/crates/prose-core-client/src/domain/messaging/models/message.rs @@ -5,7 +5,7 @@ use chrono::{DateTime, Utc}; use indexmap::IndexMap; -use jid::BareJid; +use jid::{BareJid, Jid}; use serde::{Deserialize, Serialize}; use prose_utils::id_string; @@ -26,7 +26,7 @@ pub struct Reaction { pub struct Message { pub id: Option, pub stanza_id: Option, - pub from: BareJid, + pub from: Jid, pub body: String, pub timestamp: DateTime, pub is_read: bool, @@ -82,7 +82,7 @@ impl Message { let message = Message { id: message_id.into_original_id(), stanza_id: msg.stanza_id, - from: msg.from.into_bare(), + from: msg.from, body, timestamp: msg.timestamp.into(), is_read: false, @@ -359,7 +359,7 @@ mod tests { Message { id: Some("1".into()), stanza_id: None, - from: BareJid::from_str("b@prose.org").unwrap(), + from: jid!("b@prose.org"), body: "Hello World".to_string(), timestamp: Utc .with_ymd_and_hms(2023, 04, 07, 16, 00, 00) diff --git a/crates/prose-core-client/src/domain/messaging/models/message_like.rs b/crates/prose-core-client/src/domain/messaging/models/message_like.rs index f9653a85..fc04d15c 100644 --- a/crates/prose-core-client/src/domain/messaging/models/message_like.rs +++ b/crates/prose-core-client/src/domain/messaging/models/message_like.rs @@ -131,10 +131,7 @@ impl TryFrom> for MessageLike { let id = MessageLikeId::new(msg.id.as_ref().map(|id| id.into())); let stanza_id = msg.stanza_id(); - let from = msg - .from - .as_ref() - .ok_or(StanzaParseError::missing_attribute("from"))?; + let from = msg.resolved_from()?; let to = msg.to.as_ref(); let timestamp = msg .delay() @@ -150,7 +147,7 @@ impl TryFrom> for MessageLike { stanza_id: stanza_id.map(|s| s.id.as_ref().into()), target: refs.map(|id| id.as_ref().into()), to: to.map(|jid| jid.to_bare()), - from: from.clone(), + from, timestamp: timestamp.into(), payload, }) @@ -187,10 +184,7 @@ impl TryFrom<(Option, &Forwarded)> for MessageLike { let id = MessageLikeId::new(message.id.as_ref().map(|id| id.into())); let to = message.to.as_ref(); - let from = message - .from - .as_ref() - .ok_or(StanzaParseError::missing_attribute("from"))?; + let from = message.resolved_from()?; let timestamp = &carbon .delay .as_ref() @@ -202,7 +196,7 @@ impl TryFrom<(Option, &Forwarded)> for MessageLike { stanza_id: Some(stanza_id.as_ref().into()), target: refs.map(|id| id.as_ref().into()), to: to.map(|jid| jid.to_bare()), - from: from.clone(), + from, timestamp: timestamp.0.into(), payload, }) @@ -280,3 +274,21 @@ impl TryFrom<&Message> for TargetedPayload { Err(MessageLikeError::NoPayload.into()) } } + +trait MessageExt { + /// Returns either the real jid from a muc user or the original `from` value. + fn resolved_from(&self) -> Result; +} + +impl MessageExt for Message { + fn resolved_from(&self) -> Result { + if let Some(muc_user) = &self.muc_user() { + if let Some(jid) = &muc_user.jid { + return Ok(jid.clone()); + } + } + self.from + .clone() + .ok_or(StanzaParseError::missing_attribute("from")) + } +} diff --git a/crates/prose-core-client/src/domain/rooms/models/mod.rs b/crates/prose-core-client/src/domain/rooms/models/mod.rs index 1cbec77e..a3a4760d 100644 --- a/crates/prose-core-client/src/domain/rooms/models/mod.rs +++ b/crates/prose-core-client/src/domain/rooms/models/mod.rs @@ -4,18 +4,14 @@ // License: Mozilla Public License v2.0 (MPL v2.0) pub use bookmark::Bookmark; -pub use composing_user::ComposingUser; pub use room_config::RoomConfig; pub use room_error::RoomError; -#[cfg(feature = "test")] -pub use room_internals::RoomInfo; -pub use room_internals::RoomInternals; +pub use room_internals::{Member, RoomInfo, RoomInternals}; pub use room_metadata::RoomMetadata; pub use room_settings::{RoomSettings, RoomValidationError}; pub use room_state::{Occupant, RoomState}; mod bookmark; -mod composing_user; mod room_config; mod room_error; mod room_internals; diff --git a/crates/prose-core-client/src/domain/rooms/models/room_internals.rs b/crates/prose-core-client/src/domain/rooms/models/room_internals.rs index 0f79c1ab..88d171da 100644 --- a/crates/prose-core-client/src/domain/rooms/models/room_internals.rs +++ b/crates/prose-core-client/src/domain/rooms/models/room_internals.rs @@ -3,14 +3,15 @@ // Copyright: 2023, Marc Bauer // License: Mozilla Public License v2.0 (MPL v2.0) +use std::collections::HashMap; + use jid::{BareJid, Jid}; use parking_lot::RwLock; -use std::collections::HashMap; use xmpp_parsers::chatstates::ChatState; use xmpp_parsers::muc::user::Affiliation; use crate::domain::contacts::models::Contact; -use crate::domain::rooms::models::{RoomMetadata, RoomState}; +use crate::domain::rooms::models::RoomState; use crate::domain::shared::models::RoomType; use crate::dtos::Occupant; @@ -34,11 +35,16 @@ pub struct RoomInfo { /// The nickname with which our user is connected to the room. pub user_nickname: String, /// The list of members. Only available for DirectMessage and Group (member-only rooms). - pub members: Vec, + pub members: HashMap, /// The type of the room. pub room_type: RoomType, } +#[derive(Debug, Clone, PartialEq)] +pub struct Member { + pub name: String, +} + impl RoomInternals { pub fn pending(room_jid: &BareJid, user_jid: &BareJid, nickname: &str) -> Self { Self { @@ -48,7 +54,7 @@ impl RoomInternals { description: None, user_jid: user_jid.clone(), user_nickname: nickname.to_string(), - members: vec![], + members: HashMap::new(), room_type: RoomType::Pending, }, state: Default::default(), @@ -58,32 +64,6 @@ impl RoomInternals { pub fn is_pending(&self) -> bool { self.info.room_type == RoomType::Pending } - - pub fn to_permanent(&self, metadata: RoomMetadata) -> Self { - assert!(self.is_pending(), "Cannot promote a non-pending room"); - - let mut room = Self { - info: self.info.clone(), - state: RwLock::new(self.state.read().clone()), - }; - - room.info.jid = metadata.room_jid.to_bare(); - room.info.user_nickname = metadata.room_jid.resource_str().to_string(); - room.info.name = metadata.settings.name; - room.info.description = metadata.settings.description; - room.info.members = metadata.members; - - let features = &metadata.settings.features; - - room.info.room_type = match features { - _ if features.can_act_as_group() => RoomType::Group, - _ if features.can_act_as_private_channel() => RoomType::PrivateChannel, - _ if features.can_act_as_public_channel() => RoomType::PublicChannel, - _ => RoomType::Generic, - }; - - room - } } impl RoomInternals { @@ -95,7 +75,12 @@ impl RoomInternals { description: None, user_jid: user_jid.clone(), user_nickname: "no_nickname".to_string(), - members: vec![contact.jid.clone()], + members: HashMap::from([( + contact.jid.clone(), + Member { + name: contact_name.to_string(), + }, + )]), room_type: RoomType::DirectMessage, }, state: RwLock::new(RoomState { @@ -104,6 +89,7 @@ impl RoomInternals { Jid::Bare(contact.jid.clone()), Occupant { jid: Some(contact.jid.clone()), + name: Some(contact_name.to_string()), affiliation: Affiliation::Owner, chat_state: ChatState::Gone, chat_state_updated: Default::default(), @@ -123,11 +109,13 @@ impl PartialEq for RoomInternals { #[cfg(test)] mod tests { - use prose_xmpp::{bare, jid}; use std::collections::HashMap; + use xmpp_parsers::chatstates::ChatState; use xmpp_parsers::muc::user::Affiliation; + use prose_xmpp::{bare, jid}; + use crate::domain::contacts::models::Group; use crate::dtos::Occupant; @@ -154,7 +142,12 @@ mod tests { description: None, user_jid: bare!("logged-in-user@prose.org"), user_nickname: "no_nickname".to_string(), - members: vec![bare!("contact@prose.org")], + members: HashMap::from([( + bare!("contact@prose.org"), + Member { + name: "Jane Doe".to_string() + } + )]), room_type: RoomType::DirectMessage, }, state: RwLock::new(RoomState { @@ -163,6 +156,7 @@ mod tests { jid!("contact@prose.org"), Occupant { jid: Some(bare!("contact@prose.org")), + name: Some("Jane Doe".to_string()), affiliation: Affiliation::Owner, chat_state: ChatState::Gone, chat_state_updated: Default::default(), diff --git a/crates/prose-core-client/src/domain/rooms/models/room_state.rs b/crates/prose-core-client/src/domain/rooms/models/room_state.rs index 497f96d4..e261b0f5 100644 --- a/crates/prose-core-client/src/domain/rooms/models/room_state.rs +++ b/crates/prose-core-client/src/domain/rooms/models/room_state.rs @@ -5,6 +5,8 @@ use std::collections::HashMap; +use crate::domain::shared::models::UserBasicInfo; +use crate::util::jid_ext::BareJidExt; use chrono::{DateTime, Utc}; use jid::{BareJid, Jid}; use xmpp_parsers::chatstates::ChatState; @@ -23,6 +25,7 @@ pub struct RoomState { pub struct Occupant { /// The real JID of the occupant. Only available in non-anonymous rooms. pub jid: Option, + pub name: Option, pub affiliation: Affiliation, pub chat_state: ChatState, pub chat_state_updated: DateTime, @@ -32,6 +35,7 @@ impl Default for Occupant { fn default() -> Self { Self { jid: None, + name: None, affiliation: Default::default(), chat_state: ChatState::Gone, chat_state_updated: Default::default(), @@ -44,10 +48,12 @@ impl RoomState { &mut self, jid: &Jid, real_jid: Option<&BareJid>, + name: Option<&str>, affiliation: &Affiliation, ) { let occupant = self.occupants.entry(jid.clone()).or_default(); occupant.jid = real_jid.cloned(); + occupant.name = name.map(ToString::to_string); occupant.affiliation = affiliation.clone(); } @@ -67,7 +73,7 @@ impl RoomState { /// Returns the real JIDs of all composing users that started composing after `started_after`. /// If we don't have a real JID for a composing user they are excluded from the list. - pub fn composing_users(&self, started_after: DateTime) -> Vec { + pub fn composing_users(&self, started_after: DateTime) -> Vec { let mut composing_occupants = self .occupants .values() @@ -81,16 +87,26 @@ impl RoomState { Some(occupant.clone()) }) .collect::>(); + composing_occupants.sort_by_key(|o| o.chat_state_updated); + composing_occupants .into_iter() - .filter_map(|occupant| occupant.jid) + .filter_map(|occupant| { + let Some(jid) = &occupant.jid else { + return None; + }; + + Some(UserBasicInfo { + name: occupant + .name + .clone() + .unwrap_or_else(|| jid.to_display_name()), + jid: jid.clone(), + }) + }) .collect() } - - pub fn real_jid_for_occupant(&self, occupant_jid: &Jid) -> Option { - self.occupants.get(occupant_jid).and_then(|o| o.jid.clone()) - } } #[cfg(test)] @@ -109,9 +125,10 @@ mod tests { state.insert_occupant( &jid!("room@prose.org/a"), Some(&bare!("a@prose.org")), + None, &Affiliation::Owner, ); - state.insert_occupant(&jid!("b@prose.org"), None, &Affiliation::Member); + state.insert_occupant(&jid!("b@prose.org"), None, None, &Affiliation::Member); assert_eq!(state.occupants.len(), 2); assert_eq!( @@ -119,17 +136,14 @@ mod tests { &Occupant { jid: Some(bare!("a@prose.org")), affiliation: Affiliation::Owner, - chat_state: ChatState::Gone, - chat_state_updated: Default::default(), + ..Default::default() } ); assert_eq!( state.occupants.get(&jid!("b@prose.org")).unwrap(), &Occupant { - jid: None, affiliation: Affiliation::Member, - chat_state: ChatState::Gone, - chat_state_updated: Default::default(), + ..Default::default() } ); } @@ -141,6 +155,7 @@ mod tests { state.insert_occupant( &jid!("room@prose.org/a"), Some(&bare!("a@prose.org")), + None, &Affiliation::Owner, ); @@ -194,6 +209,7 @@ mod tests { jid!("room@prose.org/c"), Occupant { jid: Some(bare!("c@prose.org")), + name: Some("Jonathan Doe".to_string()), chat_state: ChatState::Composing, chat_state_updated: Utc.with_ymd_and_hms(2023, 01, 03, 0, 0, 20).unwrap(), ..Default::default() @@ -211,7 +227,16 @@ mod tests { assert_eq!( state.composing_users(Utc.with_ymd_and_hms(2023, 01, 03, 0, 0, 10).unwrap()), - vec![bare!("c@prose.org"), bare!("a@prose.org")] + vec![ + UserBasicInfo { + name: "Jonathan Doe".to_string(), + jid: bare!("c@prose.org") + }, + UserBasicInfo { + name: "A".to_string(), + jid: bare!("a@prose.org") + }, + ] ); } } diff --git a/crates/prose-core-client/src/domain/rooms/services/impls/rooms_domain_service.rs b/crates/prose-core-client/src/domain/rooms/services/impls/rooms_domain_service.rs index f2068533..489f17d5 100644 --- a/crates/prose-core-client/src/domain/rooms/services/impls/rooms_domain_service.rs +++ b/crates/prose-core-client/src/domain/rooms/services/impls/rooms_domain_service.rs @@ -3,6 +3,7 @@ // Copyright: 2023, Marc Bauer // License: Mozilla Public License v2.0 (MPL v2.0) +use std::collections::HashMap; use std::future::Future; use std::iter; use std::sync::Arc; @@ -10,6 +11,7 @@ use std::sync::Arc; use anyhow::{Context, Result}; use async_trait::async_trait; use jid::{BareJid, NodePart}; +use parking_lot::RwLock; use sha1::{Digest, Sha1}; use tracing::{error, info}; use xmpp_parsers::stanza_error::DefinedCondition; @@ -18,18 +20,23 @@ use prose_wasm_utils::PinnedFuture; use prose_xmpp::mods::muc::RoomConfigResponse; use prose_xmpp::RequestError; -use super::super::{ - CreateOrEnterRoomRequest, CreateOrEnterRoomRequestType, CreateRoomType, - RoomsDomainService as RoomsDomainServiceTrait, -}; use crate::app::deps::{ DynAppContext, DynBookmarksRepository, DynClientEventDispatcher, DynConnectedRoomsRepository, DynIDProvider, DynRoomManagementService, DynRoomParticipationService, DynUserProfileRepository, }; -use crate::domain::rooms::models::{Bookmark, RoomConfig, RoomError, RoomInternals, RoomMetadata}; +use crate::domain::rooms::models::{ + Bookmark, Member, RoomConfig, RoomError, RoomInfo, RoomInternals, RoomMetadata, +}; +use crate::domain::shared::models::RoomType; +use crate::util::jid_ext::BareJidExt; use crate::util::StringExt; use crate::ClientEvent; +use super::super::{ + CreateOrEnterRoomRequest, CreateOrEnterRoomRequestType, CreateRoomType, + RoomsDomainService as RoomsDomainServiceTrait, +}; + const GROUP_PREFIX: &str = "org.prose.group"; const PRIVATE_CHANNEL_PREFIX: &str = "org.prose.private-channel"; const PUBLIC_CHANNEL_PREFIX: &str = "org.prose.public-channel"; @@ -148,12 +155,18 @@ impl RoomsDomainServiceTrait for RoomsDomainService { // It could be the case that the room_jid was modified, i.e. if the preferred JID was // taken already. let room_jid = metadata.room_jid.clone(); + let room_info = self.collect_room_info(&user_jid, metadata).await?; let Some(room) = self.connected_rooms_repo.update( &room_jid.to_bare(), Box::new(move |room| { // Convert the temporary room to its final form… - room.to_permanent(metadata) + assert!(room.is_pending(), "Cannot promote a non-pending room"); + + RoomInternals { + info: room_info, + state: RwLock::new(room.state.read().clone()), + } }), ) else { return Err(RequestError::Generic { @@ -455,6 +468,45 @@ impl RoomsDomainService { }; } } + + async fn collect_room_info( + &self, + user_jid: &BareJid, + metadata: RoomMetadata, + ) -> Result { + let mut members = HashMap::with_capacity(metadata.members.len()); + + for jid in metadata.members { + let name = self + .user_profile_repo + .get_display_name(&jid) + .await + .unwrap_or_default() + .unwrap_or_else(|| jid.to_display_name()); + members.insert(jid, Member { name }); + } + + let features = &metadata.settings.features; + + let room_type = match features { + _ if features.can_act_as_group() => RoomType::Group, + _ if features.can_act_as_private_channel() => RoomType::PrivateChannel, + _ if features.can_act_as_public_channel() => RoomType::PublicChannel, + _ => RoomType::Generic, + }; + + let room_info = RoomInfo { + jid: metadata.room_jid.to_bare(), + name: metadata.settings.name, + description: metadata.settings.description, + user_jid: user_jid.clone(), + user_nickname: metadata.room_jid.resource_str().to_string(), + members, + room_type, + }; + + Ok(room_info) + } } impl RoomsDomainService { diff --git a/crates/prose-core-client/src/domain/shared/models/mod.rs b/crates/prose-core-client/src/domain/shared/models/mod.rs index 3d815d99..1b479d3f 100644 --- a/crates/prose-core-client/src/domain/shared/models/mod.rs +++ b/crates/prose-core-client/src/domain/shared/models/mod.rs @@ -5,6 +5,8 @@ pub use availability::Availability; pub use room_type::RoomType; +pub use user_info::{UserBasicInfo, UserPresenceInfo}; mod availability; mod room_type; +mod user_info; diff --git a/crates/prose-core-client/src/domain/rooms/models/composing_user.rs b/crates/prose-core-client/src/domain/shared/models/user_info.rs similarity index 53% rename from crates/prose-core-client/src/domain/rooms/models/composing_user.rs rename to crates/prose-core-client/src/domain/shared/models/user_info.rs index c509c217..41bfead3 100644 --- a/crates/prose-core-client/src/domain/rooms/models/composing_user.rs +++ b/crates/prose-core-client/src/domain/shared/models/user_info.rs @@ -5,9 +5,17 @@ use jid::BareJid; -/// A user who is currently typing in a Room. +use crate::dtos::Availability; + #[derive(Debug, Clone, PartialEq)] -pub struct ComposingUser { +pub struct UserBasicInfo { + pub jid: BareJid, pub name: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct UserPresenceInfo { pub jid: BareJid, + pub name: String, + pub availability: Availability, } diff --git a/crates/prose-core-client/src/domain/shared/utils/contact_name_builder.rs b/crates/prose-core-client/src/domain/shared/utils/contact_name_builder.rs index 3cab06f2..5166de40 100644 --- a/crates/prose-core-client/src/domain/shared/utils/contact_name_builder.rs +++ b/crates/prose-core-client/src/domain/shared/utils/contact_name_builder.rs @@ -5,7 +5,7 @@ use crate::app::dtos::UserProfile; use crate::domain::contacts::models::Contact; -use crate::util::jid_ext::JidExt; +use crate::util::jid_ext::BareJidExt; use std::ops::Deref; pub(crate) fn build_contact_name(contact: &Contact, profile: &UserProfile) -> String { diff --git a/crates/prose-core-client/src/infra/platform_dependencies.rs b/crates/prose-core-client/src/infra/platform_dependencies.rs index af38f0c3..b162a59a 100644 --- a/crates/prose-core-client/src/infra/platform_dependencies.rs +++ b/crates/prose-core-client/src/infra/platform_dependencies.rs @@ -3,6 +3,7 @@ // Copyright: 2023, Marc Bauer // License: Mozilla Public License v2.0 (MPL v2.0) +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use prose_store::prelude::*; @@ -37,19 +38,41 @@ pub(crate) struct PlatformDependencies { } pub async fn open_store(driver: D) -> Result, D::Error> { - Store::open(driver, 10, |event| { + let versions_changed = Arc::new(AtomicBool::new(false)); + + let inner_versions_changed = versions_changed.clone(); + let store = Store::open(driver, 12, move |event| { let tx = &event.tx; - create_collection::(&tx)?; - create_collection::(&tx)?; - create_collection::(&tx)?; - create_collection::(&tx)?; - create_collection::(&tx)?; - #[cfg(target_arch = "wasm32")] - create_collection::(&tx)?; + inner_versions_changed.store(true, Ordering::Relaxed); + + if event.old_version < 10 { + create_collection::(&tx)?; + create_collection::(&tx)?; + create_collection::(&tx)?; + create_collection::(&tx)?; + create_collection::(&tx)?; + #[cfg(target_arch = "wasm32")] + create_collection::(&tx)?; + } + Ok(()) }) - .await + .await?; + + if versions_changed.load(Ordering::Acquire) { + store + .truncate_collections(&[ + MessagesRecord::collection(), + UserInfoRecord::collection(), + UserProfileRecord::collection(), + #[cfg(target_arch = "wasm32")] + crate::infra::avatars::AvatarRecord::collection(), + ]) + .await?; + } + + Ok(store) } fn create_collection(tx: &D::UpgradeTransaction<'_>) -> Result<(), D::Error> { diff --git a/crates/prose-core-client/src/test/message_builder.rs b/crates/prose-core-client/src/test/message_builder.rs index 6b85a0c8..1814050d 100644 --- a/crates/prose-core-client/src/test/message_builder.rs +++ b/crates/prose-core-client/src/test/message_builder.rs @@ -13,12 +13,13 @@ use xmpp_parsers::{date, mam, Element}; use prose_xmpp::stanza::message; use prose_xmpp::stanza::message::mam::ArchivedMessage; -use prose_xmpp::stanza::message::Forwarded; +use prose_xmpp::stanza::message::{Forwarded, MucUser}; use prose_xmpp::test::BareJidTestAdditions; use crate::domain::messaging::models::{ Message, MessageId, MessageLike, MessageLikeId, MessageLikePayload, Reaction, StanzaId, }; +use crate::dtos::{Message as MessageDTO, MessageSender}; use crate::test::mock_data; impl From for MessageLikeId @@ -34,6 +35,7 @@ pub struct MessageBuilder { id: MessageId, stanza_id: Option, from: Jid, + from_name: Option, to: BareJid, body: String, timestamp: DateTime, @@ -55,6 +57,7 @@ impl MessageBuilder { id: Self::id_for_index(idx), stanza_id: Some(format!("res-{}", idx).into()), from: Jid::Bare(BareJid::ours()), + from_name: None, to: BareJid::theirs(), body: format!("Message {}", idx).to_string(), timestamp: mock_data::reference_date() + Duration::minutes(idx.into()), @@ -74,6 +77,11 @@ impl MessageBuilder { self.from = from.clone(); self } + + pub fn set_from_name(mut self, name: impl Into) -> Self { + self.from_name = Some(name.into()); + self + } } impl MessageBuilder { @@ -81,7 +89,26 @@ impl MessageBuilder { Message { id: Some(self.id), stanza_id: self.stanza_id, - from: self.from.into_bare(), + from: self.from, + body: self.body, + timestamp: self.timestamp.into(), + is_read: self.is_read, + is_edited: self.is_edited, + is_delivered: self.is_delivered, + reactions: self.reactions, + } + } + + pub fn build_message_dto(self) -> MessageDTO { + MessageDTO { + id: Some(self.id), + stanza_id: self.stanza_id, + from: MessageSender { + jid: self.from, + name: self + .from_name + .expect("You must set a name when building a MessageDTO"), + }, body: self.body, timestamp: self.timestamp.into(), is_read: self.is_read, @@ -128,13 +155,32 @@ impl MessageBuilder { ) } - pub fn build_mam_message(self, query_id: impl Into) -> Element { + pub fn build_mam_message( + self, + query_id: impl Into, + muc_user: Option, + ) -> Element { prose_xmpp::stanza::Message::new() - .set_archived_message(self.build_archived_message(query_id)) + .set_archived_message(self.build_archived_message(query_id, muc_user)) .into() } - pub fn build_archived_message(self, query_id: impl Into) -> ArchivedMessage { + pub fn build_archived_message( + self, + query_id: impl Into, + muc_user: Option, + ) -> ArchivedMessage { + let mut message = prose_xmpp::stanza::Message::new() + .set_id(self.id.as_ref().into()) + .set_type(MessageType::Chat) + .set_to(self.to) + .set_from(self.from) + .set_body(self.body); + + if let Some(muc_user) = muc_user { + message = message.set_muc_user(muc_user); + } + ArchivedMessage { id: self.stanza_id.expect("Missing stanzaId").to_string().into(), query_id: Some(mam::QueryId(query_id.into())), @@ -144,14 +190,7 @@ impl MessageBuilder { stamp: date::DateTime(self.timestamp.into()), data: None, }), - stanza: Some(Box::new( - prose_xmpp::stanza::Message::new() - .set_id(self.id.as_ref().into()) - .set_type(MessageType::Chat) - .set_to(self.to) - .set_from(self.from) - .set_body(self.body), - )), + stanza: Some(Box::new(message)), }, } } diff --git a/crates/prose-core-client/src/test/room_internals.rs b/crates/prose-core-client/src/test/room_internals.rs index 7c66c7ae..f676d5eb 100644 --- a/crates/prose-core-client/src/test/room_internals.rs +++ b/crates/prose-core-client/src/test/room_internals.rs @@ -3,13 +3,15 @@ // Copyright: 2023, Marc Bauer // License: Mozilla Public License v2.0 (MPL v2.0) +use chrono::{DateTime, Utc}; use jid::{BareJid, Jid}; +use std::collections::HashMap; use xmpp_parsers::chatstates::ChatState; use xmpp_parsers::muc::user::Affiliation; use crate::domain::rooms::models::{RoomInfo, RoomInternals}; use crate::domain::shared::models::RoomType; -use crate::dtos::Occupant; +use crate::dtos::{Member, Occupant}; use crate::test::mock_data; impl RoomInternals { @@ -21,13 +23,18 @@ impl RoomInternals { description: None, user_jid: mock_data::account_jid().into_bare(), user_nickname: mock_data::account_jid().node_str().unwrap().to_string(), - members: vec![], + members: HashMap::new(), room_type: RoomType::Group, }, state: Default::default(), } } + pub fn with_members(mut self, members: impl IntoIterator) -> Self { + self.info.members = members.into_iter().collect(); + self + } + pub fn with_occupants(self, occupant: impl IntoIterator) -> Self { self.state.write().occupants = occupant.into_iter().collect(); self @@ -38,6 +45,17 @@ impl Occupant { pub fn owner() -> Self { Occupant { jid: None, + name: None, + affiliation: Affiliation::Owner, + chat_state: ChatState::Gone, + chat_state_updated: Default::default(), + } + } + + pub fn member() -> Self { + Occupant { + jid: None, + name: None, affiliation: Affiliation::Owner, chat_state: ChatState::Gone, chat_state_updated: Default::default(), @@ -48,4 +66,19 @@ impl Occupant { self.jid = Some(jid.clone()); self } + + pub fn set_name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + + pub fn set_chat_state(mut self, chat_state: ChatState) -> Self { + self.chat_state = chat_state; + self + } + + pub fn set_chat_state_updated(mut self, timestamp: DateTime) -> Self { + self.chat_state_updated = timestamp; + self + } } diff --git a/crates/prose-core-client/src/util/jid_ext.rs b/crates/prose-core-client/src/util/jid_ext.rs index 6fd5e2fb..68669e99 100644 --- a/crates/prose-core-client/src/util/jid_ext.rs +++ b/crates/prose-core-client/src/util/jid_ext.rs @@ -3,27 +3,61 @@ // Copyright: 2023, Marc Bauer // License: Mozilla Public License v2.0 (MPL v2.0) -use jid::BareJid; +use jid::{BareJid, FullJid, Jid}; use crate::util::StringExt; -pub trait JidExt { +pub trait BareJidExt { fn to_display_name(&self) -> String; } -impl JidExt for BareJid { +pub trait FullJidExt { + fn resource_to_display_name(&self) -> String; +} + +pub trait JidExt { + fn node_to_display_name(&self) -> String; + fn resource_to_display_name(&self) -> String; +} + +impl BareJidExt for BareJid { fn to_display_name(&self) -> String { let Some(node) = self.node_str() else { return self.to_string().to_uppercase_first_letter(); }; + str_to_display_name(node) + } +} + +impl FullJidExt for FullJid { + fn resource_to_display_name(&self) -> String { + str_to_display_name(self.resource_str()) + } +} + +impl JidExt for Jid { + fn node_to_display_name(&self) -> String { + let Some(node) = self.node_str() else { + return self.to_string().to_uppercase_first_letter(); + }; + str_to_display_name(node) + } - node.split_terminator(&['.', '_', '-'][..]) - .map(|s| s.to_uppercase_first_letter()) - .collect::>() - .join(" ") + fn resource_to_display_name(&self) -> String { + let Some(resource) = self.resource_str() else { + return self.to_string().to_uppercase_first_letter(); + }; + str_to_display_name(resource) } } +fn str_to_display_name(text: &str) -> String { + text.split_terminator(&['.', '_', '-'][..]) + .map(|s| s.to_uppercase_first_letter()) + .collect::>() + .join(" ") +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/prose-core-client/tests/room.rs b/crates/prose-core-client/tests/room.rs index c508b5f0..3e83f4d3 100644 --- a/crates/prose-core-client/tests/room.rs +++ b/crates/prose-core-client/tests/room.rs @@ -14,24 +14,32 @@ use prose_core_client::domain::messaging::models::MessageLikePayload; use prose_core_client::domain::rooms::models::RoomInternals; use prose_core_client::domain::rooms::services::RoomFactory; use prose_core_client::domain::shared::models::RoomType; -use prose_core_client::dtos::Occupant; +use prose_core_client::dtos::{Member, Occupant}; use prose_core_client::test::{mock_data, MessageBuilder, MockRoomFactoryDependencies}; +use prose_xmpp::stanza::message::MucUser; use prose_xmpp::{bare, jid}; #[tokio::test] async fn test_load_messages_with_ids_resolves_real_jids() -> Result<()> { let mut deps = MockRoomFactoryDependencies::default(); - let internals = RoomInternals::group(&bare!("room@conference.prose.org")).with_occupants([ - ( - jid!("room@conference.prose.org/a"), - Occupant::owner().set_real_jid(&bare!("a@prose.org")), - ), - ( - jid!("room@conference.prose.org/c"), - Occupant::owner().set_real_jid(&bare!("c@prose.org")), - ), - ]); + let internals = RoomInternals::group(&bare!("room@conference.prose.org")) + .with_members([( + bare!("a@prose.org"), + Member { + name: "Aron Doe".to_string(), + }, + )]) + .with_occupants([( + jid!("room@conference.prose.org/b"), + Occupant::owner().set_name("Bernhard Doe"), + )]); + + deps.user_profile_repo + .expect_get_display_name() + .once() + .with(predicate::eq(bare!("c@prose.org"))) + .return_once(|_| Box::pin(async { Ok(Some("Carl Doe".to_string())) })); deps.message_repo .expect_get_all() @@ -40,13 +48,16 @@ async fn test_load_messages_with_ids_resolves_real_jids() -> Result<()> { Box::pin(async { Ok(vec![ MessageBuilder::new_with_index(1) - .set_from(&jid!("room@conference.prose.org/a")) + .set_from(&jid!("a@prose.org")) .build_message_like(), MessageBuilder::new_with_index(2) .set_from(&jid!("room@conference.prose.org/b")) .build_message_like(), MessageBuilder::new_with_index(3) - .set_from(&jid!("room@conference.prose.org/c")) + .set_from(&jid!("c@prose.org")) + .build_message_like(), + MessageBuilder::new_with_index(4) + .set_from(&jid!("room@conference.prose.org/denise_doe")) .build_message_like(), ]) }) @@ -66,13 +77,20 @@ async fn test_load_messages_with_ids_resolves_real_jids() -> Result<()> { vec![ MessageBuilder::new_with_index(1) .set_from(&jid!("a@prose.org")) - .build_message(), + .set_from_name("Aron Doe") + .build_message_dto(), MessageBuilder::new_with_index(2) .set_from(&jid!("room@conference.prose.org/b")) - .build_message(), + .set_from_name("Bernhard Doe") + .build_message_dto(), MessageBuilder::new_with_index(3) .set_from(&jid!("c@prose.org")) - .build_message(), + .set_from_name("Carl Doe") + .build_message_dto(), + MessageBuilder::new_with_index(4) + .set_from(&jid!("room@conference.prose.org/denise_doe")) + .set_from_name("Denise Doe") + .build_message_dto(), ] ); @@ -83,16 +101,23 @@ async fn test_load_messages_with_ids_resolves_real_jids() -> Result<()> { async fn test_load_latest_messages_resolves_real_jids() -> Result<()> { let mut deps = MockRoomFactoryDependencies::default(); - let internals = RoomInternals::group(&bare!("room@conference.prose.org")).with_occupants([ - ( - jid!("room@conference.prose.org/a"), - Occupant::owner().set_real_jid(&bare!("a@prose.org")), - ), - ( - jid!("room@conference.prose.org/c"), - Occupant::owner().set_real_jid(&bare!("c@prose.org")), - ), - ]); + let internals = RoomInternals::group(&bare!("room@conference.prose.org")) + .with_members([( + bare!("a@prose.org"), + Member { + name: "Aron Doe".to_string(), + }, + )]) + .with_occupants([( + jid!("room@conference.prose.org/b"), + Occupant::owner().set_name("Bernhard Doe"), + )]); + + deps.user_profile_repo + .expect_get_display_name() + .once() + .with(predicate::eq(bare!("c@prose.org"))) + .return_once(|_| Box::pin(async { Ok(Some("Carl Doe".to_string())) })); deps.message_archive_service .expect_load_messages() @@ -103,13 +128,30 @@ async fn test_load_latest_messages_resolves_real_jids() -> Result<()> { vec![ MessageBuilder::new_with_index(1) .set_from(&jid!("room@conference.prose.org/a")) - .build_archived_message("q1"), + .build_archived_message( + "q1", + Some(MucUser { + jid: Some(jid!("a@prose.org")), + affiliation: Default::default(), + role: Default::default(), + }), + ), MessageBuilder::new_with_index(2) .set_from(&jid!("room@conference.prose.org/b")) - .build_archived_message("q1"), + .build_archived_message("q1", None), MessageBuilder::new_with_index(3) .set_from(&jid!("room@conference.prose.org/c")) - .build_archived_message("q1"), + .build_archived_message( + "q1", + Some(MucUser { + jid: Some(jid!("c@prose.org")), + affiliation: Default::default(), + role: Default::default(), + }), + ), + MessageBuilder::new_with_index(4) + .set_from(&jid!("room@conference.prose.org/denise_doe")) + .build_archived_message("q1", None), ], Fin { complete: Default::default(), @@ -138,13 +180,20 @@ async fn test_load_latest_messages_resolves_real_jids() -> Result<()> { vec![ MessageBuilder::new_with_index(1) .set_from(&jid!("a@prose.org")) - .build_message(), + .set_from_name("Aron Doe") + .build_message_dto(), MessageBuilder::new_with_index(2) .set_from(&jid!("room@conference.prose.org/b")) - .build_message(), + .set_from_name("Bernhard Doe") + .build_message_dto(), MessageBuilder::new_with_index(3) .set_from(&jid!("c@prose.org")) - .build_message(), + .set_from_name("Carl Doe") + .build_message_dto(), + MessageBuilder::new_with_index(4) + .set_from(&jid!("room@conference.prose.org/denise_doe")) + .set_from_name("Denise Doe") + .build_message_dto(), ] ); diff --git a/crates/prose-core-client/tests/rooms_event_handler.rs b/crates/prose-core-client/tests/rooms_event_handler.rs index 8e470cc3..62eca25f 100644 --- a/crates/prose-core-client/tests/rooms_event_handler.rs +++ b/crates/prose-core-client/tests/rooms_event_handler.rs @@ -3,6 +3,7 @@ // Copyright: 2023, Marc Bauer // License: Mozilla Public License v2.0 (MPL v2.0) +use std::collections::HashMap; use std::sync::Arc; use anyhow::Result; @@ -19,7 +20,7 @@ use prose_core_client::domain::contacts::models::Contact; use prose_core_client::domain::rooms::models::{RoomInfo, RoomInternals}; use prose_core_client::domain::rooms::services::RoomFactory; use prose_core_client::domain::shared::models::RoomType; -use prose_core_client::dtos::{ComposingUser, Group, Occupant}; +use prose_core_client::dtos::{Group, Occupant, UserBasicInfo}; use prose_core_client::test::{ mock_data, ConstantTimeProvider, MockAppDependencies, MockRoomFactoryDependencies, }; @@ -37,7 +38,7 @@ async fn test_handles_presence_for_muc_room() -> Result<()> { description: None, user_jid: mock_data::account_jid().into_bare(), user_nickname: "".to_string(), - members: vec![], + members: HashMap::new(), room_type: RoomType::Group, }, state: Default::default(), @@ -52,6 +53,12 @@ async fn test_handles_presence_for_muc_room() -> Result<()> { .return_once(move |_| Some(room.clone())); } + deps.user_profile_repo + .expect_get_display_name() + .once() + .with(predicate::eq(bare!("real-jid@prose.org"))) + .return_once(|_| Box::pin(async { Ok(Some("George Washington".to_string())) })); + let event_handler = RoomsEventHandler::from(&deps.into_deps()); event_handler @@ -81,6 +88,7 @@ async fn test_handles_presence_for_muc_room() -> Result<()> { occupant, Occupant { jid: Some(bare!("real-jid@prose.org")), + name: Some("George Washington".to_string()), affiliation: Affiliation::Member, chat_state: ChatState::Gone, chat_state_updated: Default::default(), @@ -97,7 +105,9 @@ async fn test_handles_chat_state_for_muc_room() -> Result<()> { let room = Arc::new( RoomInternals::group(&bare!("room@conference.prose.org")).with_occupants([( jid!("room@conference.prose.org/nickname"), - Occupant::owner().set_real_jid(&bare!("nickname@prose.org")), + Occupant::owner() + .set_real_jid(&bare!("nickname@prose.org")) + .set_name("Janice Doe"), )]), ); @@ -147,18 +157,12 @@ async fn test_handles_chat_state_for_muc_room() -> Result<()> { let mut factory_deps = MockRoomFactoryDependencies::default(); factory_deps.time_provider = time_provider.clone(); - factory_deps - .user_profile_repo - .expect_get_display_name() - .once() - .with(predicate::eq(bare!("nickname@prose.org"))) - .return_once(|_| Box::pin(async { Ok(Some("Janice Doe".to_string())) })); let room_factory = RoomFactory::from(factory_deps); let room = room_factory.build(room.clone()).to_generic_room(); assert_eq!( room.load_composing_users().await?, - vec![ComposingUser { + vec![UserBasicInfo { name: "Janice Doe".to_string(), jid: bare!("nickname@prose.org") }] @@ -181,7 +185,7 @@ async fn test_handles_chat_state_for_direct_message_room() -> Result<()> { name: None, group: Group::Team, }, - "", + "Janice Doe", )); { @@ -230,18 +234,12 @@ async fn test_handles_chat_state_for_direct_message_room() -> Result<()> { let mut factory_deps = MockRoomFactoryDependencies::default(); factory_deps.time_provider = time_provider.clone(); - factory_deps - .user_profile_repo - .expect_get_display_name() - .once() - .with(predicate::eq(bare!("contact@prose.org"))) - .return_once(|_| Box::pin(async { Ok(Some("Janice Doe".to_string())) })); let room_factory = RoomFactory::from(factory_deps); let room = room_factory.build(room.clone()).to_generic_room(); assert_eq!( room.load_composing_users().await?, - vec![ComposingUser { + vec![UserBasicInfo { name: "Janice Doe".to_string(), jid: bare!("contact@prose.org") }] diff --git a/crates/prose-core-client/tests/rooms_service.rs b/crates/prose-core-client/tests/rooms_service.rs index 248db8c2..152f6a8a 100644 --- a/crates/prose-core-client/tests/rooms_service.rs +++ b/crates/prose-core-client/tests/rooms_service.rs @@ -3,6 +3,7 @@ // Copyright: 2023, Marc Bauer // License: Mozilla Public License v2.0 (MPL v2.0) +use std::collections::HashMap; use std::sync::Arc; use anyhow::Result; @@ -81,7 +82,7 @@ async fn test_creates_rooms_for_contacts_and_bookmarks() -> Result<()> { description: None, user_jid: mock_data::account_jid().into_bare(), user_nickname: "".to_string(), - members: vec![], + members: HashMap::new(), room_type: RoomType::Group, }, state: Default::default(), diff --git a/crates/prose-xmpp/src/stanza/message/builder.rs b/crates/prose-xmpp/src/stanza/message/builder.rs index 6027e951..52ea91bf 100644 --- a/crates/prose-xmpp/src/stanza/message/builder.rs +++ b/crates/prose-xmpp/src/stanza/message/builder.rs @@ -15,6 +15,7 @@ use crate::stanza::message::chat_marker::{Acknowledged, Displayed, Received}; use crate::stanza::message::fasten::ApplyTo; use crate::stanza::message::mam::ArchivedMessage; use crate::stanza::message::message::Message; +use crate::stanza::message::muc_user::MucUser; use crate::stanza::message::{carbons, chat_marker, Fallback, Id, Reactions}; impl Message { @@ -126,4 +127,9 @@ impl Message { ); self } + + pub fn set_muc_user(mut self, user: MucUser) -> Self { + self.payloads.push(user.into()); + self + } } diff --git a/crates/prose-xmpp/src/stanza/message/message.rs b/crates/prose-xmpp/src/stanza/message/message.rs index b7db32d1..769012fa 100644 --- a/crates/prose-xmpp/src/stanza/message/message.rs +++ b/crates/prose-xmpp/src/stanza/message/message.rs @@ -18,6 +18,7 @@ use prose_utils::id_string; use crate::ns; use crate::stanza::message::fasten::ApplyTo; +use crate::stanza::message::muc_user::MucUser; use crate::stanza::message::stanza_id::StanzaId; use crate::stanza::message::{carbons, Reactions}; use crate::stanza::message::{chat_marker, mam}; @@ -139,6 +140,10 @@ impl Message { pub fn displayed_marker(&self) -> Option { self.typed_payload("displayed", ns::CHAT_MARKERS) } + + pub fn muc_user(&self) -> Option { + self.typed_payload("x", ns::MUC_USER) + } } impl Message { diff --git a/crates/prose-xmpp/src/stanza/message/mod.rs b/crates/prose-xmpp/src/stanza/message/mod.rs index 69e4a8dc..87bc37b0 100644 --- a/crates/prose-xmpp/src/stanza/message/mod.rs +++ b/crates/prose-xmpp/src/stanza/message/mod.rs @@ -8,6 +8,7 @@ pub use xmpp_parsers::message::MessageType; pub use fallback::Fallback; pub use forwarding::Forwarded; pub use message::{Id, Message}; +pub use muc_user::MucUser; pub use reactions::{Emoji, Reactions}; mod builder; @@ -18,6 +19,7 @@ pub mod fasten; mod forwarding; pub mod mam; mod message; +mod muc_user; mod reactions; pub mod retract; pub mod stanza_id; diff --git a/crates/prose-xmpp/src/stanza/message/muc_user.rs b/crates/prose-xmpp/src/stanza/message/muc_user.rs new file mode 100644 index 00000000..41411e90 --- /dev/null +++ b/crates/prose-xmpp/src/stanza/message/muc_user.rs @@ -0,0 +1,160 @@ +// prose-core-client/prose-xmpp +// +// Copyright: 2023, Marc Bauer +// License: Mozilla Public License v2.0 (MPL v2.0) + +use std::str::FromStr; + +use anyhow::Result; +use jid::Jid; +use minidom::Element; +use xmpp_parsers::message::MessagePayload; +use xmpp_parsers::muc::user::{Affiliation, Role}; + +use crate::{ns, ElementExt, ParseError}; + +#[derive(Debug, Clone, PartialEq)] +pub struct MucUser { + pub jid: Option, + pub affiliation: Affiliation, + pub role: Role, +} + +impl TryFrom for MucUser { + type Error = anyhow::Error; + + fn try_from(value: Element) -> Result { + value.expect_is("x", ns::MUC_USER)?; + + let Some(item) = value.get_child("item", ns::MUC_USER) else { + return Err(ParseError::Generic { + msg: "Missing item in MucUser".to_string(), + } + .into()); + }; + + let jid = item + .attr("jid") + .map(|jid_str| Jid::from_str(jid_str)) + .transpose()?; + let affiliation = Affiliation::from_str(item.attr_req("affiliation")?)?; + let role = Role::from_str(item.attr_req("role")?)?; + + Ok(Self { + jid, + affiliation, + role, + }) + } +} + +impl From for Element { + fn from(value: MucUser) -> Self { + let affiliation = match &value.affiliation { + Affiliation::Owner => "owner", + Affiliation::Admin => "admin", + Affiliation::Member => "member", + Affiliation::Outcast => "outcast", + Affiliation::None => "none", + }; + + let role = match &value.role { + Role::Moderator => "moderator", + Role::Participant => "participant", + Role::Visitor => "visitor", + Role::None => "none", + }; + + Element::builder("x", ns::MUC_USER) + .append( + Element::builder("item", ns::MUC_USER) + .attr("jid", value.jid) + .attr("affiliation", affiliation) + .attr("role", role), + ) + .build() + } +} + +impl MessagePayload for MucUser {} + +#[cfg(test)] +mod tests { + use crate::jid; + + use super::*; + + #[test] + fn test_deserialize_with_jid() -> Result<()> { + let xml = r#" + + "#; + + let elem = Element::from_str(xml)?; + let user = MucUser::try_from(elem)?; + + assert_eq!( + user, + MucUser { + jid: Some(jid!("hello@prose.org")), + affiliation: Affiliation::None, + role: Role::Participant, + } + ); + + Ok(()) + } + + #[test] + fn test_serialize_with_jid() -> Result<()> { + let user = MucUser { + jid: Some(jid!("hello@prose.org")), + affiliation: Affiliation::None, + role: Role::Moderator, + }; + + let elem = Element::from(user.clone()); + let parsed_user = MucUser::try_from(elem)?; + + assert_eq!(user, parsed_user); + + Ok(()) + } + + #[test] + fn test_deserialize_without_jid() -> Result<()> { + let xml = r#" + + "#; + + let elem = Element::from_str(xml)?; + let user = MucUser::try_from(elem)?; + + assert_eq!( + user, + MucUser { + jid: None, + affiliation: Affiliation::None, + role: Role::Participant, + } + ); + + Ok(()) + } + + #[test] + fn test_serialize_without_jid() -> Result<()> { + let user = MucUser { + jid: None, + affiliation: Affiliation::Owner, + role: Role::None, + }; + + let elem = Element::from(user.clone()); + let parsed_user = MucUser::try_from(elem)?; + + assert_eq!(user, parsed_user); + + Ok(()) + } +} diff --git a/crates/prose-xmpp/src/stanza/muc/mod.rs b/crates/prose-xmpp/src/stanza/muc/mod.rs index 12a96dd5..d40e24b6 100644 --- a/crates/prose-xmpp/src/stanza/muc/mod.rs +++ b/crates/prose-xmpp/src/stanza/muc/mod.rs @@ -1,3 +1,8 @@ +// prose-core-client/prose-xmpp +// +// Copyright: 2023, Marc Bauer +// License: Mozilla Public License v2.0 (MPL v2.0) + pub use direct_invite::DirectInvite; pub use mediated_invite::{Continue, Invite, MediatedInvite}; pub use query::Query; diff --git a/crates/prose-xmpp/src/stanza/muc/ns.rs b/crates/prose-xmpp/src/stanza/muc/ns.rs index 60f1b455..fa03b905 100644 --- a/crates/prose-xmpp/src/stanza/muc/ns.rs +++ b/crates/prose-xmpp/src/stanza/muc/ns.rs @@ -1,3 +1,8 @@ +// prose-core-client/prose-xmpp +// +// Copyright: 2023, Marc Bauer +// License: Mozilla Public License v2.0 (MPL v2.0) + /// https://xmpp.org/extensions/xep-0045.html#registrar-formtype-owner pub mod roomconfig { /// Whether to Allow Occupants to Invite Others diff --git a/crates/prose-xmpp/src/stanza/muc/query.rs b/crates/prose-xmpp/src/stanza/muc/query.rs index 93735277..16a97aca 100644 --- a/crates/prose-xmpp/src/stanza/muc/query.rs +++ b/crates/prose-xmpp/src/stanza/muc/query.rs @@ -1,3 +1,8 @@ +// prose-core-client/prose-xmpp +// +// Copyright: 2023, Marc Bauer +// License: Mozilla Public License v2.0 (MPL v2.0) + use crate::ns; use crate::util::{ElementExt, RequestError}; use jid::{BareJid, Jid}; diff --git a/examples/prose-core-client-cli/src/main.rs b/examples/prose-core-client-cli/src/main.rs index 28418c18..e26feaef 100644 --- a/examples/prose-core-client-cli/src/main.rs +++ b/examples/prose-core-client-cli/src/main.rs @@ -527,7 +527,7 @@ impl Display for MessageEnvelope { .as_ref() .map(|id| id.clone().into_inner()) .unwrap_or("".to_string()), - self.0.from.to_string().truncate_to(20), + self.0.from.jid.to_string().truncate_to(20), self.0.body ) } @@ -891,7 +891,7 @@ async fn main() -> Result<()> { let members = room .members() .iter() - .map(|jid| jid.to_string()) + .map(|info| info.jid.to_string()) .collect::>(); println!("{}", members.join("\n")) }