Merge branch 'roster' into 'master'

lumi created

Add a roster plugin

See merge request !18

Change summary

src/plugins/mod.rs    |   1 
src/plugins/roster.rs | 186 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 187 insertions(+)

Detailed changes

src/plugins/mod.rs 🔗

@@ -1,5 +1,6 @@
 pub mod messaging;
 pub mod presence;
+pub mod roster;
 pub mod disco;
 pub mod ping;
 pub mod ibb;

src/plugins/roster.rs 🔗

@@ -0,0 +1,186 @@
+use std::collections::HashMap;
+use std::convert::TryFrom;
+use std::sync::Mutex;
+
+use plugin::PluginProxy;
+use event::{Event, Priority, Propagation};
+use jid::Jid;
+
+use plugins::stanza::Iq;
+use plugins::disco::DiscoPlugin;
+use xmpp_parsers::iq::{IqType, IqPayload};
+use xmpp_parsers::roster::{Roster, Item, Subscription};
+use xmpp_parsers::ns;
+
+#[derive(Debug)]
+pub struct RosterReceived {
+    pub ver: Option<String>,
+    pub jids: HashMap<Jid, Item>,
+}
+
+#[derive(Debug)]
+pub enum RosterPush {
+    Added(Item),
+    Modified(Item),
+    Removed(Item),
+}
+
+impl Event for RosterReceived {}
+impl Event for RosterPush {}
+
+pub struct RosterPlugin {
+    proxy: PluginProxy,
+    current_version: Mutex<Option<String>>,
+    // TODO: allow for a different backing store.
+    jids: Mutex<HashMap<Jid, Item>>,
+}
+
+impl RosterPlugin {
+    pub fn new(ver: Option<String>) -> RosterPlugin {
+        RosterPlugin {
+            proxy: PluginProxy::new(),
+            current_version: Mutex::new(ver),
+            jids: Mutex::new(HashMap::new()),
+        }
+    }
+
+    // TODO: make that called automatically after plugins are created.
+    pub fn init(&self) {
+        if let Some(disco) = self.proxy.plugin::<DiscoPlugin>() {
+            disco.add_feature(ns::IBB);
+        } else {
+            panic!("Please handle dependencies in the correct order.");
+        }
+    }
+
+    // TODO: make that called automatically before removal.
+    pub fn deinit(&self) {
+        if let Some(disco) = self.proxy.plugin::<DiscoPlugin>() {
+            disco.remove_feature(ns::IBB);
+        } else {
+            panic!("Please handle dependencies in the correct order.");
+        }
+    }
+
+    pub fn send_roster_get(&self, ver: Option<String>) {
+        let iq = Iq {
+            from: None,
+            to: None,
+            id: Some(self.proxy.gen_id()),
+            payload: IqType::Get(Roster {
+                ver,
+                items: vec!(),
+            }.into()),
+        };
+        self.proxy.send(iq.into());
+    }
+
+    // TODO: use a better error type.
+    pub fn send_roster_set(&self, to: Option<Jid>, item: Item) -> Result<(), String> {
+        if item.subscription.is_some() && item.subscription != Some(Subscription::Remove) {
+            return Err(String::from("Subscription must be either nothing or Remove."));
+        }
+        let iq = Iq {
+            from: None,
+            to,
+            id: Some(self.proxy.gen_id()),
+            payload: IqType::Set(Roster {
+                ver: None,
+                items: vec!(item),
+            }.into()),
+        };
+        self.proxy.send(iq.into());
+        Ok(())
+    }
+
+    fn handle_roster_reply(&self, roster: Roster) {
+        // TODO: handle the same-ver case!
+        let mut current_version = self.current_version.lock().unwrap();
+        *current_version = roster.ver;
+        let mut jids = self.jids.lock().unwrap();
+        jids.clear();
+        for item in roster.items {
+            jids.insert(item.jid.clone(), item);
+        }
+        self.proxy.dispatch(RosterReceived {
+            ver: current_version.clone(),
+            jids: jids.clone(),
+        });
+    }
+
+    fn handle_roster_push(&self, roster: Roster) -> Result<(), String> {
+        let item = roster.items.get(0);
+        if item.is_none() || roster.items.len() != 1 {
+            return Err(String::from("Server sent an invalid roster push!"));
+        }
+        let item = item.unwrap().clone();
+        let mut jids = self.jids.lock().unwrap();
+        let previous = jids.insert(item.jid.clone(), item.clone());
+        if previous.is_none() {
+            assert!(item.subscription != Some(Subscription::Remove));
+            self.proxy.dispatch(RosterPush::Added(item));
+        } else {
+            if item.subscription == Some(Subscription::Remove) {
+                self.proxy.dispatch(RosterPush::Removed(item));
+            } else {
+                self.proxy.dispatch(RosterPush::Modified(item));
+            }
+        }
+        Ok(())
+    }
+
+    fn handle_iq(&self, iq: &Iq) -> Propagation {
+        let jid = self.proxy.get_own_jid();
+        let jid = Jid::bare(jid.node.unwrap(), jid.domain);
+        if iq.from.is_some() && iq.from != Some(jid) {
+            // Not from our roster.
+            return Propagation::Continue;
+        }
+        let iq = iq.clone();
+        let id = iq.id.unwrap();
+        match iq.payload {
+            IqType::Result(Some(payload)) => {
+                match IqPayload::try_from(payload) {
+                    Ok(IqPayload::Roster(roster)) => {
+                        self.handle_roster_reply(roster);
+                        Propagation::Stop
+                    },
+                    Ok(_)
+                  | Err(_) => Propagation::Continue,
+                }
+            },
+            IqType::Set(payload) => {
+                match IqPayload::try_from(payload) {
+                    Ok(IqPayload::Roster(roster)) => {
+                        let payload = match self.handle_roster_push(roster) {
+                            Ok(_) => IqType::Result(None),
+                            Err(string) => {
+                                // The specification says that the server should ignore an error.
+                                println!("{}", string);
+                                IqType::Result(None)
+                            },
+                        };
+                        self.proxy.send(Iq {
+                            from: None,
+                            to: None,
+                            id: Some(id),
+                            payload: payload,
+                        }.into());
+                        Propagation::Stop
+                    },
+                    Ok(_)
+                  | Err(_) => return Propagation::Continue,
+                }
+            },
+            IqType::Result(None)
+          | IqType::Get(_)
+          | IqType::Error(_) => {
+                Propagation::Continue
+            },
+        }
+    }
+}
+
+impl_plugin!(RosterPlugin, proxy, [
+    (Iq, Priority::Default) => handle_iq,
+]);