Compare commits
12 commits
main
...
component-
Author | SHA1 | Date | |
---|---|---|---|
ad5ece8ad5 | |||
e338bff920 | |||
db7237069c | |||
7a687df552 | |||
919f3cf754 | |||
4ec1aa42b4 | |||
e7d31e41dc | |||
562aadb488 | |||
44bec0e232 | |||
c884f38dc2 | |||
7554ff4d7c | |||
ef03c1b032 |
9 changed files with 965 additions and 272 deletions
11
.woodpecker.yml
Normal file
11
.woodpecker.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
|
||||
pipeline:
|
||||
lint:
|
||||
image: rustlang/rust:nightly-alpine
|
||||
commands:
|
||||
- cargo fmt --check
|
||||
test:
|
||||
image: rustlang/rust:nightly-alpine
|
||||
commands:
|
||||
- RUST_BACKTRACE=1 cargo test
|
12
README.md
12
README.md
|
@ -13,13 +13,13 @@ file.
|
|||
|
||||
### XMPP
|
||||
|
||||
- [ ] Join
|
||||
* [x] Single session
|
||||
* [ ] Multiple sessions non-MSN
|
||||
* [ ] MSN
|
||||
- [x] Join
|
||||
* [x] Normal sessions
|
||||
* [x] MSN
|
||||
- [ ] Presence
|
||||
* [ ] Probes
|
||||
* [ ] Resync
|
||||
* [x] Updates
|
||||
* [x] Resync
|
||||
* [ ] Probes (storing updates to answer probes)
|
||||
- [ ] Iq
|
||||
* [ ] Ping answers
|
||||
* [ ] Ping probes?
|
||||
|
|
173
src/component.rs
173
src/component.rs
|
@ -12,6 +12,7 @@ use std::marker::Send;
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::pin::Pin;
|
||||
use std::task::Context;
|
||||
use std::thread;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::{task::Poll, Stream};
|
||||
|
@ -78,10 +79,14 @@ impl Component {
|
|||
}
|
||||
|
||||
enum Expect {
|
||||
Element(Element),
|
||||
Iq(Box<dyn FnOnce(Iq) + Send + 'static>),
|
||||
Presence(Box<dyn FnOnce(Presence) + Send + 'static>),
|
||||
Message(Box<dyn FnOnce(Message) + Send + 'static>),
|
||||
/// Simple Element
|
||||
Element(TestElement),
|
||||
/// Callback taking an Iq, with a description alongside
|
||||
Iq(Box<dyn FnOnce(Iq) + Send + 'static>, String),
|
||||
/// Callback taking a Presence, with a description alongside
|
||||
Presence(Box<dyn FnOnce(Presence) + Send + 'static>, String),
|
||||
/// Callback taking a Message, with a description alongside
|
||||
Message(Box<dyn FnOnce(Message) + Send + 'static>, String),
|
||||
}
|
||||
|
||||
impl fmt::Debug for Expect {
|
||||
|
@ -89,75 +94,138 @@ impl fmt::Debug for Expect {
|
|||
write!(f, "Expect::")?;
|
||||
match self {
|
||||
Expect::Element(el) => write!(f, "Element({:?})", String::from(el)),
|
||||
Expect::Iq(_) => write!(f, "Iq(<cb>)"),
|
||||
Expect::Message(_) => write!(f, "Message(<cb>)"),
|
||||
Expect::Presence(_) => write!(f, "Presence(<cb>)"),
|
||||
Expect::Iq(_, desc) => write!(f, "Iq(<cb>, {})", desc),
|
||||
Expect::Message(_, desc) => write!(f, "Message(<cb>, {})", desc),
|
||||
Expect::Presence(_, desc) => write!(f, "Presence(<cb>, {})", desc),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct TestElement(pub Element);
|
||||
|
||||
impl Deref for TestElement {
|
||||
type Target = Element;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TestElement {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", String::from(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TestElement {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", String::from(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TestElement> for String {
|
||||
fn from(elem: &TestElement) -> Self {
|
||||
format!("{}", elem)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Element> for TestElement {
|
||||
fn from(elem: Element) -> Self {
|
||||
Self(elem)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TestElement> for Element {
|
||||
fn from(elem: TestElement) -> Self {
|
||||
elem.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Iq> for TestElement {
|
||||
fn from(elem: Iq) -> Self {
|
||||
Self(Element::from(elem))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Presence> for TestElement {
|
||||
fn from(elem: Presence) -> Self {
|
||||
Self(Element::from(elem))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for TestElement {
|
||||
fn from(elem: Message) -> Self {
|
||||
Self(Element::from(elem))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestComponent {
|
||||
in_buffer: VecDeque<Element>,
|
||||
out_buffer: VecDeque<Element>,
|
||||
in_buffer: VecDeque<TestElement>,
|
||||
expect_buffer: VecDeque<Expect>,
|
||||
}
|
||||
|
||||
impl TestComponent {
|
||||
pub fn new(in_buffer: Vec<Element>) -> Self {
|
||||
TestComponent {
|
||||
in_buffer: VecDeque::from(in_buffer),
|
||||
out_buffer: VecDeque::new(),
|
||||
in_buffer: VecDeque::from(
|
||||
in_buffer
|
||||
.into_iter()
|
||||
.map(|el| TestElement(el))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
expect_buffer: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds elements to be expected, in the order they're being added
|
||||
pub fn expect<E: Into<Element>>(&mut self, el: E) {
|
||||
pub fn expect<E: Into<TestElement>>(&mut self, el: E) {
|
||||
self.expect_buffer.push_back(Expect::Element(el.into()))
|
||||
}
|
||||
|
||||
pub fn expect_iq<F: FnOnce(Iq) + Send + 'static>(&mut self, callback: F) {
|
||||
self.expect_buffer.push_back(Expect::Iq(Box::new(callback)))
|
||||
}
|
||||
|
||||
pub fn expect_message<F: FnOnce(Message) + Send + 'static>(&mut self, callback: F) {
|
||||
pub fn expect_iq<F: FnOnce(Iq) + Send + 'static, S: Into<String>>(
|
||||
&mut self,
|
||||
callback: F,
|
||||
desc: S,
|
||||
) {
|
||||
self.expect_buffer
|
||||
.push_back(Expect::Message(Box::new(callback)))
|
||||
.push_back(Expect::Iq(Box::new(callback), desc.into()))
|
||||
}
|
||||
|
||||
pub fn expect_presence<F: FnOnce(Presence) + Send + 'static>(&mut self, callback: F) {
|
||||
pub fn expect_message<F: FnOnce(Message) + Send + 'static, S: Into<String>>(
|
||||
&mut self,
|
||||
callback: F,
|
||||
desc: S,
|
||||
) {
|
||||
self.expect_buffer
|
||||
.push_back(Expect::Presence(Box::new(callback)))
|
||||
.push_back(Expect::Message(Box::new(callback), desc.into()))
|
||||
}
|
||||
|
||||
/// Asserts expected output and actual output are the same
|
||||
pub fn assert(&mut self) {
|
||||
loop {
|
||||
let out = self.out_buffer.pop_front();
|
||||
let expected = self.expect_buffer.pop_front();
|
||||
pub fn expect_presence<F: FnOnce(Presence) + Send + 'static, S: Into<String>>(
|
||||
&mut self,
|
||||
callback: F,
|
||||
desc: S,
|
||||
) {
|
||||
self.expect_buffer
|
||||
.push_back(Expect::Presence(Box::new(callback), desc.into()))
|
||||
}
|
||||
|
||||
match (out, expected) {
|
||||
(None, None) => break,
|
||||
(Some(out), Some(expected)) => match expected {
|
||||
Expect::Element(el) => assert_eq!(String::from(&el), String::from(&out)),
|
||||
Expect::Iq(cb) => cb(Iq::try_from(out).unwrap()),
|
||||
Expect::Message(cb) => cb(Message::try_from(out).unwrap()),
|
||||
Expect::Presence(cb) => cb(Presence::try_from(out).unwrap()),
|
||||
},
|
||||
(Some(out), None) => panic!("Missing matching expected element: {:?}", out),
|
||||
(None, Some(expected)) => match expected {
|
||||
Expect::Element(el) => panic!("Missing matching sent element: {:?}", el),
|
||||
Expect::Iq(_) => panic!("Missing matching sent iq"),
|
||||
Expect::Message(_) => panic!("Missing matching sent message"),
|
||||
Expect::Presence(_) => panic!("Missing matching sent presence"),
|
||||
},
|
||||
}
|
||||
fn _send_stanza<E: Into<TestElement> + Send>(&mut self, el: E) -> Result<(), Error> {
|
||||
let out: TestElement = el.into();
|
||||
let expected = self.expect_buffer.pop_front();
|
||||
|
||||
match expected {
|
||||
Some(expected) => match expected {
|
||||
Expect::Element(el) => assert_eq!(String::from(&el), String::from(&out)),
|
||||
Expect::Iq(cb, _) => cb(Iq::try_from(out.0).unwrap()),
|
||||
Expect::Message(cb, _) => cb(Message::try_from(out.0).unwrap()),
|
||||
Expect::Presence(cb, _) => cb(Presence::try_from(out.0).unwrap()),
|
||||
},
|
||||
None => panic!("Missing matching expected element: {:?}", out),
|
||||
}
|
||||
}
|
||||
|
||||
fn _send_stanza<E: Into<Element> + Send>(&mut self, el: E) -> Result<(), Error> {
|
||||
Ok(self.out_buffer.push_back(el.into()))
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,23 +234,34 @@ impl Stream for TestComponent {
|
|||
|
||||
fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
while self.in_buffer.len() > 0 {
|
||||
return Poll::Ready(self.in_buffer.pop_front());
|
||||
return Poll::Ready(self.in_buffer.pop_front().map(|el| el.0));
|
||||
}
|
||||
|
||||
Poll::Ready(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestComponent {
|
||||
fn drop(&mut self) {
|
||||
// Don't assert if we're already panicking. Rustc displays a huge backtrace when "panicked
|
||||
// while panicking" even when nobody asks for it (RUST_BACKTRACE unset). Let the error
|
||||
// appear if there isn't any other error.
|
||||
if ! thread::panicking() {
|
||||
assert_eq!(self.expect_buffer.len(), 0, "Remaining expected elements in the buffer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ComponentTrait for TestComponent {
|
||||
async fn send_stanza<E: Into<Element> + Send>(&mut self, el: E) -> Result<(), Error> {
|
||||
self._send_stanza(el)
|
||||
self._send_stanza(el.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ComponentTrait for &mut TestComponent {
|
||||
async fn send_stanza<E: Into<Element> + Send>(&mut self, el: E) -> Result<(), Error> {
|
||||
self._send_stanza(el)
|
||||
self._send_stanza(el.into())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ pub enum Error {
|
|||
MismatchJids(Jid, Jid),
|
||||
NickAlreadyAssigned(String),
|
||||
NonexistantSession(FullJid),
|
||||
SessionAlreadyExists(FullJid),
|
||||
XMPPError(TokioXMPPError),
|
||||
}
|
||||
|
||||
|
@ -35,6 +36,7 @@ impl fmt::Display for Error {
|
|||
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::SessionAlreadyExists(err) => write!(f, "Session already exist: {}", err),
|
||||
Error::XMPPError(err) => write!(f, "XMPP error: {}", err),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -175,7 +175,7 @@ async fn handle_presence<C: ComponentTrait>(
|
|||
).into()
|
||||
]);
|
||||
if let Some(mut room) = rooms.remove(&roomjid) {
|
||||
match room.remove_session(component, realjid).await {
|
||||
match room.remove_session(component, realjid, participant.resource.clone()).await {
|
||||
Ok(()) => (),
|
||||
Err(Error::NonexistantSession(_)) => {
|
||||
component.send_stanza(error).await.unwrap();
|
||||
|
|
680
src/room.rs
680
src/room.rs
|
@ -16,7 +16,7 @@
|
|||
use crate::component::ComponentTrait;
|
||||
use crate::error::Error;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::iter::IntoIterator;
|
||||
|
||||
use chrono::{FixedOffset, Utc};
|
||||
|
@ -34,11 +34,25 @@ use xmpp_parsers::{
|
|||
};
|
||||
|
||||
pub type Nick = String;
|
||||
type Session = FullJid;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum BroadcastPresence {
|
||||
/// Resource joined the room. It needs to know about all other participants, and other
|
||||
/// participants needs to know about it.
|
||||
Join,
|
||||
/// Resource lost sync. It needs to know about all other participants.
|
||||
Resync,
|
||||
/// Resource change status (becomes busy, etc.), tell all other participants.
|
||||
Update,
|
||||
/// Resource leaves. It only needs confirmation that it leaves. Tell all other participants.
|
||||
Leave,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Room {
|
||||
pub jid: BareJid,
|
||||
pub occupants: HashMap<BareJid, Occupant>,
|
||||
pub occupants: BTreeMap<Nick, Occupant>,
|
||||
// TODO: Subject struct.
|
||||
// TODO: Store subject lang
|
||||
pub subject: Option<(String, Occupant, DateTime)>,
|
||||
|
@ -48,107 +62,235 @@ impl Room {
|
|||
pub fn new(jid: BareJid) -> Self {
|
||||
Room {
|
||||
jid,
|
||||
occupants: HashMap::new(),
|
||||
occupants: BTreeMap::new(),
|
||||
subject: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn broadcast_presence<C: ComponentTrait>(
|
||||
&self,
|
||||
component: &mut C,
|
||||
own_occupant: &Occupant,
|
||||
own_session: &Session,
|
||||
mode: BroadcastPresence,
|
||||
) -> Result<(), Error> {
|
||||
let leave = mode == BroadcastPresence::Leave;
|
||||
|
||||
// All participants to new participant
|
||||
let presence_to_new = Presence::new(
|
||||
if leave { PresenceType::Unavailable } else { PresenceType::None }
|
||||
)
|
||||
.with_to(own_session.clone())
|
||||
.with_payloads(vec![MucUser {
|
||||
status: Vec::new(),
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
}
|
||||
.into()]);
|
||||
|
||||
// New participant to all other sessions
|
||||
let presence_to_old = Presence::new(
|
||||
if leave { PresenceType::Unavailable } else { PresenceType::None }
|
||||
)
|
||||
.with_from(Jid::Full(own_occupant.participant.clone()))
|
||||
.with_payloads(vec![MucUser {
|
||||
status: Vec::new(),
|
||||
items: vec![
|
||||
MucItem::new(
|
||||
Affiliation::Owner,
|
||||
if leave { Role::None } else { Role::Moderator },
|
||||
)
|
||||
],
|
||||
}
|
||||
.into()]);
|
||||
|
||||
let sync = match mode {
|
||||
BroadcastPresence::Join |
|
||||
BroadcastPresence::Resync => true,
|
||||
_ => false,
|
||||
};
|
||||
let update = match mode {
|
||||
BroadcastPresence::Join |
|
||||
BroadcastPresence::Update => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
for (_, other) in self.occupants.iter() {
|
||||
if own_occupant.nick == other.nick {
|
||||
continue;
|
||||
}
|
||||
if sync { // Send presences from others to participant.
|
||||
let presence = presence_to_new.clone()
|
||||
.with_from(Jid::Full(other.participant.clone()));
|
||||
component.send_stanza(presence).await?;
|
||||
}
|
||||
if update || leave { // Send presence from participant to others.
|
||||
for session in other.iter() {
|
||||
// Skip sending if it's us.
|
||||
if session == own_session {
|
||||
continue;
|
||||
}
|
||||
|
||||
let presence = presence_to_old.clone()
|
||||
.with_to(Jid::Full(session.clone()));
|
||||
component.send_stanza(presence).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MucItems to be sent to sessions of this occupant
|
||||
let self_items = own_occupant.iter().map(|session| {
|
||||
MucItem {
|
||||
affiliation: Affiliation::Owner,
|
||||
role: if leave { Role::None } else { Role::Moderator },
|
||||
jid: Some(session.clone()),
|
||||
nick: None,
|
||||
actor: None,
|
||||
continue_: None,
|
||||
reason: None,
|
||||
}
|
||||
}).collect::<Vec<MucItem>>();
|
||||
|
||||
// Multi-Session Nick: For this occupant, include all sessions all with item@jid discovered
|
||||
// so they can identify each other as being the same account under the same nick.
|
||||
let session_presence = Presence::new(
|
||||
if leave { PresenceType::Unavailable } else { PresenceType::None }
|
||||
)
|
||||
.with_from(Jid::Full(own_occupant.participant.clone()));
|
||||
|
||||
for session in own_occupant.iter() {
|
||||
if session == own_session {
|
||||
continue;
|
||||
}
|
||||
let presence = session_presence.clone()
|
||||
.with_to(Jid::Full(session.clone()))
|
||||
.with_payloads(vec![MucUser {
|
||||
status: vec![],
|
||||
items: self_items.clone(),
|
||||
}.into()]);
|
||||
component.send_stanza(presence).await?;
|
||||
}
|
||||
|
||||
// Send self-presence
|
||||
if sync || leave {
|
||||
// New participant to all other sessions
|
||||
let self_presence = Presence::new(
|
||||
if leave { PresenceType::Unavailable } else { PresenceType::None }
|
||||
)
|
||||
.with_from(Jid::Full(own_occupant.participant.clone()))
|
||||
.with_to(own_session.clone())
|
||||
.with_payloads(vec![MucUser {
|
||||
status: if leave { vec![
|
||||
MucStatus::SelfPresence,
|
||||
] } else {
|
||||
vec![
|
||||
MucStatus::SelfPresence,
|
||||
MucStatus::AssignedNick,
|
||||
] },
|
||||
items: if leave {
|
||||
vec![MucItem::new(Affiliation::Owner, Role::None)]
|
||||
} else {
|
||||
self_items
|
||||
},
|
||||
}
|
||||
.into()]);
|
||||
component.send_stanza(self_presence).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_subject<C: ComponentTrait>(
|
||||
&mut self,
|
||||
component: &mut C,
|
||||
realjid: Session,
|
||||
occupant: Occupant,
|
||||
) -> Result<(), Error> {
|
||||
debug!("Sending subject!");
|
||||
if self.subject.is_none() {
|
||||
let subject = String::from("");
|
||||
let setter = occupant;
|
||||
let stamp = DateTime(Utc::now().with_timezone(&FixedOffset::east(0)));
|
||||
self.subject = Some((subject, setter, stamp));
|
||||
}
|
||||
|
||||
let mut subject = Message::new(Some(Jid::Full(realjid)));
|
||||
subject.from = Some(Jid::Full(
|
||||
self.subject.as_ref().unwrap().1.participant.clone(),
|
||||
));
|
||||
subject.subjects.insert(
|
||||
String::from("en"),
|
||||
Subject(self.subject.as_ref().unwrap().0.clone()),
|
||||
);
|
||||
subject.type_ = MessageType::Groupchat;
|
||||
subject.payloads = vec![Delay {
|
||||
from: Some(Jid::Bare(self.jid.clone())),
|
||||
stamp: self.subject.as_ref().unwrap().2.clone(),
|
||||
data: None,
|
||||
}
|
||||
.into()];
|
||||
component.send_stanza(subject).await
|
||||
}
|
||||
|
||||
pub async fn add_session<C: ComponentTrait>(
|
||||
&mut self,
|
||||
component: &mut C,
|
||||
realjid: FullJid,
|
||||
nick: Nick,
|
||||
realjid: Session,
|
||||
new_nick: Nick,
|
||||
) -> Result<(), Error> {
|
||||
let bare = BareJid::from(realjid.clone());
|
||||
if let Some(occupant) = self.occupants.get_mut(&bare) {
|
||||
occupant.add_session(realjid)?;
|
||||
} else {
|
||||
debug!("{} is joining {}", realjid, self.jid);
|
||||
|
||||
let new_occupant = Occupant::new(&self, realjid.clone(), nick.clone());
|
||||
|
||||
// Ensure nick isn't already assigned
|
||||
let _ = self.occupants.iter().try_for_each(|(_, occupant)| {
|
||||
let nick = nick.clone();
|
||||
if occupant.nick == nick {
|
||||
return Err(Error::NickAlreadyAssigned(nick));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Send occupants
|
||||
debug!("Sending occupants for {}", realjid);
|
||||
// Other participants to new participant
|
||||
let presence_to_new = Presence::new(PresenceType::None)
|
||||
// New occupant with a single session
|
||||
.with_to(new_occupant.sessions[0].clone())
|
||||
.with_payloads(vec![MucUser {
|
||||
status: Vec::new(),
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
}
|
||||
.into()]);
|
||||
// New participant to other participants
|
||||
let presence_to_old = Presence::new(PresenceType::None)
|
||||
.with_from(Jid::Full(new_occupant.participant.clone()))
|
||||
.with_payloads(vec![MucUser {
|
||||
status: Vec::new(),
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
}
|
||||
.into()]);
|
||||
for (_, occupant) in self.occupants.iter() {
|
||||
component
|
||||
.send_stanza(
|
||||
presence_to_new
|
||||
.clone()
|
||||
.with_from(occupant.participant.clone()),
|
||||
)
|
||||
.await?;
|
||||
for session in occupant.iter() {
|
||||
component
|
||||
.send_stanza(presence_to_old.clone().with_to(session.clone()))
|
||||
.await?;
|
||||
}
|
||||
// Ensure nick isn't already assigned
|
||||
let _ = self.occupants.iter().try_for_each(|(nick, occupant)| {
|
||||
let new_nick = new_nick.as_str();
|
||||
if new_nick == nick && occupant.real != BareJid::from(realjid.clone()) {
|
||||
return Err(Error::NickAlreadyAssigned(String::from(new_nick)));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Add into occupants
|
||||
let _ = self.occupants.insert(bare.clone(), new_occupant.clone());
|
||||
|
||||
// Self-presence
|
||||
debug!("Sending self-presence for {}", realjid);
|
||||
let participant: FullJid = self.jid.clone().with_resource(nick);
|
||||
let status = vec![MucStatus::SelfPresence, MucStatus::AssignedNick];
|
||||
let items = vec![MucItem::new(Affiliation::Owner, Role::Moderator)];
|
||||
let self_presence = Presence::new(PresenceType::None)
|
||||
.with_from(participant.clone())
|
||||
.with_to(realjid.clone())
|
||||
.with_payloads(vec![MucUser { status, items }.into()]);
|
||||
component.send_stanza(self_presence).await?;
|
||||
|
||||
// Send subject
|
||||
debug!("Sending subject!");
|
||||
if self.subject.is_none() {
|
||||
let subject = String::from("");
|
||||
let setter = new_occupant;
|
||||
let stamp = DateTime(Utc::now().with_timezone(&FixedOffset::east(0)));
|
||||
self.subject = Some((subject, setter, stamp));
|
||||
let mode: Option<BroadcastPresence> = {
|
||||
if let Some(occupant) = self.occupants.get_mut(&new_nick) {
|
||||
match occupant.add_session(realjid.clone()) {
|
||||
Ok(_) => {
|
||||
Some(BroadcastPresence::Join)
|
||||
},
|
||||
Err(Error::SessionAlreadyExists(_)) => {
|
||||
Some(BroadcastPresence::Resync)
|
||||
},
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
} else {
|
||||
Some(BroadcastPresence::Join)
|
||||
}
|
||||
};
|
||||
|
||||
let mut subject = Message::new(Some(Jid::Full(realjid)));
|
||||
subject.from = Some(Jid::Full(
|
||||
self.subject.as_ref().unwrap().1.participant.clone(),
|
||||
));
|
||||
subject.subjects.insert(
|
||||
String::from("en"),
|
||||
Subject(self.subject.as_ref().unwrap().0.clone()),
|
||||
if ! self.occupants.contains_key(&new_nick) {
|
||||
let _ = self.occupants.insert(
|
||||
new_nick.clone(),
|
||||
Occupant::new(&self, realjid.clone(), new_nick.clone()),
|
||||
);
|
||||
subject.type_ = MessageType::Groupchat;
|
||||
subject.payloads = vec![Delay {
|
||||
from: Some(Jid::Bare(self.jid.clone())),
|
||||
stamp: self.subject.as_ref().unwrap().2.clone(),
|
||||
data: None,
|
||||
}
|
||||
.into()];
|
||||
component.send_stanza(subject).await?;
|
||||
}
|
||||
let occupant = self.occupants.get(&new_nick).unwrap();
|
||||
|
||||
match mode {
|
||||
Some(BroadcastPresence::Resync) => {
|
||||
self.broadcast_presence(
|
||||
component,
|
||||
&occupant,
|
||||
&realjid,
|
||||
BroadcastPresence::Resync,
|
||||
).await?;
|
||||
},
|
||||
Some(BroadcastPresence::Join) => {
|
||||
debug!("{} is joining {}", realjid, self.jid);
|
||||
|
||||
self.broadcast_presence(
|
||||
component,
|
||||
&occupant,
|
||||
&realjid,
|
||||
BroadcastPresence::Join,
|
||||
).await?;
|
||||
self.send_subject(component, realjid, occupant.clone()).await?;
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -157,56 +299,34 @@ impl Room {
|
|||
pub async fn remove_session<C: ComponentTrait>(
|
||||
&mut self,
|
||||
component: &mut C,
|
||||
realjid: FullJid,
|
||||
realjid: Session,
|
||||
nick: Nick,
|
||||
) -> 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()]);
|
||||
if let Some(mut occupant) = self.occupants.remove(&nick) {
|
||||
self.broadcast_presence(
|
||||
component,
|
||||
&occupant,
|
||||
&realjid,
|
||||
BroadcastPresence::Leave,
|
||||
).await?;
|
||||
|
||||
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?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: Error
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Occupant {
|
||||
/// Public Jid for the Occupant
|
||||
real: BareJid,
|
||||
participant: FullJid,
|
||||
nick: Nick,
|
||||
sessions: Vec<FullJid>,
|
||||
pub real: BareJid,
|
||||
pub participant: FullJid,
|
||||
pub nick: Nick,
|
||||
pub sessions: Vec<FullJid>,
|
||||
}
|
||||
|
||||
impl Occupant {
|
||||
|
@ -224,6 +344,13 @@ impl Occupant {
|
|||
return Err(Error::MismatchJids(Jid::from(self.real.clone()), Jid::from(real.clone())));
|
||||
}
|
||||
|
||||
for session in &self.sessions {
|
||||
if &real == session {
|
||||
return Err(Error::SessionAlreadyExists(real))
|
||||
}
|
||||
}
|
||||
|
||||
self.sessions.push(real);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -263,14 +390,293 @@ impl Occupant {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use xmpp_parsers::{BareJid, FullJid};
|
||||
use crate::component::TestComponent;
|
||||
use xmpp_parsers::{
|
||||
BareJid, Element,
|
||||
presence::{Presence, Type as PresenceType},
|
||||
muc::{
|
||||
MucUser,
|
||||
user::{Affiliation, Role, Item as MucItem, Status as MucStatus},
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn occupant_ignore_dup_session() {
|
||||
let room = Room::new(BareJid::from_str("room@muc").unwrap());
|
||||
let real = FullJid::from_str("foo@bar/meh").unwrap();
|
||||
let mut occupant = Occupant::new(&room, real.clone(), String::from("nick"));
|
||||
occupant.add_session(real.clone()).unwrap();
|
||||
assert_eq!(occupant.iter().count(), 1);
|
||||
#[tokio::test]
|
||||
async fn test_broadcast_presence_resync() {
|
||||
let roomjid = BareJid::from_str("room@muc").unwrap();
|
||||
let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap();
|
||||
let participant1 = roomjid.clone().with_resource(String::from("nick1"));
|
||||
let realjid2 = FullJid::from_str("qxx@foo/bar").unwrap();
|
||||
let participant2 = roomjid.clone().with_resource(String::from("nick2"));
|
||||
let realjid3 = FullJid::from_str("bar@qxx/foo").unwrap();
|
||||
let participant3 = roomjid.clone().with_resource(String::from("nick3"));
|
||||
|
||||
let mut room = Room::new(roomjid.clone());
|
||||
|
||||
room.occupants = BTreeMap::new();
|
||||
room.occupants.insert(
|
||||
participant1.resource.clone(), Occupant::new(&room, realjid1.clone(), String::from("nick1")),
|
||||
);
|
||||
room.occupants.insert(
|
||||
participant2.resource.clone(), Occupant::new(&room, realjid2.clone(), String::from("nick2")),
|
||||
);
|
||||
let occupant3 = Occupant::new(&room, realjid3.clone(), String::from("nick3"));
|
||||
room.occupants.insert(participant3.resource.clone(), occupant3.clone());
|
||||
|
||||
let self_item = MucItem::try_from(
|
||||
r#"<item xmlns="http://jabber.org/protocol/muc#user" affiliation="owner" role="moderator" jid="bar@qxx/foo"/>"#
|
||||
.parse::<Element>()
|
||||
.unwrap()
|
||||
).unwrap();
|
||||
|
||||
// BroadcastPresence::Resync
|
||||
let mut component = TestComponent::new(vec![]);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(participant1.clone())
|
||||
.with_to(realjid3.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(participant2.clone())
|
||||
.with_to(realjid3.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(participant3.clone())
|
||||
.with_to(realjid3.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
|
||||
items: vec![self_item.clone()],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
room.broadcast_presence(&mut component, &occupant3, &realjid3, BroadcastPresence::Resync).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_broadcast_presence_update() {
|
||||
let roomjid = BareJid::from_str("room@muc").unwrap();
|
||||
let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap();
|
||||
let participant1 = roomjid.clone().with_resource(String::from("nick1"));
|
||||
let realjid2 = FullJid::from_str("qxx@foo/bar").unwrap();
|
||||
let participant2 = roomjid.clone().with_resource(String::from("nick2"));
|
||||
let realjid3 = FullJid::from_str("bar@qxx/foo").unwrap();
|
||||
let participant3 = roomjid.clone().with_resource(String::from("nick3"));
|
||||
|
||||
let mut room = Room::new(roomjid.clone());
|
||||
|
||||
room.occupants = BTreeMap::new();
|
||||
room.occupants.insert(
|
||||
participant1.resource.clone(), Occupant::new(&room, realjid1.clone(), String::from("nick1")),
|
||||
);
|
||||
room.occupants.insert(
|
||||
participant2.resource.clone(), Occupant::new(&room, realjid2.clone(), String::from("nick2")),
|
||||
);
|
||||
let occupant3 = Occupant::new(&room, realjid3.clone(), String::from("nick3"));
|
||||
room.occupants.insert(participant3.resource.clone(), occupant3.clone());
|
||||
|
||||
// BroadcastPresence::Update
|
||||
let mut component = TestComponent::new(vec![]);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(participant3.clone())
|
||||
.with_to(realjid1.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(participant3.clone())
|
||||
.with_to(realjid2.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
room.broadcast_presence(&mut component, &occupant3, &realjid3, BroadcastPresence::Update).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_broadcast_presence_join() {
|
||||
let roomjid = BareJid::from_str("room@muc").unwrap();
|
||||
let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap();
|
||||
let participant1 = roomjid.clone().with_resource(String::from("nick1"));
|
||||
let realjid2 = FullJid::from_str("qxx@foo/bar").unwrap();
|
||||
let participant2 = roomjid.clone().with_resource(String::from("nick2"));
|
||||
let realjid3 = FullJid::from_str("bar@qxx/foo").unwrap();
|
||||
let participant3 = roomjid.clone().with_resource(String::from("nick3"));
|
||||
|
||||
let mut room = Room::new(roomjid.clone());
|
||||
|
||||
room.occupants = BTreeMap::new();
|
||||
room.occupants.insert(
|
||||
participant1.resource.clone(), Occupant::new(&room, realjid1.clone(), String::from("nick1")),
|
||||
);
|
||||
room.occupants.insert(
|
||||
participant2.resource.clone(), Occupant::new(&room, realjid2.clone(), String::from("nick2")),
|
||||
);
|
||||
let occupant3 = Occupant::new(&room, realjid3.clone(), String::from("nick3"));
|
||||
room.occupants.insert(participant3.resource.clone(), occupant3.clone());
|
||||
|
||||
let self_item = MucItem::try_from(
|
||||
r#"<item xmlns="http://jabber.org/protocol/muc#user" affiliation="owner" role="moderator" jid="bar@qxx/foo"/>"#
|
||||
.parse::<Element>()
|
||||
.unwrap()
|
||||
).unwrap();
|
||||
|
||||
// BroadcastPresence::Join
|
||||
let mut component = TestComponent::new(vec![]);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(participant1.clone())
|
||||
.with_to(realjid3.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(participant3.clone())
|
||||
.with_to(realjid1.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(participant2.clone())
|
||||
.with_to(realjid3.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(participant3.clone())
|
||||
.with_to(realjid2.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(participant3.clone())
|
||||
.with_to(realjid3.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
|
||||
items: vec![self_item],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
room.broadcast_presence(&mut component, &occupant3, &realjid3, BroadcastPresence::Join).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_broadcast_presence_leave() {
|
||||
let roomjid = BareJid::from_str("room@muc").unwrap();
|
||||
let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap();
|
||||
let participant1 = roomjid.clone().with_resource(String::from("nick1"));
|
||||
let realjid2 = FullJid::from_str("qxx@foo/bar").unwrap();
|
||||
let participant2 = roomjid.clone().with_resource(String::from("nick2"));
|
||||
let realjid3 = FullJid::from_str("bar@qxx/foo").unwrap();
|
||||
let participant3 = roomjid.clone().with_resource(String::from("nick3"));
|
||||
|
||||
let mut room = Room::new(roomjid.clone());
|
||||
|
||||
room.occupants = BTreeMap::new();
|
||||
room.occupants.insert(
|
||||
participant1.resource.clone(), Occupant::new(&room, realjid1.clone(), String::from("nick1")),
|
||||
);
|
||||
room.occupants.insert(
|
||||
participant2.resource.clone(), Occupant::new(&room, realjid2.clone(), String::from("nick2")),
|
||||
);
|
||||
let occupant3 = Occupant::new(&room, realjid3.clone(), String::from("nick3"));
|
||||
room.occupants.insert(participant3.resource.clone(), occupant3.clone());
|
||||
|
||||
// BroadcastPresence::Leave
|
||||
let mut component = TestComponent::new(vec![]);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::Unavailable)
|
||||
.with_from(participant3.clone())
|
||||
.with_to(realjid1.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::None)],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::Unavailable)
|
||||
.with_from(participant3.clone())
|
||||
.with_to(realjid2.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::None)],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::Unavailable)
|
||||
.with_from(participant3.clone())
|
||||
.with_to(realjid3.clone())
|
||||
.with_payloads(vec![
|
||||
MucUser {
|
||||
status: vec![MucStatus::SelfPresence],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::None)],
|
||||
}.into()
|
||||
])
|
||||
);
|
||||
|
||||
room.broadcast_presence(&mut component, &occupant3, &realjid3, BroadcastPresence::Leave).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
64
src/tests/iq.rs
Normal file
64
src/tests/iq.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// 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::component::TestComponent;
|
||||
use crate::handlers::handle_stanza;
|
||||
use crate::room::Room;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use xmpp_parsers::{
|
||||
iq::{Iq, IqType},
|
||||
stanza_error::{DefinedCondition, ErrorType, StanzaError},
|
||||
BareJid, Element, FullJid, Jid,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
static ref COMPONENT_JID: BareJid = BareJid::from_str("muc.component").unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_iq_unimplemented() {
|
||||
let from = Jid::Full(FullJid::from_str("foo@bar/qxx").unwrap());
|
||||
let to = Jid::Bare(COMPONENT_JID.clone());
|
||||
|
||||
let disco: Element = Iq {
|
||||
from: Some(from.clone()),
|
||||
to: Some(to.clone()),
|
||||
id: String::from("disco"),
|
||||
payload: IqType::Get(Element::builder("x", "urn:example:unimplemented").build()),
|
||||
}
|
||||
.into();
|
||||
|
||||
let reply: Element = Iq::from_error(
|
||||
"disco",
|
||||
StanzaError::new(
|
||||
ErrorType::Cancel,
|
||||
DefinedCondition::ServiceUnavailable,
|
||||
"en",
|
||||
"No handler defined for this kind of iq.",
|
||||
),
|
||||
)
|
||||
.with_from(to)
|
||||
.with_to(from)
|
||||
.into();
|
||||
|
||||
let mut component = TestComponent::new(vec![disco]);
|
||||
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
|
||||
|
||||
component.expect(reply);
|
||||
handle_stanza(&mut component, &mut rooms).await.unwrap();
|
||||
}
|
20
src/tests/mod.rs
Normal file
20
src/tests/mod.rs
Normal file
|
@ -0,0 +1,20 @@
|
|||
// 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/>.
|
||||
|
||||
#[cfg(test)]
|
||||
mod iq;
|
||||
|
||||
#[cfg(test)]
|
||||
mod presence;
|
|
@ -23,7 +23,6 @@ use std::str::FromStr;
|
|||
use lazy_static::lazy_static;
|
||||
use xmpp_parsers::{
|
||||
delay::Delay,
|
||||
iq::{Iq, IqType},
|
||||
message::{Message, MessageType, Subject as MessageSubject},
|
||||
muc::{
|
||||
user::{Affiliation, Item as MucItem, Role, Status as MucStatus},
|
||||
|
@ -38,40 +37,6 @@ lazy_static! {
|
|||
static ref COMPONENT_JID: BareJid = BareJid::from_str("muc.component").unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_iq_unimplemented() {
|
||||
let from = Jid::Full(FullJid::from_str("foo@bar/qxx").unwrap());
|
||||
let to = Jid::Bare(COMPONENT_JID.clone());
|
||||
|
||||
let disco: Element = Iq {
|
||||
from: Some(from.clone()),
|
||||
to: Some(to.clone()),
|
||||
id: String::from("disco"),
|
||||
payload: IqType::Get(Element::builder("x", "urn:example:unimplemented").build()),
|
||||
}
|
||||
.into();
|
||||
|
||||
let reply: Element = Iq::from_error(
|
||||
"disco",
|
||||
StanzaError::new(
|
||||
ErrorType::Cancel,
|
||||
DefinedCondition::ServiceUnavailable,
|
||||
"en",
|
||||
"No handler defined for this kind of iq.",
|
||||
),
|
||||
)
|
||||
.with_from(to)
|
||||
.with_to(from)
|
||||
.into();
|
||||
|
||||
let mut component = TestComponent::new(vec![disco]);
|
||||
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
|
||||
|
||||
component.expect(reply);
|
||||
handle_stanza(&mut component, &mut rooms).await.unwrap();
|
||||
component.assert();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_join_presence_empty_room() {
|
||||
let from = FullJid::from_str("foo@bar/qxx").unwrap();
|
||||
|
@ -101,7 +66,11 @@ async fn test_join_presence_empty_room() {
|
|||
.with_to(Jid::Full(from.clone()))
|
||||
.with_payloads(vec![MucUser {
|
||||
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
items: vec![{
|
||||
let mut item = MucItem::new(Affiliation::Owner, Role::Moderator);
|
||||
item.jid = Some(from.clone());
|
||||
item
|
||||
}],
|
||||
}
|
||||
.into()]),
|
||||
);
|
||||
|
@ -134,10 +103,9 @@ async fn test_join_presence_empty_room() {
|
|||
String::from(&expected),
|
||||
String::from(&Into::<Element>::into(out))
|
||||
);
|
||||
});
|
||||
}, "Room subject to participant1");
|
||||
|
||||
handle_stanza(&mut component, &mut rooms).await.unwrap();
|
||||
component.assert();
|
||||
|
||||
assert_eq!(rooms.len(), 1);
|
||||
match rooms.get(&roomjid) {
|
||||
|
@ -174,10 +142,8 @@ async fn test_join_presence_nick_already_assigned() {
|
|||
let mut component = TestComponent::new(vec![join1, join2]);
|
||||
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
|
||||
|
||||
// Ignore self-presence for first participant
|
||||
component.expect_presence(|_| ());
|
||||
// Ignore message subject for first participant
|
||||
component.expect_message(|_| ());
|
||||
component.expect_presence(|_| (), "Self-presence for first participant");
|
||||
component.expect_message(|_| (), "Subject for first participant");
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::Error)
|
||||
|
@ -193,7 +159,6 @@ async fn test_join_presence_nick_already_assigned() {
|
|||
);
|
||||
|
||||
handle_stanza(&mut component, &mut rooms).await.unwrap();
|
||||
component.assert();
|
||||
|
||||
match rooms.get(&roomjid) {
|
||||
Some(room) => assert_eq!(room.occupants.len(), 1),
|
||||
|
@ -224,10 +189,8 @@ async fn test_join_presence_existing_room() {
|
|||
let mut component = TestComponent::new(vec![join1, join2]);
|
||||
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
|
||||
|
||||
// Ignore self-presence for first participant
|
||||
component.expect_presence(|_| ());
|
||||
// Ignore message subject for first participant
|
||||
component.expect_message(|_| ());
|
||||
component.expect_presence(|_| (), "Self-presence for participant1");
|
||||
component.expect_message(|_| (), "Subject for participant1");
|
||||
|
||||
// Participant1 presence for participant2
|
||||
component.expect(
|
||||
|
@ -260,12 +223,15 @@ async fn test_join_presence_existing_room() {
|
|||
.with_to(Jid::Full(realjid2.clone()))
|
||||
.with_payloads(vec![MucUser {
|
||||
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
|
||||
items: vec![MucItem::new(Affiliation::Owner, Role::Moderator)],
|
||||
items: vec![{
|
||||
let mut item = MucItem::new(Affiliation::Owner, Role::Moderator);
|
||||
item.jid = Some(realjid2.clone());
|
||||
item
|
||||
}],
|
||||
}
|
||||
.into()]),
|
||||
);
|
||||
|
||||
// Subject for participant2
|
||||
component.expect_message(|el| {
|
||||
let mut subjects = BTreeMap::new();
|
||||
subjects.insert(String::from("en"), MessageSubject::from_str("").unwrap());
|
||||
|
@ -292,10 +258,9 @@ async fn test_join_presence_existing_room() {
|
|||
String::from(&expected),
|
||||
String::from(&Into::<Element>::into(out))
|
||||
);
|
||||
});
|
||||
}, "Subject for participant2");
|
||||
|
||||
handle_stanza(&mut component, &mut rooms).await.unwrap();
|
||||
component.assert();
|
||||
|
||||
match rooms.get(&roomjid) {
|
||||
Some(room) => assert_eq!(room.occupants.len(), 2),
|
||||
|
@ -303,6 +268,73 @@ async fn test_join_presence_existing_room() {
|
|||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_presence_resync() {
|
||||
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 mut component = TestComponent::new(vec![join1.clone(), join2, join1]);
|
||||
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
|
||||
|
||||
component.expect_presence(|_| (), "Self-presence for participant1");
|
||||
component.expect_message(|_| (), "Subject for participant1");
|
||||
component.expect_presence(|_| (), "Participant1 presence for participant2");
|
||||
component.expect_presence(|_| (), "Participant2 presence for participant1");
|
||||
component.expect_presence(|_| (), "Self-presence for participant2");
|
||||
component.expect_message(|_| (), "Subject for participant2");
|
||||
|
||||
// Resync: Participant2 presence for participant1
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.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::Moderator)],
|
||||
}
|
||||
.into()]),
|
||||
);
|
||||
|
||||
// Resync: Participant1 self-presence
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(Jid::Full(participant1))
|
||||
.with_to(Jid::Full(realjid1.clone()))
|
||||
.with_payloads(vec![MucUser {
|
||||
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
|
||||
items: vec![{
|
||||
let mut item = MucItem::new(Affiliation::Owner, Role::Moderator);
|
||||
item.jid = Some(realjid1.clone());
|
||||
item
|
||||
}],
|
||||
}
|
||||
.into()]),
|
||||
);
|
||||
|
||||
handle_stanza(&mut component, &mut rooms).await.unwrap();
|
||||
|
||||
match rooms.get(&roomjid) {
|
||||
Some(room) => assert_eq!(room.occupants.len(), 2),
|
||||
None => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_leave_non_existing_room() {
|
||||
let realjid1 = FullJid::from_str("foo@bar/qxx").unwrap();
|
||||
|
@ -319,7 +351,6 @@ async fn test_leave_non_existing_room() {
|
|||
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]
|
||||
|
@ -343,10 +374,8 @@ async fn test_leave_last_participant() {
|
|||
let mut component = TestComponent::new(vec![join1, leave1]);
|
||||
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
|
||||
|
||||
// Ignore self-presence for participant1
|
||||
component.expect_presence(|_| ());
|
||||
// Ignore subject message for participant1
|
||||
component.expect_message(|_| ());
|
||||
component.expect_presence(|_| (), "Self-presence for participant1");
|
||||
component.expect_message(|_| (), "Subject for participant1");
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::Unavailable)
|
||||
|
@ -361,7 +390,6 @@ async fn test_leave_last_participant() {
|
|||
|
||||
handle_stanza(&mut component, &mut rooms).await.unwrap();
|
||||
|
||||
component.assert();
|
||||
assert_eq!(rooms.len(), 0);
|
||||
}
|
||||
|
||||
|
@ -394,29 +422,12 @@ async fn test_leave_room_not_last() {
|
|||
let mut component = TestComponent::new(vec![join1, join2, leave2]);
|
||||
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
|
||||
|
||||
// Ignore self-presence for participant1
|
||||
component.expect_presence(|_| ());
|
||||
// Ignore subject message for participant1
|
||||
component.expect_message(|_| ());
|
||||
// Ignore participant1 presence for participant2
|
||||
component.expect_presence(|_| ());
|
||||
// Ignore self-presence for participant2
|
||||
component.expect_presence(|_| ());
|
||||
// Ignore participant2 presence for participant1
|
||||
component.expect_presence(|_| ());
|
||||
// Ignore subject message for participant2
|
||||
component.expect_message(|_| ());
|
||||
|
||||
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(|_| (), "Self-presence for participant1");
|
||||
component.expect_message(|_| (), "Subject message for participant1");
|
||||
component.expect_presence(|_| (), "Participant1 presence for participant2");
|
||||
component.expect_presence(|_| (), "Self-presence for participant2");
|
||||
component.expect_presence(|_| (), "Participant2 presence for participant1");
|
||||
component.expect_message(|_| (), "Subject message for participant2");
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::Unavailable)
|
||||
|
@ -429,11 +440,111 @@ async fn test_leave_room_not_last() {
|
|||
.into()]),
|
||||
);
|
||||
|
||||
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()]),
|
||||
);
|
||||
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_join_msn() {
|
||||
let barejid = BareJid::from_str("foo@bar").unwrap();
|
||||
let realjid1 = barejid.clone().with_resource("qxx");
|
||||
let realjid2 = barejid.clone().with_resource("hah");
|
||||
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 join2: Element = Presence::new(PresenceType::None)
|
||||
.with_from(Jid::Full(realjid2.clone()))
|
||||
.with_to(Jid::Full(participant1.clone()))
|
||||
.with_payloads(vec![Muc::new().into()])
|
||||
.into();
|
||||
|
||||
let mut component = TestComponent::new(vec![join1, join2]);
|
||||
let mut rooms: HashMap<BareJid, Room> = HashMap::new();
|
||||
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(Jid::Full(participant1.clone()))
|
||||
.with_to(Jid::Full(realjid1.clone()))
|
||||
.with_payloads(vec![MucUser {
|
||||
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
|
||||
items: vec![{
|
||||
let mut item = MucItem::new(Affiliation::Owner, Role::Moderator);
|
||||
item.jid = Some(realjid1.clone());
|
||||
item
|
||||
}],
|
||||
}.into()])
|
||||
);
|
||||
|
||||
component.expect_message(|_| (), "Subject message for participant1");
|
||||
|
||||
// New session joins
|
||||
// Participant1 presence for participant2
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(Jid::Full(participant1.clone()))
|
||||
.with_to(Jid::Full(realjid1.clone()))
|
||||
.with_payloads(vec![MucUser {
|
||||
status: Vec::new(),
|
||||
items: {
|
||||
let item = MucItem::new(Affiliation::Owner, Role::Moderator);
|
||||
let mut item1 = item.clone();
|
||||
item1.jid = Some(realjid1.clone());
|
||||
let mut item2 = item.clone();
|
||||
item2.jid = Some(realjid2.clone());
|
||||
vec![item1, item2]
|
||||
},
|
||||
}.into()])
|
||||
);
|
||||
|
||||
// Self-presence for participant2
|
||||
component.expect(
|
||||
Presence::new(PresenceType::None)
|
||||
.with_from(Jid::Full(participant1.clone()))
|
||||
.with_to(Jid::Full(realjid2.clone()))
|
||||
.with_payloads(vec![MucUser {
|
||||
status: vec![MucStatus::SelfPresence, MucStatus::AssignedNick],
|
||||
items: {
|
||||
let item = MucItem::new(Affiliation::Owner, Role::Moderator);
|
||||
let mut item1 = item.clone();
|
||||
item1.jid = Some(realjid1.clone());
|
||||
let mut item2 = item.clone();
|
||||
item2.jid = Some(realjid2.clone());
|
||||
vec![item1, item2]
|
||||
},
|
||||
}.into()])
|
||||
);
|
||||
|
||||
component.expect_message(|_| (), "Subject message for participant2");
|
||||
|
||||
handle_stanza(&mut component, &mut rooms).await.unwrap();
|
||||
|
||||
assert_eq!(rooms.len(), 1);
|
||||
match rooms.get(&roomjid) {
|
||||
Some(room) => {
|
||||
assert_eq!(room.occupants.len(), 1);
|
||||
assert_eq!(room.occupants.get(&participant1.resource).unwrap().sessions.len(), 2);
|
||||
},
|
||||
None => panic!(),
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue