We can now leave rooms!

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
Maxime “pep” Buquet 2022-09-11 22:10:46 +02:00
parent b7ee30c3f7
commit d383aa8655
4 changed files with 268 additions and 41 deletions

View file

@ -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),
}
}

View file

@ -106,51 +106,87 @@ async fn handle_presence<C: ComponentTrait>(
presence: Presence,
rooms: &mut HashMap<BareJid, Room>,
) -> 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);
}
}

View file

@ -134,6 +134,51 @@ impl Room {
Ok(())
}
pub async fn remove_session<C: ComponentTrait>(
&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 {

View file

@ -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<BareJid, Room> = 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<BareJid, Room> = 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<BareJid, Room> = 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!(),
}
}