commit 05033ee6f7e5e155b28486197f6fb3def7cbaf4d Author: Maxime “pep” Buquet Date: Wed Sep 7 20:08:27 2022 +0200 Initial commit: Users can join! Signed-off-by: Maxime “pep” Buquet diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..714c5d3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "hanabi-muc" +version = "0.1.0" +edition = "2021" +license = "AGPL-3.0-or-later" +description = "MUC implementation allowing participants to play the Hanabi game." + +[dependencies] +env_logger = "^0.9" +futures = "^0.3" +log = "^0.4" +tokio = "^1.20" +tokio-xmpp = { version = "^3.2", default-features = false, features = ["tls-rust"] } +xmpp-parsers = { version = "^0.19", features = ["component"] } + +[patch.crates-io] +jid = { path = "../xmpp-rs/jid" } +minidom = { path = "../xmpp-rs/minidom" } +tokio-xmpp = { path = "../xmpp-rs/tokio-xmpp" } +xmpp-parsers = { path = "../xmpp-rs/parsers" } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..698fe58 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,345 @@ +// 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 . + +#![feature(once_cell)] + +use std::collections::HashMap; +use std::convert::TryFrom; +use std::env::args; +use std::error::Error as StdError; +use std::fmt; +use std::iter::IntoIterator; +use std::ops::ControlFlow; +use std::process::exit; +use std::sync::{LazyLock, Mutex}; + +use env_logger; +use futures::stream::StreamExt; +use log::{debug, error, info}; +use tokio_xmpp::{Component, Error as TokioXMPPError}; +use xmpp_parsers::{ + disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity}, + iq::{Iq, IqType}, + message::{Message, Subject, MessageType}, + muc::{ + user::{Affiliation, Item as MucItem, Role, Status as MucStatus}, + Muc, MucUser, + }, + ns, + presence::{Presence, Type as PresenceType}, + stanza_error::{DefinedCondition, ErrorType, StanzaError}, + BareJid, Element, FullJid, Jid, +}; + +#[derive(Debug)] +enum Error { + MismatchJids(Jid), + NickAlreadyAssigned(String), + XMPPError(TokioXMPPError), +} + +impl StdError for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::MismatchJids(err) => write!(f, "Mismatch Jids: {}", err), + Error::NickAlreadyAssigned(err) => write!(f, "Nickname already assigned: {}", err), + Error::XMPPError(err) => write!(f, "XMPP error: {}", err), + } + } +} + +impl From for Error { + fn from(err: TokioXMPPError) -> Error { + Error::XMPPError(err) + } +} + +async fn send_stanza>(component: &mut Component, el: E) -> Result<(), Error> { + let el: Element = el.into(); + debug!("SEND: {}", String::from(&el)); + component.send_stanza(el).await?; + Ok(()) +} + +pub type Nick = String; + +#[derive(Debug)] +struct Room { + jid: BareJid, + occupants: HashMap, +} + +impl Room { + fn new(jid: BareJid) -> Self { + Room { + jid, + occupants: HashMap::new(), + } + } + + async fn add_session( + &mut self, + component: &mut Component, + realjid: FullJid, + 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); + + // 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); + let presence = Presence::new(PresenceType::None) + .with_to(realjid.clone()); + for (_, occupant) in self.occupants.iter() { + for session in occupant.iter() { + let presence = presence.clone().with_from(session.clone()); + send_stanza(component, presence).await?; + } + } + + // Add into occupants + let _ = self + .occupants + .insert(bare.clone(), Occupant::new(realjid.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) + .with_to(realjid.clone()) + .with_payloads(vec![MucUser { status, items }.into()]); + send_stanza(component, self_presence).await?; + + // Send subject + debug!("Sending subject!"); + let mut subject = Message::new(Some(Jid::Full(realjid))); + subject.from = Some(Jid::Bare(self.jid.clone())); + subject.subjects.insert(String::from("en"), Subject(String::from("Hanabi"))); + subject.type_ = MessageType::Groupchat; + send_stanza(component, subject).await?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct Occupant { + jid: BareJid, + nick: Nick, + sessions: Vec, +} + +impl Occupant { + fn new(fulljid: FullJid) -> Occupant { + Occupant { + jid: BareJid::from(fulljid.clone()), + nick: fulljid.resource.clone(), + sessions: vec![fulljid], + } + } + + fn add_session(&mut self, fulljid: FullJid) -> Result<(), Error> { + if BareJid::from(fulljid.clone()) != self.jid { + return Err(Error::MismatchJids(Jid::from(fulljid.clone()))); + } + + Ok(()) + } +} + +impl IntoIterator for Occupant { + type Item = FullJid; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.sessions.into_iter() + } +} + +impl Occupant { + fn iter(&self) -> std::slice::Iter<'_, FullJid> { + self.sessions.iter() + } + + fn iter_mut(&mut self) -> std::slice::IterMut<'_, FullJid> { + self.sessions.iter_mut() + } +} + +static mut ROOMS: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +async fn handle_iq_disco(component: &mut Component, iq: Iq, payload: Element) { + match DiscoInfoQuery::try_from(payload) { + Ok(DiscoInfoQuery { node }) if node.is_none() => { + let identities = vec![Identity::new("conference", "text", "en", "Hanabi")]; + let features = vec![ + Feature::new("http://jabber.org/protocol/disco#info"), + Feature::new("xmpp:bouah.net:hanabi:muc:0"), + ]; + let extensions = Vec::new(); + let payload = DiscoInfoResult { + node: None, + identities, + features, + extensions, + }; + let reply = Iq::from_result(iq.id, Some(payload)) + .with_from(iq.to.unwrap()) + .with_to(iq.from.unwrap()); + send_stanza(component, reply).await.unwrap(); + } + Ok(DiscoInfoQuery { .. }) => { + let error = StanzaError::new( + ErrorType::Modify, + DefinedCondition::BadRequest, + "en", + format!("Unknown disco#info node"), + ); + let reply = Iq::from_error(iq.id, error) + .with_from(iq.to.unwrap()) + .with_to(iq.from.unwrap()); + send_stanza(component, reply).await.unwrap(); + } + Err(err) => error!("Failed to parse iq: {}", err), + } +} + +async fn handle_iq(component: &mut Component, iq: Iq) { + match iq.payload { + IqType::Get(ref payload) => { + if payload.is("query", ns::DISCO_INFO) { + handle_iq_disco(component, iq.clone(), payload.clone()).await + } else { + // We MUST answer unhandled get iqs with a service-unavailable error. + let error = StanzaError::new( + ErrorType::Cancel, + DefinedCondition::ServiceUnavailable, + "en", + "No handler defined for this kind of iq.", + ); + let iq = Iq::from_error(iq.id, error) + .with_from(iq.to.unwrap()) + .with_to(iq.from.unwrap()) + .into(); + let _ = component.send_stanza(iq).await; + } + } + _ => error!("Not handled iq: {:?}", iq), + } +} + +async fn handle_presence(component: &mut Component, presence: Presence) { + 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; + } + + // Presences to MUC come from resources not accounts + if let Jid::Full(realjid) = presence.from.unwrap() && + let Jid::Full(participant) = presence.to.unwrap() { + + let roomjid = BareJid::from(participant.clone()); + let nick: Nick = participant.resource.clone(); + + // Room already exists + if let Some(room) = unsafe { ROOMS.lock().unwrap().get_mut(&roomjid) } { + debug!("Presence received to existing room: {}", &roomjid); + room.add_session(component, realjid, nick).await.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 _ = unsafe { ROOMS.lock().unwrap().insert(roomjid, room) }; + } + } +} + +async fn handle_message(_component: &mut Component, _message: Message) {} + +#[tokio::main] +async fn main() { + let args: Vec = args().collect(); + if args.len() != 3 { + println!("Usage: {} ", args[0]); + exit(1); + } + + let jid = &args[1]; + let passwd = &args[2]; + let server = "::1"; + let port = 5347u16; + + env_logger::init(); + + let mut component = Component::new(jid, passwd, server, port).await.unwrap(); + info!("Online as {}!", component.jid); + + while let Some(elem) = component.next().await { + debug!("RECV {}", String::from(&elem)); + if elem.is("iq", ns::COMPONENT_ACCEPT) { + let iq = Iq::try_from(elem).unwrap(); + handle_iq(&mut component, iq).await; + } else if elem.is("message", ns::COMPONENT_ACCEPT) { + let message = Message::try_from(elem).unwrap(); + handle_message(&mut component, message).await; + } else if elem.is("presence", ns::COMPONENT_ACCEPT) { + let presence = Presence::try_from(elem).unwrap(); + handle_presence(&mut component, presence).await; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::str::FromStr; + + use xmpp_parsers::{BareJid, FullJid, Jid}; + + #[test] + fn occupant_ignore_dup_session() { + let fulljid = FullJid::from_str("foo@bar/meh").unwrap(); + let mut occupant = Occupant::new(fulljid.clone()); + occupant.add_session(fulljid.clone()); + assert_eq!(occupant.iter().count(), 1); + } +}