Split Occupant and Session into their own module

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
Maxime “pep” Buquet 2022-12-05 21:01:50 +01:00
parent 1463a55d3f
commit 2a2d8cd051
Signed by: pep
GPG key ID: DEDA74AEECA9D0F2
6 changed files with 290 additions and 232 deletions

View file

@ -13,7 +13,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::room::Session;
use crate::session::Session;
use std::error::Error as StdError;
use std::fmt;

View file

@ -21,7 +21,9 @@
mod component;
mod error;
mod handlers;
mod occupant;
mod room;
mod session;
#[cfg(test)]
mod tests;

196
src/occupant.rs Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
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<Session>,
}
impl Occupant {
/// New occupant
pub fn new(presence: Presence) -> Result<Occupant, Error> {
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<J: Into<Jid>>(&self, _jid: &J) -> bool {
true
}
}
impl IntoIterator for Occupant {
type Item = Session;
type IntoIter = std::vec::IntoIter<Self::Item>;
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,
),
}
}
}

View file

@ -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<N: Into<Nick>>(presence: Presence, nick: N) -> Result<Session, Error> {
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<Presence> for Session {
type Error = Error;
fn try_from(presence: Presence) -> Result<Session, Error> {
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<Session>,
}
impl Occupant {
/// New occupant
pub fn new(presence: Presence) -> Result<Occupant, Error> {
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<J: Into<Jid>>(&self, _jid: &J) -> bool {
true
}
}
impl IntoIterator for Occupant {
type Item = Session;
type IntoIter = std::vec::IntoIter<Self::Item>;
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,
),
}
}
}

81
src/session.rs Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
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<N: Into<Nick>>(presence: Presence, nick: N) -> Result<Session, Error> {
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<Presence> for Session {
type Error = Error;
fn try_from(presence: Presence) -> Result<Session, Error> {
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,
})
}
}

View file

@ -13,7 +13,8 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::room::{Occupant, Room};
use crate::occupant::Occupant;
use crate::room::Room;
use std::str::FromStr;
use std::sync::LazyLock;
use xmpp_parsers::{