diff --git a/src/error.rs b/src/error.rs index 0122a28..ba4ca76 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,7 +13,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use crate::room::Session; +use crate::session::Session; use std::error::Error as StdError; use std::fmt; diff --git a/src/main.rs b/src/main.rs index 2b4881c..ce35657 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,9 @@ mod component; mod error; mod handlers; +mod occupant; mod room; +mod session; #[cfg(test)] mod tests; diff --git a/src/occupant.rs b/src/occupant.rs new file mode 100644 index 0000000..e2c4dc0 --- /dev/null +++ b/src/occupant.rs @@ -0,0 +1,196 @@ +// Copyright (C) 2022-2099 The crate authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +// for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::error::Error; +use crate::session::{Nick, Session}; + +use std::iter::IntoIterator; + +use xmpp_parsers::{presence::Presence, BareJid, FullJid, Jid}; + +/// An occupant in a room. May contain multiple sessions (Multi-Session Nicks) +#[derive(Debug, Clone, PartialEq)] +pub struct Occupant { + /// Public Jid for the Occupant + pub real: BareJid, + pub participant: FullJid, + pub nick: Nick, + pub sessions: Vec, +} + +impl Occupant { + /// New occupant + pub fn new(presence: Presence) -> Result { + let session = Session::try_from(presence)?; + Ok(Occupant { + real: BareJid::from(session.real.clone()), + participant: session.participant.clone(), + nick: session.participant.resource.clone(), + sessions: vec![session], + }) + } + + /// Add a new session to the occupant + pub fn add_session(&mut self, presence: Presence) -> Result<(), Error> { + let new_session = Session::try_from(presence)?; + if BareJid::from(new_session.real.clone()) != self.real { + return Err(Error::MismatchJids( + Jid::from(self.real.clone()), + Jid::from(new_session.real), + )); + } + + for session in &self.sessions { + if &new_session.real == &session.real { + return Err(Error::SessionAlreadyExists(new_session)); + } + } + + self.sessions.push(new_session); + Ok(()) + } + + /// Remove a session from the occupant + pub fn remove_session(&mut self, presence: Presence) -> Result<(), Error> { + let own_session = Session::try_from(presence)?; + + let len = self.sessions.len(); + self.sessions + .retain(|session| session.real != own_session.real); + + // An item has been removed + if len != self.sessions.len() { + Ok(()) + } else { + Err(Error::NonexistantSession(own_session)) + } + } + + /// Update session presence + pub fn update_presence(&mut self, presence: Presence) -> Result<(), Error> { + let own_session = Session::with_nick(presence, self.participant.resource.clone())?; + + for (i, session) in self.sessions.iter().enumerate() { + if &own_session == session { + if i == 0 { + // Leader session + self.sessions[0] = own_session; + return Ok(()); + } + return Err(Error::SecondarySession(own_session)); + } + } + + Err(Error::NonexistantSession(own_session)) + } + + /// Return whether a Jid matches the occupant. If FullJid, compare with each session, otherwise + /// see if the BareJid matches. + // TODO: We may want to split checking for Bare/Full as this can be error prone + pub fn contains>(&self, _jid: &J) -> bool { + true + } +} + +impl IntoIterator for Occupant { + type Item = Session; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.sessions.into_iter() + } +} + +impl Occupant { + pub fn iter(&self) -> std::slice::Iter<'_, Session> { + self.sessions.iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::templates::{ + LOUISE_FULL1, LOUISE_FULL2, LOUISE_NICK, LOUISE_ROOM1_PART, ROOM1_BARE, + }; + use xmpp_parsers::{ + muc::{ + user::{Affiliation, Item as MucItem, Role}, + Muc, MucUser, + }, + presence::{Presence, Show as PresenceShow, Type as PresenceType}, + }; + + #[tokio::test] + async fn test_occupant_update_presence() { + let presence_louise1 = Presence::new(PresenceType::None) + .with_from(LOUISE_FULL1.clone()) + .with_to(LOUISE_ROOM1_PART.clone()); + let presence_louise2 = Presence::new(PresenceType::None) + .with_from(LOUISE_FULL2.clone()) + .with_to(LOUISE_ROOM1_PART.clone()); + + let mut occupant = Occupant::new(presence_louise1).unwrap(); + occupant.add_session(presence_louise2).unwrap(); + + let presence1 = Presence::new(PresenceType::None) + .with_from(LOUISE_FULL1.clone()) + .with_to(ROOM1_BARE.clone()) + .with_show(PresenceShow::Away) + .with_payloads(vec![ + Muc::new().into(), + MucUser { + status: Vec::new(), + items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)], + } + .into(), + ]); + match occupant.update_presence(presence1.clone()) { + Ok(()) => (), + err => panic!("Err: {:?}", err), + } + assert_eq!(occupant.sessions[0].presence, presence1); + + let presence2 = Presence::new(PresenceType::None) + .with_from(LOUISE_FULL2.clone()) + .with_to(ROOM1_BARE.clone()) + .with_show(PresenceShow::Xa); + + match occupant.update_presence(presence2.clone()) { + Err(Error::SecondarySession(session)) if session.real == *LOUISE_FULL2 => (), + err => panic!( + "Should return Error::SecondarySession(Session {{ {:?} }}), returned: {:?}", + *LOUISE_FULL2, err, + ), + } + assert_eq!( + occupant.sessions[1], + Session::with_nick(presence2.clone(), LOUISE_NICK.clone()).unwrap() + ); + + let presence_leave_louise2 = Presence::new(PresenceType::Unavailable) + .with_from(LOUISE_FULL2.clone()) + .with_to(LOUISE_ROOM1_PART.clone()); + 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::SecondarySession(Session {{ {:?} }}), returned: {:?}", + *LOUISE_FULL2, err, + ), + } + } +} diff --git a/src/room.rs b/src/room.rs index ccf098b..0d6b870 100644 --- a/src/room.rs +++ b/src/room.rs @@ -15,9 +15,10 @@ use crate::component::ComponentTrait; use crate::error::Error; +use crate::occupant::Occupant; +use crate::session::{Nick, Session}; use std::collections::BTreeMap; -use std::iter::IntoIterator; use chrono::{FixedOffset, Utc}; use log::debug; @@ -30,11 +31,9 @@ use xmpp_parsers::{ MucUser, }, presence::{Presence, Type as PresenceType}, - BareJid, FullJid, Jid, + BareJid, Jid, }; -pub type Nick = String; - #[derive(Debug, PartialEq, Eq)] pub enum BroadcastPresence { /// Resource joined the room. It needs to know about all other participants, and other @@ -383,182 +382,22 @@ impl Room { } } -/// An occupant session -#[derive(Debug, Clone)] -pub struct Session { - pub presence: Presence, - pub real: FullJid, - pub participant: FullJid, -} - -impl Session { - /// Ensure presence doesn't contain payloads that would impersonate us - fn filter_presence(presence: Presence) -> Presence { - presence - } - - /// Instanciate a Session from a presence which is adressed to the room JID. - fn with_nick>(presence: Presence, nick: N) -> Result { - let presence = Session::filter_presence(presence); - Ok(Session { - real: presence - .from - .clone() - .map(FullJid::try_from) - .ok_or(Error::MissingJid)??, - participant: presence - .to - .clone() - .map(BareJid::from) - .ok_or(Error::MissingJid)? - .with_resource(nick.into()), - presence, - }) - } -} - -impl PartialEq for Session { - fn eq(&self, other: &Session) -> bool { - self.real == other.real && self.participant == other.participant - } -} - -impl TryFrom for Session { - type Error = Error; - - fn try_from(presence: Presence) -> Result { - let presence = Session::filter_presence(presence); - Ok(Session { - real: presence - .from - .clone() - .map(FullJid::try_from) - .ok_or(Error::MissingJid)??, - participant: presence - .to - .clone() - .map(FullJid::try_from) - .ok_or(Error::MissingJid)??, - presence, - }) - } -} - -/// An occupant in a room. May contain multiple sessions (Multi-Session Nicks) -#[derive(Debug, Clone, PartialEq)] -pub struct Occupant { - /// Public Jid for the Occupant - pub real: BareJid, - pub participant: FullJid, - pub nick: Nick, - pub sessions: Vec, -} - -impl Occupant { - /// New occupant - pub fn new(presence: Presence) -> Result { - let session = Session::try_from(presence)?; - Ok(Occupant { - real: BareJid::from(session.real.clone()), - participant: session.participant.clone(), - nick: session.participant.resource.clone(), - sessions: vec![session], - }) - } - - /// Add a new session to the occupant - pub fn add_session(&mut self, presence: Presence) -> Result<(), Error> { - let new_session = Session::try_from(presence)?; - if BareJid::from(new_session.real.clone()) != self.real { - return Err(Error::MismatchJids( - Jid::from(self.real.clone()), - Jid::from(new_session.real), - )); - } - - for session in &self.sessions { - if &new_session.real == &session.real { - return Err(Error::SessionAlreadyExists(new_session)); - } - } - - self.sessions.push(new_session); - Ok(()) - } - - /// Remove a session from the occupant - pub fn remove_session(&mut self, presence: Presence) -> Result<(), Error> { - let own_session = Session::try_from(presence)?; - - let len = self.sessions.len(); - self.sessions - .retain(|session| session.real != own_session.real); - - // An item has been removed - if len != self.sessions.len() { - Ok(()) - } else { - Err(Error::NonexistantSession(own_session)) - } - } - - /// Update session presence - pub fn update_presence(&mut self, presence: Presence) -> Result<(), Error> { - let own_session = Session::with_nick(presence, self.participant.resource.clone())?; - - for (i, session) in self.sessions.iter().enumerate() { - if &own_session == session { - if i == 0 { - // Leader session - self.sessions[0] = own_session; - return Ok(()); - } - return Err(Error::SecondarySession(own_session)); - } - } - - Err(Error::NonexistantSession(own_session)) - } - - /// Return whether a Jid matches the occupant. If FullJid, compare with each session, otherwise - /// see if the BareJid matches. - // TODO: We may want to split checking for Bare/Full as this can be error prone - pub fn contains>(&self, _jid: &J) -> bool { - true - } -} - -impl IntoIterator for Occupant { - type Item = Session; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.sessions.into_iter() - } -} - -impl Occupant { - fn iter(&self) -> std::slice::Iter<'_, Session> { - self.sessions.iter() - } -} - #[cfg(test)] mod tests { use super::*; use crate::component::TestComponent; use crate::tests::templates::{ - LOUISE_FULL1, LOUISE_FULL2, LOUISE_NICK, LOUISE_ROOM1_PART, ROOM1_BARE, ROSA_FULL1, - ROSA_NICK, ROSA_ROOM1_PART, SUGAKO_FULL1, SUGAKO_NICK, SUGAKO_ROOM1_PART, + LOUISE_FULL1, LOUISE_NICK, LOUISE_ROOM1_PART, ROOM1_BARE, ROSA_FULL1, ROSA_NICK, + ROSA_ROOM1_PART, SUGAKO_FULL1, SUGAKO_NICK, SUGAKO_ROOM1_PART, }; use std::str::FromStr; use xmpp_parsers::{ muc::{ user::{Affiliation, Item as MucItem, Role, Status as MucStatus}, - Muc, MucUser, + MucUser, }, - presence::{Presence, Show as PresenceShow, Type as PresenceType}, - BareJid, Element, + presence::{Presence, Type as PresenceType}, + BareJid, Element, FullJid, }; #[tokio::test] @@ -915,65 +754,4 @@ mod tests { .await .unwrap(); } - - #[tokio::test] - async fn test_occupant_update_presence() { - let presence_louise1 = Presence::new(PresenceType::None) - .with_from(LOUISE_FULL1.clone()) - .with_to(LOUISE_ROOM1_PART.clone()); - let presence_louise2 = Presence::new(PresenceType::None) - .with_from(LOUISE_FULL2.clone()) - .with_to(LOUISE_ROOM1_PART.clone()); - - let mut occupant = Occupant::new(presence_louise1).unwrap(); - occupant.add_session(presence_louise2).unwrap(); - - let presence1 = Presence::new(PresenceType::None) - .with_from(LOUISE_FULL1.clone()) - .with_to(ROOM1_BARE.clone()) - .with_show(PresenceShow::Away) - .with_payloads(vec![ - Muc::new().into(), - MucUser { - status: Vec::new(), - items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)], - } - .into(), - ]); - match occupant.update_presence(presence1.clone()) { - Ok(()) => (), - err => panic!("Err: {:?}", err), - } - assert_eq!(occupant.sessions[0].presence, presence1); - - let presence2 = Presence::new(PresenceType::None) - .with_from(LOUISE_FULL2.clone()) - .with_to(ROOM1_BARE.clone()) - .with_show(PresenceShow::Xa); - - match occupant.update_presence(presence2.clone()) { - Err(Error::SecondarySession(session)) if session.real == *LOUISE_FULL2 => (), - err => panic!( - "Should return Error::SecondarySession(Session {{ {:?} }}), returned: {:?}", - *LOUISE_FULL2, err, - ), - } - assert_eq!( - occupant.sessions[1], - Session::with_nick(presence2.clone(), LOUISE_NICK.clone()).unwrap() - ); - - let presence_leave_louise2 = Presence::new(PresenceType::Unavailable) - .with_from(LOUISE_FULL2.clone()) - .with_to(LOUISE_ROOM1_PART.clone()); - 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::SecondarySession(Session {{ {:?} }}), returned: {:?}", - *LOUISE_FULL2, err, - ), - } - } } diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..018efd2 --- /dev/null +++ b/src/session.rs @@ -0,0 +1,81 @@ +// Copyright (C) 2022-2099 The crate authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +// for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::error::Error; + +use xmpp_parsers::{presence::Presence, BareJid, FullJid}; + +pub type Nick = String; + +/// An occupant session +#[derive(Debug, Clone)] +pub struct Session { + pub presence: Presence, + pub real: FullJid, + pub participant: FullJid, +} + +impl Session { + /// Ensure presence doesn't contain payloads that would impersonate us + fn filter_presence(presence: Presence) -> Presence { + presence + } + + /// Instanciate a Session from a presence which is adressed to the room JID. + pub fn with_nick>(presence: Presence, nick: N) -> Result { + let presence = Session::filter_presence(presence); + Ok(Session { + real: presence + .from + .clone() + .map(FullJid::try_from) + .ok_or(Error::MissingJid)??, + participant: presence + .to + .clone() + .map(BareJid::from) + .ok_or(Error::MissingJid)? + .with_resource(nick.into()), + presence, + }) + } +} + +impl PartialEq for Session { + fn eq(&self, other: &Session) -> bool { + self.real == other.real && self.participant == other.participant + } +} + +impl TryFrom for Session { + type Error = Error; + + fn try_from(presence: Presence) -> Result { + let presence = Session::filter_presence(presence); + Ok(Session { + real: presence + .from + .clone() + .map(FullJid::try_from) + .ok_or(Error::MissingJid)??, + participant: presence + .to + .clone() + .map(FullJid::try_from) + .ok_or(Error::MissingJid)??, + presence, + }) + } +} diff --git a/src/tests/templates.rs b/src/tests/templates.rs index f66f807..01b74bd 100644 --- a/src/tests/templates.rs +++ b/src/tests/templates.rs @@ -13,7 +13,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use crate::room::{Occupant, Room}; +use crate::occupant::Occupant; +use crate::room::Room; use std::str::FromStr; use std::sync::LazyLock; use xmpp_parsers::{