lib.rs

  1// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7#![deny(bare_trait_objects)]
  8
  9use futures::stream::StreamExt;
 10use std::cell::RefCell;
 11use std::convert::TryFrom;
 12use std::rc::Rc;
 13use tokio_xmpp::{AsyncClient as TokioXmppClient, Event as TokioXmppEvent};
 14use xmpp_parsers::{
 15    bookmarks2::Conference,
 16    caps::{compute_disco, hash_caps, Caps},
 17    disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity},
 18    hashes::Algo,
 19    iq::{Iq, IqType},
 20    message::{Body, Message, MessageType},
 21    muc::{
 22        user::{MucUser, Status},
 23        Muc,
 24    },
 25    ns,
 26    presence::{Presence, Type as PresenceType},
 27    pubsub::pubsub::{Items, PubSub},
 28    roster::{Item as RosterItem, Roster},
 29    stanza_error::{DefinedCondition, ErrorType, StanzaError},
 30    BareJid, FullJid, Jid,
 31};
 32#[macro_use]
 33extern crate log;
 34
 35mod pubsub;
 36
 37pub type Error = tokio_xmpp::Error;
 38
 39#[derive(Debug)]
 40pub enum ClientType {
 41    Bot,
 42    Pc,
 43}
 44
 45impl Default for ClientType {
 46    fn default() -> Self {
 47        ClientType::Bot
 48    }
 49}
 50
 51impl ToString for ClientType {
 52    fn to_string(&self) -> String {
 53        String::from(match self {
 54            ClientType::Bot => "bot",
 55            ClientType::Pc => "pc",
 56        })
 57    }
 58}
 59
 60#[derive(PartialEq)]
 61pub enum ClientFeature {
 62    #[cfg(feature = "avatars")]
 63    Avatars,
 64    ContactList,
 65    JoinRooms,
 66}
 67
 68pub type RoomNick = String;
 69
 70#[derive(Debug)]
 71pub enum Event {
 72    Online,
 73    Disconnected,
 74    ContactAdded(RosterItem),
 75    ContactRemoved(RosterItem),
 76    ContactChanged(RosterItem),
 77    #[cfg(feature = "avatars")]
 78    AvatarRetrieved(Jid, String),
 79    ChatMessage(BareJid, Body),
 80    JoinRoom(BareJid, Conference),
 81    LeaveRoom(BareJid),
 82    LeaveAllRooms,
 83    RoomJoined(BareJid),
 84    RoomLeft(BareJid),
 85    RoomMessage(BareJid, RoomNick, Body),
 86}
 87
 88#[derive(Default)]
 89pub struct ClientBuilder<'a> {
 90    jid: &'a str,
 91    password: &'a str,
 92    website: String,
 93    default_nick: String,
 94    lang: Vec<String>,
 95    disco: (ClientType, String),
 96    features: Vec<ClientFeature>,
 97}
 98
 99impl ClientBuilder<'_> {
100    pub fn new<'a>(jid: &'a str, password: &'a str) -> ClientBuilder<'a> {
101        ClientBuilder {
102            jid,
103            password,
104            website: String::from("https://gitlab.com/xmpp-rs/tokio-xmpp"),
105            default_nick: String::from("xmpp-rs"),
106            lang: vec![String::from("en")],
107            disco: (ClientType::default(), String::from("tokio-xmpp")),
108            features: vec![],
109        }
110    }
111
112    pub fn set_client(mut self, type_: ClientType, name: &str) -> Self {
113        self.disco = (type_, String::from(name));
114        self
115    }
116
117    pub fn set_website(mut self, url: &str) -> Self {
118        self.website = String::from(url);
119        self
120    }
121
122    pub fn set_default_nick(mut self, nick: &str) -> Self {
123        self.default_nick = String::from(nick);
124        self
125    }
126
127    pub fn set_lang(mut self, lang: Vec<String>) -> Self {
128        self.lang = lang;
129        self
130    }
131
132    pub fn enable_feature(mut self, feature: ClientFeature) -> Self {
133        self.features.push(feature);
134        self
135    }
136
137    fn make_disco(&self) -> DiscoInfoResult {
138        let identities = vec![Identity::new(
139            "client",
140            self.disco.0.to_string(),
141            "en",
142            self.disco.1.to_string(),
143        )];
144        let mut features = vec![Feature::new(ns::DISCO_INFO)];
145        #[cfg(feature = "avatars")]
146        {
147            if self.features.contains(&ClientFeature::Avatars) {
148                features.push(Feature::new(format!("{}+notify", ns::AVATAR_METADATA)));
149            }
150        }
151        if self.features.contains(&ClientFeature::JoinRooms) {
152            features.push(Feature::new(format!("{}+notify", ns::BOOKMARKS2)));
153        }
154        DiscoInfoResult {
155            node: None,
156            identities,
157            features,
158            extensions: vec![],
159        }
160    }
161
162    pub fn build(self) -> Result<Agent, Error> {
163        let client = TokioXmppClient::new(self.jid, self.password)?;
164        Ok(self.build_impl(client)?)
165    }
166
167    // This function is meant to be used for testing build
168    pub(crate) fn build_impl(self, client: TokioXmppClient) -> Result<Agent, Error> {
169        let disco = self.make_disco();
170        let node = self.website;
171
172        let agent = Agent {
173            client,
174            default_nick: Rc::new(RefCell::new(self.default_nick)),
175            lang: Rc::new(self.lang),
176            disco,
177            node,
178        };
179
180        Ok(agent)
181    }
182}
183
184pub struct Agent {
185    client: TokioXmppClient,
186    default_nick: Rc<RefCell<String>>,
187    lang: Rc<Vec<String>>,
188    disco: DiscoInfoResult,
189    node: String,
190}
191
192impl Agent {
193    pub async fn join_room(
194        &mut self,
195        room: BareJid,
196        nick: Option<String>,
197        password: Option<String>,
198        lang: &str,
199        status: &str,
200    ) {
201        let mut muc = Muc::new();
202        if let Some(password) = password {
203            muc = muc.with_password(password);
204        }
205
206        let nick = nick.unwrap_or_else(|| self.default_nick.borrow().clone());
207        let room_jid = room.with_resource(nick);
208        let mut presence = Presence::new(PresenceType::None).with_to(Jid::Full(room_jid));
209        presence.add_payload(muc);
210        presence.set_status(String::from(lang), String::from(status));
211        let _ = self.client.send_stanza(presence.into()).await;
212    }
213
214    pub async fn send_message(
215        &mut self,
216        recipient: Jid,
217        type_: MessageType,
218        lang: &str,
219        text: &str,
220    ) {
221        let mut message = Message::new(Some(recipient));
222        message.type_ = type_;
223        message
224            .bodies
225            .insert(String::from(lang), Body(String::from(text)));
226        let _ = self.client.send_stanza(message.into()).await;
227    }
228
229    fn make_initial_presence(disco: &DiscoInfoResult, node: &str) -> Presence {
230        let caps_data = compute_disco(disco);
231        let hash = hash_caps(&caps_data, Algo::Sha_1).unwrap();
232        let caps = Caps::new(node, hash);
233
234        let mut presence = Presence::new(PresenceType::None);
235        presence.add_payload(caps);
236        presence
237    }
238
239    async fn handle_iq(&mut self, iq: Iq) -> Vec<Event> {
240        let mut events = vec![];
241        let from = iq
242            .from
243            .clone()
244            .unwrap_or_else(|| self.client.bound_jid().unwrap().clone());
245        if let IqType::Get(payload) = iq.payload {
246            if payload.is("query", ns::DISCO_INFO) {
247                let query = DiscoInfoQuery::try_from(payload);
248                match query {
249                    Ok(query) => {
250                        let mut disco_info = self.disco.clone();
251                        disco_info.node = query.node;
252                        let iq = Iq::from_result(iq.id, Some(disco_info))
253                            .with_to(iq.from.unwrap())
254                            .into();
255                        let _ = self.client.send_stanza(iq).await;
256                    }
257                    Err(err) => {
258                        let error = StanzaError::new(
259                            ErrorType::Modify,
260                            DefinedCondition::BadRequest,
261                            "en",
262                            &format!("{}", err),
263                        );
264                        let iq = Iq::from_error(iq.id, error)
265                            .with_to(iq.from.unwrap())
266                            .into();
267                        let _ = self.client.send_stanza(iq).await;
268                    }
269                }
270            } else {
271                // We MUST answer unhandled get iqs with a service-unavailable error.
272                let error = StanzaError::new(
273                    ErrorType::Cancel,
274                    DefinedCondition::ServiceUnavailable,
275                    "en",
276                    "No handler defined for this kind of iq.",
277                );
278                let iq = Iq::from_error(iq.id, error)
279                    .with_to(iq.from.unwrap())
280                    .into();
281                let _ = self.client.send_stanza(iq).await;
282            }
283        } else if let IqType::Result(Some(payload)) = iq.payload {
284            // TODO: move private iqs like this one somewhere else, for
285            // security reasons.
286            if payload.is("query", ns::ROSTER) && iq.from.is_none() {
287                let roster = Roster::try_from(payload).unwrap();
288                for item in roster.items.into_iter() {
289                    events.push(Event::ContactAdded(item));
290                }
291            } else if payload.is("pubsub", ns::PUBSUB) {
292                let new_events = pubsub::handle_iq_result(&from, payload);
293                events.extend(new_events);
294            }
295        } else if let IqType::Set(_) = iq.payload {
296            // We MUST answer unhandled set iqs with a service-unavailable error.
297            let error = StanzaError::new(
298                ErrorType::Cancel,
299                DefinedCondition::ServiceUnavailable,
300                "en",
301                "No handler defined for this kind of iq.",
302            );
303            let iq = Iq::from_error(iq.id, error)
304                .with_to(iq.from.unwrap())
305                .into();
306            let _ = self.client.send_stanza(iq).await;
307        }
308
309        events
310    }
311
312    async fn handle_message(&mut self, message: Message) -> Vec<Event> {
313        let mut events = vec![];
314        let from = message.from.clone().unwrap();
315        let langs: Vec<&str> = self.lang.iter().map(String::as_str).collect();
316        match message.get_best_body(langs) {
317            Some((_lang, body)) => match message.type_ {
318                MessageType::Groupchat => {
319                    let event = Event::RoomMessage(
320                        from.clone().into(),
321                        FullJid::try_from(from.clone()).unwrap().resource,
322                        body.clone(),
323                    );
324                    events.push(event)
325                }
326                MessageType::Chat | MessageType::Normal => {
327                    let event = Event::ChatMessage(from.clone().into(), body.clone());
328                    events.push(event)
329                }
330                _ => (),
331            },
332            None => (),
333        }
334        for child in message.payloads {
335            if child.is("event", ns::PUBSUB_EVENT) {
336                let new_events = pubsub::handle_event(&from, child, self).await;
337                events.extend(new_events);
338            }
339        }
340
341        events
342    }
343
344    async fn handle_presence(&mut self, presence: Presence) -> Vec<Event> {
345        let mut events = vec![];
346        let from: BareJid = match presence.from.clone().unwrap() {
347            Jid::Full(FullJid { node, domain, .. }) => BareJid { node, domain },
348            Jid::Bare(bare) => bare,
349        };
350        for payload in presence.payloads.into_iter() {
351            let muc_user = match MucUser::try_from(payload) {
352                Ok(muc_user) => muc_user,
353                _ => continue,
354            };
355            for status in muc_user.status.into_iter() {
356                if status == Status::SelfPresence {
357                    events.push(Event::RoomJoined(from.clone()));
358                    break;
359                }
360            }
361        }
362
363        events
364    }
365
366    pub async fn wait_for_events(&mut self) -> Option<Vec<Event>> {
367        if let Some(event) = self.client.next().await {
368            let mut events = Vec::new();
369
370            match event {
371                TokioXmppEvent::Online { resumed: false, .. } => {
372                    let presence = Self::make_initial_presence(&self.disco, &self.node).into();
373                    let _ = self.client.send_stanza(presence).await;
374                    events.push(Event::Online);
375                    // TODO: only send this when the ContactList feature is enabled.
376                    let iq = Iq::from_get(
377                        "roster",
378                        Roster {
379                            ver: None,
380                            items: vec![],
381                        },
382                    )
383                    .into();
384                    let _ = self.client.send_stanza(iq).await;
385                    // TODO: only send this when the JoinRooms feature is enabled.
386                    let iq =
387                        Iq::from_get("bookmarks", PubSub::Items(Items::new(ns::BOOKMARKS2))).into();
388                    let _ = self.client.send_stanza(iq).await;
389                }
390                TokioXmppEvent::Online { resumed: true, .. } => {}
391                TokioXmppEvent::Disconnected(_) => {
392                    events.push(Event::Disconnected);
393                }
394                TokioXmppEvent::Stanza(elem) => {
395                    if elem.is("iq", "jabber:client") {
396                        let iq = Iq::try_from(elem).unwrap();
397                        let new_events = self.handle_iq(iq).await;
398                        events.extend(new_events);
399                    } else if elem.is("message", "jabber:client") {
400                        let message = Message::try_from(elem).unwrap();
401                        let new_events = self.handle_message(message).await;
402                        events.extend(new_events);
403                    } else if elem.is("presence", "jabber:client") {
404                        let presence = Presence::try_from(elem).unwrap();
405                        let new_events = self.handle_presence(presence).await;
406                        events.extend(new_events);
407                    } else if elem.is("error", "http://etherx.jabber.org/streams") {
408                        println!("Received a fatal stream error: {}", String::from(&elem));
409                    } else {
410                        panic!("Unknown stanza: {}", String::from(&elem));
411                    }
412                }
413            }
414
415            Some(events)
416        } else {
417            None
418        }
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::{Agent, ClientBuilder, ClientFeature, ClientType, Event};
425    use tokio_xmpp::AsyncClient as TokioXmppClient;
426
427    #[tokio::test]
428    async fn test_simple() {
429        let client = TokioXmppClient::new("foo@bar", "meh").unwrap();
430
431        // Client instance
432        let client_builder = ClientBuilder::new("foo@bar", "meh")
433            .set_client(ClientType::Bot, "xmpp-rs")
434            .set_website("https://gitlab.com/xmpp-rs/xmpp-rs")
435            .set_default_nick("bot")
436            .enable_feature(ClientFeature::Avatars)
437            .enable_feature(ClientFeature::ContactList);
438
439        let mut agent: Agent = client_builder.build_impl(client).unwrap();
440
441        while let Some(events) = agent.wait_for_events().await {
442            assert!(match events[0] {
443                Event::Disconnected => true,
444                _ => false,
445            });
446            assert_eq!(events.len(), 1);
447            break;
448        }
449    }
450}