xmpp_client_rs/mods/
disco.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4use std::collections::{HashMap, HashSet};
5use std::convert::TryFrom;
6use std::fmt;
7use std::str::FromStr;
8
9use anyhow::{anyhow, Result};
10use uuid::Uuid;
11
12use jid::Jid;
13use xmpp_parsers::disco;
14use xmpp_parsers::disco::Feature;
15use xmpp_parsers::iq::{Iq, IqType};
16use xmpp_parsers::ns;
17
18use crate::account::Account;
19use crate::core::{Aparte, AparteAsync, Event, ModTrait};
20use crate::i18n;
21
22pub struct DiscoMod {
23    identity: disco::Identity,
24    client_features: HashSet<Feature>,
25    server_features: HashMap<Account, Vec<String>>,
26}
27
28impl DiscoMod {
29    pub fn new<C: Into<String>, T: Into<String>, L: Into<String>, N: Into<String>>(
30        category: C,
31        type_: T,
32        lang: L,
33        name: N,
34    ) -> Self {
35        Self {
36            identity: disco::Identity::new(category, type_, lang, name),
37            client_features: HashSet::new(),
38            server_features: HashMap::new(),
39        }
40    }
41
42    pub fn add_feature<S: Into<String>>(&mut self, feature: S) {
43        let feature = Feature::new(feature.into());
44        log::debug!("Adding `{}` feature", feature.var);
45        self.client_features.insert(feature);
46    }
47
48    pub fn has_feature(&self, account: &Account, feature: &str) -> bool {
49        self.server_features
50            .get(account)
51            .unwrap()
52            .iter()
53            .any(|i| i == feature)
54    }
55
56    async fn get_server_disco(
57        aparte: &mut AparteAsync,
58        account: &Account,
59        jid: &Jid,
60    ) -> Result<()> {
61        let resp = aparte
62            .iq(
63                account,
64                Self::disco_info_query_iq(&Jid::from_str(jid.domain().as_ref()).unwrap(), None),
65            )
66            .await?;
67
68        match resp.payload {
69            IqType::Result(Some(el)) => {
70                if let Ok(disco) = disco::DiscoInfoResult::try_from(el) {
71                    aparte.schedule(Event::Disco(
72                        account.clone(),
73                        disco.features.iter().map(|i| i.var.clone()).collect(),
74                    ));
75
76                    Ok(())
77                } else {
78                    Err(anyhow!("Cannot get server disco info: invalid response"))
79                }
80            }
81            IqType::Error(err) => Err(anyhow!(
82                "Cannot get server disco info: {}",
83                i18n::xmpp_err_to_string(&err, vec![]).1
84            )),
85            _ => Err(anyhow!("Cannot get server disco info: invalid response")),
86        }
87    }
88
89    fn disco_info_query_iq(jid: &Jid, node: Option<String>) -> Iq {
90        let id = Uuid::new_v4().hyphenated().to_string();
91        let query = disco::DiscoInfoQuery { node };
92        Iq::from_get(id, query).with_to(jid.clone())
93    }
94
95    pub fn get_disco(&self) -> disco::DiscoInfoResult {
96        let identities = vec![self.identity.clone()];
97        disco::DiscoInfoResult {
98            node: None,
99            identities,
100            features: self.client_features.iter().cloned().collect(),
101            extensions: vec![],
102        }
103    }
104}
105
106impl ModTrait for DiscoMod {
107    fn init(&mut self, _aparte: &mut Aparte) -> Result<(), ()> {
108        self.add_feature(ns::DISCO_INFO);
109        // TODO? self.add_feature(ns::DISCO_ITEMS);
110        Ok(())
111    }
112
113    fn on_event(&mut self, aparte: &mut Aparte, event: &Event) {
114        match event {
115            Event::Connected(account, jid) => {
116                self.server_features.insert(account.clone(), Vec::new());
117
118                Aparte::spawn({
119                    let mut aparte = aparte.proxy();
120                    let account = account.clone();
121                    let jid = jid.clone();
122                    async move {
123                        if let Err(err) = Self::get_server_disco(&mut aparte, &account, &jid).await
124                        {
125                            crate::error!(aparte, err, "Cannot get server disco");
126                        }
127                    }
128                });
129            }
130            Event::Disco(account, features) => {
131                if let Some(server_features) = self.server_features.get_mut(account) {
132                    server_features.extend(features.clone());
133                }
134            }
135            Event::Iq(account, iq) => {
136                if let IqType::Get(el) = iq.payload.clone() {
137                    if let Ok(_disco) = disco::DiscoInfoQuery::try_from(el) {
138                        let id = iq.id.clone();
139                        let disco = self.get_disco();
140                        let iq = Iq::from_result(id, Some(disco));
141                        aparte.send(account, iq);
142                    }
143                }
144            }
145            _ => {}
146        }
147    }
148}
149
150impl fmt::Display for DiscoMod {
151    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
152        write!(f, "XEP-0030: Service Discovery")
153    }
154}