First pass for nickname changes
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
parent
efe6c913a1
commit
1a5101c9b2
6 changed files with 361 additions and 43 deletions
|
@ -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"),
|
||||
|
|
|
@ -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,28 +104,46 @@ async fn handle_presence_full_available<C: ComponentTrait>(
|
|||
}
|
||||
} else if let ControlFlow::Continue(_) = muc {
|
||||
// <{muc}x/> wasn't found
|
||||
|
||||
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)
|
||||
.with_payloads(vec![MucUser {
|
||||
status: vec![
|
||||
MucStatus::SelfPresence,
|
||||
MucStatus::Kicked,
|
||||
MucStatus::ServiceErrorKick,
|
||||
],
|
||||
items: {
|
||||
vec![MucItem::new(Affiliation::None, Role::None)
|
||||
.with_reason("You are not in the room.")]
|
||||
},
|
||||
}
|
||||
.into()]);
|
||||
component.send_stanza(error).await?
|
||||
}
|
||||
|
||||
/*
|
||||
match room.update_presence(component, presence.clone()).await {
|
||||
Ok(()) => (),
|
||||
Err(Error::ParticipantNotFound(_)) => {
|
||||
let error = Presence::new(PresenceType::Unavailable)
|
||||
.with_from(participant)
|
||||
.with_to(realjid)
|
||||
.with_payloads(vec![MucUser {
|
||||
status: vec![
|
||||
MucStatus::SelfPresence,
|
||||
MucStatus::Kicked,
|
||||
MucStatus::ServiceErrorKick,
|
||||
],
|
||||
items: {
|
||||
vec![MucItem::new(Affiliation::None, Role::None)
|
||||
.with_reason("You are not in the room.")]
|
||||
},
|
||||
}
|
||||
.into()]);
|
||||
component.send_stanza(error).await?
|
||||
}
|
||||
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(),
|
||||
|
|
|
@ -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:?}",),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
240
src/room.rs
240
src/room.rs
|
@ -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?;
|
||||
.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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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> =
|
||||
|
|
Loading…
Reference in a new issue