avatar.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
 7use crate::Event;
 8use futures::{sync::mpsc, Sink};
 9use std::convert::TryFrom;
10use std::fs::{self, File};
11use std::io::{self, Write};
12use tokio_xmpp::Packet;
13use xmpp_parsers::{
14    avatar::{Data, Metadata},
15    iq::Iq,
16    ns,
17    pubsub::{
18        event::Item,
19        pubsub::{Items, PubSub},
20        NodeName,
21    },
22    hashes::Hash,
23    Jid,
24};
25
26// TODO: Update xmpp-parsers to get this function for free on Hash.
27fn hash_to_hex(hash: &Hash) -> String {
28    let mut bytes = vec![];
29    for byte in hash.hash.iter() {
30        bytes.push(format!("{:02x}", byte));
31    }
32    bytes.join("")
33}
34
35pub(crate) fn handle_metadata_pubsub_event(from: &Jid, tx: &mut mpsc::UnboundedSender<Packet>, items: Vec<Item>) -> impl IntoIterator<Item = Event> {
36    let mut events = Vec::new();
37    for item in items {
38        let payload = item.payload.clone().unwrap();
39        if payload.is("metadata", ns::AVATAR_METADATA) {
40            let metadata = Metadata::try_from(payload).unwrap();
41            for info in metadata.infos {
42                let filename = format!("data/{}/{}", from, hash_to_hex(&*info.id));
43                let file_length = match fs::metadata(filename.clone()) {
44                    Ok(metadata) => metadata.len(),
45                    Err(_) => 0,
46                };
47                // TODO: Also check the hash.
48                if info.bytes as u64 == file_length {
49                    events.push(Event::AvatarRetrieved(from.clone(), filename));
50                } else {
51                    let iq = download_avatar(from);
52                    tx.start_send(Packet::Stanza(iq.into())).unwrap();
53                }
54            }
55        }
56    }
57    events
58}
59
60fn download_avatar(from: &Jid) -> Iq {
61    Iq::from_get("coucou", PubSub::Items(Items {
62        max_items: None,
63        node: NodeName(String::from(ns::AVATAR_DATA)),
64        subid: None,
65        items: Vec::new(),
66    }))
67    .with_to(from.clone())
68}
69
70// The return value of this function will be simply pushed to a Vec in the caller function,
71// so it makes no sense to allocate a Vec here - we're lazy instead
72pub(crate) fn handle_data_pubsub_iq<'a>(
73    from: &'a Jid,
74    items: &'a Items,
75) -> impl IntoIterator<Item = Event> + 'a {
76    let from = from.clone();
77    items
78        .items
79        .iter()
80        .filter_map(move |item| match (&item.id, &item.payload) {
81            (Some(id), Some(payload)) => {
82                let data = Data::try_from(payload.clone()).unwrap();
83                let filename = save_avatar(&from, id.0.clone(), &data.data).unwrap();
84                Some(Event::AvatarRetrieved(from.clone(), filename))
85            }
86            _ => None,
87        })
88}
89
90fn save_avatar(from: &Jid, id: String, data: &[u8]) -> io::Result<String> {
91    let directory = format!("data/{}", from);
92    let filename = format!("data/{}/{}", from, id);
93    fs::create_dir_all(directory)?;
94    let mut file = File::create(&filename)?;
95    file.write_all(data)?;
96    Ok(filename)
97}