diff --git a/src/error.rs b/src/error.rs index 990df26..f8ebe6e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -17,12 +17,13 @@ use std::error::Error as StdError; use std::fmt; use tokio_xmpp::Error as TokioXMPPError; -use xmpp_parsers::Jid; +use xmpp_parsers::{FullJid, Jid}; #[derive(Debug)] pub enum Error { MismatchJids(Jid), NickAlreadyAssigned(String), + NonexistantSession(FullJid), XMPPError(TokioXMPPError), } @@ -33,6 +34,7 @@ impl fmt::Display for Error { match self { Error::MismatchJids(err) => write!(f, "Mismatch Jids: {}", err), Error::NickAlreadyAssigned(err) => write!(f, "Nickname already assigned: {}", err), + Error::NonexistantSession(err) => write!(f, "Session doesn't exist: {}", err), Error::XMPPError(err) => write!(f, "XMPP error: {}", err), } } diff --git a/src/handlers.rs b/src/handlers.rs index d7b07fa..ab87bce 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -106,51 +106,87 @@ async fn handle_presence( presence: Presence, rooms: &mut HashMap, ) -> Result<(), Error> { - let muc = presence - .payloads - .into_iter() - .try_for_each(|payload| match Muc::try_from(payload) { - Ok(muc) => ControlFlow::Break(muc), - _ => ControlFlow::Continue(()), - }); + if presence.type_ == PresenceType::None { + let muc = presence + .payloads + .into_iter() + .try_for_each(|payload| match Muc::try_from(payload) { + Ok(muc) => ControlFlow::Break(muc), + _ => ControlFlow::Continue(()), + }); - if let ControlFlow::Continue(_) = muc { - return Ok(()); - } + // TODO: Handle presence probes, remove the return here. + // TODO: check for features in the Muc element. + if let ControlFlow::Continue(_) = muc { + return Ok(()); + } - // Presences to MUC come from resources not accounts - if let Jid::Full(realjid) = presence.from.unwrap() && - let Jid::Full(participant) = presence.to.unwrap() { + // Presences to MUC come from resources not accounts + if let Jid::Full(realjid) = presence.from.clone().unwrap() && + let Jid::Full(participant) = presence.to.clone().unwrap() { - let roomjid = BareJid::from(participant.clone()); - let nick: Nick = participant.resource.clone(); + let roomjid = BareJid::from(participant.clone()); + let nick: Nick = participant.resource.clone(); - // Room already exists - if let Some(room) = rooms.get_mut(&roomjid) { - debug!("Presence received to existing room: {}", &roomjid); - match room.add_session(component, realjid.clone(), nick).await { - Ok(_) => (), - Err(Error::NickAlreadyAssigned(nick)) => { - let error = Presence::new(PresenceType::Error) - .with_from(participant) - .with_to(realjid) - .with_payloads(vec![ - StanzaError::new( - ErrorType::Cancel, - DefinedCondition::Conflict, - "en", - format!("Nickname conflict: {}", nick), - ).into() - ]); - component.send_stanza(error).await?; - }, - err => err.unwrap(), + // Room already exists + if let Some(room) = rooms.get_mut(&roomjid) { + debug!("Presence received to existing room: {}", &roomjid); + match room.add_session(component, realjid.clone(), nick).await { + Ok(_) => (), + Err(Error::NickAlreadyAssigned(nick)) => { + let error = Presence::new(PresenceType::Error) + .with_from(participant) + .with_to(realjid) + .with_payloads(vec![ + StanzaError::new( + ErrorType::Cancel, + DefinedCondition::Conflict, + "en", + format!("Nickname conflict: {}", nick), + ).into() + ]); + component.send_stanza(error).await?; + }, + err => err.unwrap(), + } + } else { + debug!("Presence received to new room: {}", &roomjid); + let mut room = Room::new(roomjid.clone()); + room.add_session(component, realjid, nick).await.unwrap(); + let _ = rooms.insert(roomjid, room); + } + } + } else if presence.type_ == PresenceType::Unavailable { + if let Jid::Full(realjid) = presence.from.unwrap() && + let Jid::Full(participant) = presence.to.unwrap() { + + let roomjid = BareJid::from(participant.clone()); + + let error = Presence::new(PresenceType::Error) + .with_from(participant.clone()) + .with_to(realjid.clone()) + .with_payloads(vec![ + StanzaError::new( + ErrorType::Cancel, + DefinedCondition::ServiceUnavailable, + "en", + "Occupant does not exist", + ).into() + ]); + if let Some(mut room) = rooms.remove(&roomjid) { + match room.remove_session(component, realjid).await { + Ok(()) => (), + Err(Error::NonexistantSession(_)) => { + component.send_stanza(error).await.unwrap(); + }, + Err(err) => Err(err).unwrap(), + } + + // Add the room back + if room.occupants.len() > 0 { + rooms.insert(roomjid, room); + } } - } else { - debug!("Presence received to new room: {}", &roomjid); - let mut room = Room::new(roomjid.clone()); - room.add_session(component, realjid, nick).await.unwrap(); - let _ = rooms.insert(roomjid, room); } } diff --git a/src/room.rs b/src/room.rs index 8d3f07c..1c4aec0 100644 --- a/src/room.rs +++ b/src/room.rs @@ -134,6 +134,51 @@ impl Room { Ok(()) } + + pub async fn remove_session( + &mut self, + component: &mut C, + realjid: FullJid, + ) -> Result<(), Error> { + let bare = BareJid::from(realjid.clone()); + + // If occupant doesn't exist, ignore. + if let Some(mut occupant) = self.occupants.remove(&bare) { + let self_presence = Presence::new(PresenceType::Unavailable) + .with_from(occupant.participant.clone()) + .with_to(realjid.clone()) + .with_payloads(vec![MucUser { + status: vec![MucStatus::SelfPresence], + items: vec![MucItem::new(Affiliation::Owner, Role::None)], + } + .into()]); + + component.send_stanza(self_presence).await?; + occupant.remove_session(realjid)?; + + let presence = Presence::new(PresenceType::Unavailable) + .with_from(occupant.participant.clone()) + .with_payloads(vec![MucUser { + status: Vec::new(), + items: vec![MucItem::new(Affiliation::Owner, Role::None)], + } + .into()]); + + // Add remaining occupant sessions in the occupant pool + if occupant.sessions.len() > 0 { + self.occupants.insert(bare, occupant); + } + + for (_, occupant) in self.occupants.iter() { + for session in occupant.iter() { + let presence = presence.clone().with_to(Jid::Full(session.clone())); + component.send_stanza(presence).await?; + } + } + } + + Ok(()) + } } #[derive(Debug, Clone)] @@ -162,6 +207,22 @@ impl Occupant { Ok(()) } + + pub fn remove_session(&mut self, real: FullJid) -> Result<(), Error> { + if BareJid::from(real.clone()) != self.real { + return Err(Error::MismatchJids(Jid::from(real.clone()))); + } + + let len = self.sessions.len(); + self.sessions.retain(|session| session != &real); + + // An item has been removed + if len != self.sessions.len() { + Ok(()) + } else { + Err(Error::NonexistantSession(real)) + } + } } impl IntoIterator for Occupant { diff --git a/src/tests.rs b/src/tests.rs index f861140..dc518c7 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -288,3 +288,131 @@ async fn test_join_presence_existing_room() { None => panic!(), } } + +#[tokio::test] +async fn test_leave_non_existing_room() { + let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap(); + let roomjid = COMPONENT_JID.clone().with_node("room"); + let participant1 = roomjid.clone().with_resource("nick1"); + + let leave1: Element = Presence::new(PresenceType::Unavailable) + .with_from(Jid::Full(realjid1.clone())) + .with_to(Jid::Full(participant1.clone())) + .into(); + + // Non-existing room + let mut component = TestComponent::new(vec![leave1]); + let mut rooms: HashMap = HashMap::new(); + handle_stanza(&mut component, &mut rooms).await.unwrap(); + // The leave should be ignored, there should be no output at all. + component.assert(); +} + +#[tokio::test] +async fn test_leave_last_participant() { + let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap(); + let roomjid = COMPONENT_JID.clone().with_node("room"); + let participant1 = roomjid.clone().with_resource("nick1"); + + let join1: Element = Presence::new(PresenceType::None) + .with_from(Jid::Full(realjid1.clone())) + .with_to(Jid::Full(participant1.clone())) + .with_payloads(vec![Muc::new().into()]) + .into(); + + let leave1: Element = Presence::new(PresenceType::Unavailable) + .with_from(Jid::Full(realjid1.clone())) + .with_to(Jid::Full(participant1.clone())) + .into(); + + // Last occupant + let mut component = TestComponent::new(vec![join1, leave1]); + let mut rooms: HashMap = HashMap::new(); + + // Ignore self-presence for participant1 + component.expect_with(|_| ()); + // Ignore subject message for participant1 + component.expect_with(|_| ()); + + component.expect(Presence::new(PresenceType::Unavailable) + .with_from(Jid::Full(participant1.clone())) + .with_to(Jid::Full(realjid1.clone())) + .with_payloads(vec![MucUser { + status: vec![MucStatus::SelfPresence], + items: vec![MucItem::new(Affiliation::Owner, Role::None)], + }.into()]) + ); + + handle_stanza(&mut component, &mut rooms).await.unwrap(); + + component.assert(); + assert_eq!(rooms.len(), 0); +} + +#[tokio::test] +async fn test_leave_room_not_last() { + let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap(); + let realjid2 = FullJid::from_str("qxx@bar/foo").unwrap(); + let roomjid = COMPONENT_JID.clone().with_node("room"); + let participant1 = roomjid.clone().with_resource("nick1"); + let participant2 = roomjid.clone().with_resource("nick2"); + + let join1: Element = Presence::new(PresenceType::None) + .with_from(Jid::Full(realjid1.clone())) + .with_to(Jid::Full(participant1.clone())) + .with_payloads(vec![Muc::new().into()]) + .into(); + + let join2: Element = Presence::new(PresenceType::None) + .with_from(Jid::Full(realjid2.clone())) + .with_to(Jid::Full(participant2.clone())) + .with_payloads(vec![Muc::new().into()]) + .into(); + + let leave2: Element = Presence::new(PresenceType::Unavailable) + .with_from(Jid::Full(realjid2.clone())) + .with_to(Jid::Full(participant2.clone())) + .into(); + + + // Room still has occupants after leave + let mut component = TestComponent::new(vec![join1, join2, leave2]); + let mut rooms: HashMap = HashMap::new(); + + // Ignore self-presence for participant1 + component.expect_with(|_| ()); + // Ignore subject message for participant1 + component.expect_with(|_| ()); + // Ignore participant1 occupant presence for participant2 + component.expect_with(|_| ()); + // Ignore self-presence for participant2 + component.expect_with(|_| ()); + // Ignore subject message for participant2 + component.expect_with(|_| ()); + + component.expect(Presence::new(PresenceType::Unavailable) + .with_from(Jid::Full(participant2.clone())) + .with_to(Jid::Full(realjid2.clone())) + .with_payloads(vec![MucUser { + status: vec![MucStatus::SelfPresence], + items: vec![MucItem::new(Affiliation::Owner, Role::None)], + }.into()]) + ); + + component.expect(Presence::new(PresenceType::Unavailable) + .with_from(Jid::Full(participant2.clone())) + .with_to(Jid::Full(realjid1.clone())) + .with_payloads(vec![MucUser { + status: Vec::new(), + items: vec![MucItem::new(Affiliation::Owner, Role::None)], + }.into()]) + ); + + handle_stanza(&mut component, &mut rooms).await.unwrap(); + component.assert(); + assert_eq!(rooms.len(), 1); + match rooms.get(&roomjid) { + Some(room) => assert_eq!(room.occupants.len(), 1), + None => panic!(), + } +}