First pass for nickname changes

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
Maxime “pep” Buquet 2023-01-01 18:59:47 +01:00
parent efe6c913a1
commit 1a5101c9b2
Signed by: pep
GPG key ID: DEDA74AEECA9D0F2
6 changed files with 361 additions and 43 deletions

View file

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

View file

@ -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,9 +104,20 @@ async fn handle_presence_full_available<C: ComponentTrait>(
}
} else if let ControlFlow::Continue(_) = muc {
// <{muc}x/> wasn't found
match room.update_presence(component, presence.clone()).await {
Ok(()) => (),
Err(Error::ParticipantNotFound(_)) => {
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)
@ -123,8 +135,15 @@ async fn handle_presence_full_available<C: ComponentTrait>(
.into()]);
component.send_stanza(error).await?
}
/*
match room.update_presence(component, presence.clone()).await {
Ok(()) => (),
Err(Error::ParticipantNotFound(_)) => {
}
err => err?,
}
*/
}
} else {
debug!("Presence received to new room: {}", &roomjid);
@ -163,7 +182,7 @@ async fn handle_presence_full_unavailable<C: ComponentTrait>(
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(),

View file

@ -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<Session, Error> {
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:?}",),
}
}
}

View file

@ -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
}
/// 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<C: ComponentTrait>(
&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<Session, Error> {
// 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:?}"),
}
}
}

View file

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

View file

@ -37,8 +37,11 @@ pub const LOUISE_FULL1: LazyLock<FullJid> =
pub const LOUISE_FULL2: LazyLock<FullJid> =
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<FullJid> =
LazyLock::new(|| ROOM1_BARE.clone().with_resource(LOUISE_NICK));
pub const LOUISE_ROOM1_PART2: LazyLock<FullJid> =
LazyLock::new(|| ROOM1_BARE.clone().with_resource(LOUISE_NICK2));
/// https://en.wikipedia.org/wiki/Kanno_Sugako
pub const SUGAKO_BARE: LazyLock<BareJid> =