diff --git a/src/error.rs b/src/error.rs index cdf686d..ca559c6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,7 +29,7 @@ pub enum Error { NickAlreadyAssigned(String), /// Raised when editing or fetching an occupant with a session that isn't associated with the /// occupant. - NonexistantSession(Session), + NonexistantSession, SessionAlreadyExists(Session), /// Raised when fetching an occupant with a nickname that isn't assigned in the room. ParticipantNotFound(String), @@ -54,7 +54,7 @@ impl fmt::Display for Error { match self { Error::MismatchJids(jid1, jid2) => write!(f, "Mismatch Jids: {jid1}, {jid2}"), Error::NickAlreadyAssigned(err) => write!(f, "Nickname already assigned: {err}"), - Error::NonexistantSession(err) => write!(f, "Session doesn't exist: {err:?}"), + Error::NonexistantSession => write!(f, "Session doesn't exist"), Error::SessionAlreadyExists(err) => write!(f, "Session already exist: {err:?}"), Error::ParticipantNotFound(err) => write!(f, "Participant not found: {err}"), Error::InvalidOriginJid => write!(f, "Invalid origin JID"), diff --git a/src/handlers/presence.rs b/src/handlers/presence.rs index df503ef..bf532f9 100644 --- a/src/handlers/presence.rs +++ b/src/handlers/presence.rs @@ -17,6 +17,7 @@ use crate::component::ComponentTrait; use crate::error::Error; use crate::presence::PresenceFull; use crate::room::Room; +use crate::session::Session; use std::collections::HashMap; use std::ops::ControlFlow; @@ -103,28 +104,46 @@ async fn handle_presence_full_available( } } else if let ControlFlow::Continue(_) = muc { // <{muc}x/> wasn't found + + let new_session = Session::try_from(presence.clone())?; + + // Is the session realjid joined? + if let Ok(joined_session) = room.find_session(&new_session) { + // Is the session participant the same as the existing session? + if joined_session.participant() == new_session.participant() { + room.update_presence(component, presence).await? + } else { + room.change_nickname(component, joined_session, new_session) + .await?; + } + } else { + // Session isn't joined. Reject + let error = Presence::new(PresenceType::Unavailable) + .with_from(participant) + .with_to(realjid) + .with_payloads(vec![MucUser { + status: vec![ + MucStatus::SelfPresence, + MucStatus::Kicked, + MucStatus::ServiceErrorKick, + ], + items: { + vec![MucItem::new(Affiliation::None, Role::None) + .with_reason("You are not in the room.")] + }, + } + .into()]); + component.send_stanza(error).await? + } + + /* match room.update_presence(component, presence.clone()).await { Ok(()) => (), Err(Error::ParticipantNotFound(_)) => { - let error = Presence::new(PresenceType::Unavailable) - .with_from(participant) - .with_to(realjid) - .with_payloads(vec![MucUser { - status: vec![ - MucStatus::SelfPresence, - MucStatus::Kicked, - MucStatus::ServiceErrorKick, - ], - items: { - vec![MucItem::new(Affiliation::None, Role::None) - .with_reason("You are not in the room.")] - }, - } - .into()]); - component.send_stanza(error).await? } err => err?, } + */ } } else { debug!("Presence received to new room: {}", &roomjid); @@ -163,7 +182,7 @@ async fn handle_presence_full_unavailable( if let Some(mut room) = rooms.remove(&roomjid) { match room.remove_session(component, presence.clone()).await { Ok(()) => (), - Err(Error::NonexistantSession(_)) => { + Err(Error::NonexistantSession) => { component.send_stanza(error).await.unwrap(); } Err(err) => Err(err).unwrap(), diff --git a/src/occupant.rs b/src/occupant.rs index 61e6be4..a476609 100644 --- a/src/occupant.rs +++ b/src/occupant.rs @@ -50,6 +50,17 @@ impl Occupant { &self.sessions[0] } + /// Fetch the session associated with the requesting realjid. + pub fn find_session(&self, queried_session: &Session) -> Result { + for session in self.sessions.iter() { + if session.real() == queried_session.real() { + return Ok(session.clone()); + } + } + + Err(Error::NonexistantSession) + } + /// Add a new session to the occupant pub fn add_session(&mut self, presence: PresenceFull) -> Result<(), Error> { let new_session = Session::try_from(presence)?; @@ -82,7 +93,7 @@ impl Occupant { if len != self.sessions.len() { Ok(()) } else { - Err(Error::NonexistantSession(own_session)) + Err(Error::NonexistantSession) } } @@ -97,7 +108,7 @@ impl Occupant { } } - Err(Error::NonexistantSession(own_session)) + Err(Error::NonexistantSession) } /// Return whether a Jid matches the occupant. If FullJid, compare with each session, otherwise @@ -200,11 +211,8 @@ mod tests { occupant.remove_session(presence_leave_louise2).unwrap(); match occupant.update_presence(presence2) { - Err(Error::NonexistantSession(session)) if session.real() == &*LOUISE_FULL2 => (), - err => panic!( - "Should return Error::NonexistantSession(Session {{ {:?} }}), returned: {:?}", - *LOUISE_FULL2, err, - ), + Err(Error::NonexistantSession) => (), + err => panic!("Should return Error::NonexistantSession, returned: {err:?}",), } } } diff --git a/src/room.rs b/src/room.rs index 8c8ce3a..e8136db 100644 --- a/src/room.rs +++ b/src/room.rs @@ -32,7 +32,7 @@ use xmpp_parsers::{ MucUser, }, presence::{Presence, Type as PresenceType}, - BareJid, Jid, + BareJid, Element, Jid, }; #[derive(Debug, PartialEq, Eq)] @@ -331,7 +331,163 @@ impl Room { session.presence, BroadcastPresence::Update, ) - .await?; + .await + } + + /// Change participant nickname. + /// When reaching here, it has already been established that the requesting session comes from + /// somebody who is joined. The passed session has the newnick in its participant JID. + pub async fn change_nickname( + &mut self, + component: &mut C, + joined_session: Session, + new_session: Session, + ) -> Result<(), Error> { + let oldnick = &joined_session.participant().resource; + let newnick = &new_session.participant().resource; + let bare = BareJid::from(new_session.real().clone()); + + for (nick, occupant) in self.occupants.iter_mut() { + if nick == newnick && occupant.real != bare { + return Err(Error::NickAlreadyAssigned(newnick.clone())); + } + } + + + // Send unavailable for current session to everyone + + let presence_leave = Presence::new(PresenceType::Unavailable) + .with_from(Jid::Full(joined_session.participant().clone())); + + let presence_leave_to_others = presence_leave.clone().with_payloads(vec![MucUser { + status: vec![MucStatus::NewNick], + items: vec![MucItem::new(Affiliation::Owner, Role::None).with_nick(newnick.clone())], + } + .into()]); + + for (nick, occupant) in self.occupants.iter() { + for session in occupant.iter() { + // Self occupant + if nick == oldnick { + let mucuser = MucUser { + status: vec![MucStatus::NewNick], + items: vec![MucItem::new(Affiliation::Owner, Role::None) + .with_nick(newnick.clone()) + .with_jid(session.real().clone())], + }; + + // Self session + if session.real() == joined_session.real() { + let mucuser = MucUser { + status: vec![MucStatus::SelfPresence, MucStatus::NewNick], + items: vec![MucItem::new(Affiliation::Owner, Role::None) + .with_nick(newnick.clone()) + .with_jid(joined_session.real().clone())], + }; + + let presence: Element = presence_leave + .clone() + .with_to(Jid::Full(session.real().clone())) + .with_payloads(vec![mucuser.into()]) + .into(); + component.send_stanza(presence).await?; + } else { + // Self occupant, other sessions + let presence: Element = presence_leave + .clone() + .with_to(Jid::Full(session.real().clone())) + .with_payloads(vec![mucuser.into()]) + .into(); + component.send_stanza(presence).await?; + } + } else { + // Other occupants' sessions + component + .send_stanza( + presence_leave_to_others + .clone() + .with_to(Jid::Full(session.real().clone())), + ) + .await?; + } + } + } + + // Remove old session + match self.get_mut_occupant(&joined_session) { + Ok(occupant) => { + occupant.remove_session(joined_session.presence.clone())?; + if occupant.iter().len() == 0 { + let _ = self.occupants.remove(oldnick); + } + } + _ => unreachable!(), + } + + // Nickname isn't present already in the room. Create new occupant. + let occupant = Occupant::new(new_session.presence.clone())?; + self.occupants.insert(newnick.to_string(), occupant.clone()); + + // Send available for new session to everyone + + let presence_join = Presence::new(PresenceType::None) + .with_from(Jid::Full(new_session.participant().clone())); + + let presence_join_to_others = presence_join.clone().with_payloads(vec![MucUser { + status: vec![], + items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)], + } + .into()]); + + for (nick, occupant) in self.occupants.iter() { + for session in occupant.iter() { + // Self occupant + if nick == newnick { + let mucuser = MucUser { + status: vec![], + items: vec![MucItem::new(Affiliation::Owner, Role::Moderator) + .with_jid(new_session.real().clone())], + }; + + if session.real() == new_session.real() { + // Self session + let mucuser = MucUser { + status: vec![MucStatus::SelfPresence], + items: vec![MucItem::new(Affiliation::Owner, Role::Moderator) + .with_jid(new_session.real().clone())], + }; + component + .send_stanza( + presence_join + .clone() + .with_to(Jid::Full(session.real().clone())) + .with_payloads(vec![mucuser.clone().into()]), + ) + .await?; + } else { + // Self occupant, other sessions + component + .send_stanza( + presence_join + .clone() + .with_to(Jid::Full(session.real().clone())) + .with_payloads(vec![mucuser.clone().into()]), + ) + .await?; + } + } else { + // Other occupants' sessions + component + .send_stanza( + presence_join_to_others + .clone() + .with_to(Jid::Full(session.real().clone())), + ) + .await?; + } + } + } + Ok(()) } @@ -364,30 +520,45 @@ impl Room { Ok(()) } + /// Fetch the session associated with the requesting realjid. + pub fn find_session(&self, requested_session: &Session) -> Result { + // Go through all occupants, find if the session exists. If so return this occupant + for (_nick, occupant) in self.occupants.iter() { + #[allow(clippy::redundant_pattern_matching)] + if let Ok(joined_session) = occupant.find_session(requested_session) { + return Ok(joined_session.clone()); + } + } + + Err(Error::NonexistantSession) + } + /// Fetch the occupant associated with the provided nick and ensure the session is part of /// it. + /// . + /// Fetch the occupant associated with the requesting realjid. pub fn get_occupant(&self, session: &Session) -> Result<&Occupant, Error> { - if let Some(occupant) = self.occupants.get(&session.participant().resource) { - if occupant.contains(session.real()) { - Ok(occupant) - } else { - Err(Error::NonexistantSession(session.clone())) + // Go through all occupants, find if the session exists. If so return this occupant + for (_nick, occupant) in self.occupants.iter() { + #[allow(clippy::redundant_pattern_matching)] + if let Ok(_) = occupant.find_session(session) { + return Ok(occupant); } - } else { - Err(Error::ParticipantNotFound( - session.participant().resource.clone(), - )) } + + Err(Error::NonexistantSession) } /// Fetch a mutable reference of the occupant associated with the provided nick and ensure the /// session is part of it. + /// . + /// Fetch a mutable reference to the occupant associated with the requesting realjid. pub fn get_mut_occupant(&mut self, session: &Session) -> Result<&mut Occupant, Error> { if let Some(occupant) = self.occupants.get_mut(&session.participant().resource) { if occupant.contains(session.real()) { Ok(occupant) } else { - Err(Error::NonexistantSession(session.clone())) + Err(Error::NonexistantSession) } } else { Err(Error::ParticipantNotFound( @@ -406,8 +577,9 @@ mod tests { use super::*; use crate::component::TestComponent; use crate::tests::templates::{ - LOUISE_FULL1, LOUISE_NICK, LOUISE_ROOM1_PART, ROOM1_BARE, ROSA_FULL1, ROSA_NICK, - ROSA_ROOM1_PART, SUGAKO_FULL1, SUGAKO_NICK, SUGAKO_ROOM1_PART, + new_room, LOUISE_BARE, LOUISE_FULL1, LOUISE_FULL2, LOUISE_NICK, LOUISE_ROOM1_PART, + LOUISE_ROOM1_PART2, ROOM1_BARE, ROSA_FULL1, ROSA_NICK, ROSA_ROOM1_PART, SUGAKO_FULL1, + SUGAKO_NICK, SUGAKO_ROOM1_PART, }; use std::str::FromStr; use xmpp_parsers::{ @@ -810,4 +982,44 @@ mod tests { .await .unwrap(); } + + #[tokio::test] + async fn get_occupant() { + let room = new_room( + ROOM1_BARE.clone(), + vec![ + (LOUISE_NICK, vec![LOUISE_FULL1.clone()]), + ("newnick", vec![LOUISE_FULL2.clone()]), + ], + ) + .await; + + let result = room.occupants.get("newnick").unwrap(); + + let presence1 = PresenceFull::try_from( + Presence::new(PresenceType::None) + .with_from(Jid::Full(LOUISE_FULL2.clone())) + .with_to(Jid::Full(LOUISE_ROOM1_PART2.clone())), + ) + .unwrap(); + let session1 = Session::try_from(presence1).unwrap(); + match room.get_occupant(&session1) { + Ok(occupant) => assert_eq!(occupant, result), + other => panic!("Error: {other:?}"), + } + + // Sessions not joined to the room, even from the same barejid, should be rejected + let louise_full3 = LOUISE_BARE.clone().with_resource("othernick"); + let presence2 = PresenceFull::try_from( + Presence::new(PresenceType::None) + .with_from(Jid::Full(louise_full3)) + .with_to(Jid::Full(LOUISE_ROOM1_PART2.clone())), + ) + .unwrap(); + let session2 = Session::try_from(presence2).unwrap(); + match room.get_occupant(&session2) { + Err(Error::NonexistantSession) => (), + other => panic!("Error: {other:?}"), + } + } } diff --git a/src/tests/presence.rs b/src/tests/presence.rs index 6ea5bcb..6072f42 100644 --- a/src/tests/presence.rs +++ b/src/tests/presence.rs @@ -17,9 +17,9 @@ use crate::component::TestComponent; use crate::handlers::handle_stanza; use crate::room::Room; use crate::tests::templates::{ - new_room, two_participant_room, LOUISE_FULL1, LOUISE_NICK, LOUISE_ROOM1_PART, PETER_FULL1, - PETER_NICK, PETER_ROOM1_PART, ROOM1_BARE, ROSA_FULL1, ROSA_ROOM1_PART, SUGAKO_FULL1, - SUGAKO_NICK, SUGAKO_ROOM1_PART, + new_room, two_participant_room, LOUISE_FULL1, LOUISE_NICK, LOUISE_NICK2, LOUISE_ROOM1_PART, + LOUISE_ROOM1_PART2, PETER_FULL1, PETER_NICK, PETER_ROOM1_PART, ROOM1_BARE, ROSA_FULL1, + ROSA_ROOM1_PART, SUGAKO_FULL1, SUGAKO_NICK, SUGAKO_ROOM1_PART, }; use std::collections::{BTreeMap, HashMap}; @@ -562,3 +562,79 @@ async fn update_not_joined() { handle_stanza(&mut component, &mut rooms).await.unwrap(); } + +#[tokio::test] +async fn nickname_change() { + let mut rooms: HashMap = HashMap::new(); + rooms.insert( + ROOM1_BARE.clone(), + new_room( + ROOM1_BARE.clone(), + vec![ + (LOUISE_NICK, vec![LOUISE_FULL1.clone()]), + (SUGAKO_NICK, vec![SUGAKO_FULL1.clone()]), + ], + ) + .await, + ); + + let update: Element = Presence::new(PresenceType::None) + .with_from(Jid::Full(LOUISE_FULL1.clone())) + .with_to(Jid::Full(LOUISE_ROOM1_PART2.clone())) + .into(); + + let mut component = TestComponent::new(vec![update]); + + // Unavailable to self + component.expect( + Presence::new(PresenceType::Unavailable) + .with_from(Jid::Full(LOUISE_ROOM1_PART.clone())) + .with_to(Jid::Full(LOUISE_FULL1.clone())) + .with_payloads(vec![MucUser { + status: vec![MucStatus::SelfPresence, MucStatus::NewNick], + items: vec![MucItem::new(Affiliation::Owner, Role::None) + .with_nick(LOUISE_NICK2) + .with_jid(LOUISE_FULL1.clone())], + } + .into()]), + ); + + // Available to self + component.expect( + Presence::new(PresenceType::None) + .with_from(Jid::Full(LOUISE_ROOM1_PART2.clone())) + .with_to(Jid::Full(LOUISE_FULL1.clone())) + .with_payloads(vec![MucUser { + status: vec![MucStatus::SelfPresence], + items: vec![MucItem::new(Affiliation::Owner, Role::Moderator) + .with_jid(LOUISE_FULL1.clone())], + } + .into()]), + ); + + // Unavailable to Sugako + component.expect( + Presence::new(PresenceType::Unavailable) + .with_from(Jid::Full(LOUISE_ROOM1_PART.clone())) + .with_to(Jid::Full(SUGAKO_FULL1.clone())) + .with_payloads(vec![MucUser { + status: vec![MucStatus::NewNick], + items: vec![MucItem::new(Affiliation::Owner, Role::None).with_nick(LOUISE_NICK2)], + } + .into()]), + ); + + // Available to Sugako + component.expect( + Presence::new(PresenceType::None) + .with_from(Jid::Full(LOUISE_ROOM1_PART2.clone())) + .with_to(Jid::Full(SUGAKO_FULL1.clone())) + .with_payloads(vec![MucUser { + status: vec![], + items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)], + } + .into()]), + ); + + handle_stanza(&mut component, &mut rooms).await.unwrap(); +} diff --git a/src/tests/templates.rs b/src/tests/templates.rs index 8dcab8d..1b99cbd 100644 --- a/src/tests/templates.rs +++ b/src/tests/templates.rs @@ -37,8 +37,11 @@ pub const LOUISE_FULL1: LazyLock = pub const LOUISE_FULL2: LazyLock = LazyLock::new(|| LOUISE_BARE.clone().with_resource("desktop")); pub const LOUISE_NICK: &'static str = "louise"; +pub const LOUISE_NICK2: &'static str = "louise_"; pub const LOUISE_ROOM1_PART: LazyLock = LazyLock::new(|| ROOM1_BARE.clone().with_resource(LOUISE_NICK)); +pub const LOUISE_ROOM1_PART2: LazyLock = + LazyLock::new(|| ROOM1_BARE.clone().with_resource(LOUISE_NICK2)); /// https://en.wikipedia.org/wiki/Kanno_Sugako pub const SUGAKO_BARE: LazyLock =