1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

use crate::{
    store::{BlobEntry, StoreError, TABLE_AVATAR},
    Agent, Event,
};
use tokio_xmpp::connect::ServerConnector;
use tokio_xmpp::parsers::{
    avatar::{Data, Metadata},
    iq::Iq,
    ns,
    pubsub::{
        event::Item,
        pubsub::{Items, PubSub},
        NodeName,
    },
    sha1::{Digest, Sha1},
    Jid,
};

pub(crate) async fn handle_metadata_pubsub_event<C: ServerConnector>(
    from: &Jid,
    agent: &mut Agent<C>,
    items: Vec<Item>,
) -> Vec<Event> {
    let mut events = Vec::new();
    for item in items {
        let payload = item.payload.clone().unwrap();
        if payload.is("metadata", ns::AVATAR_METADATA) {
            match Metadata::try_from(payload) {
                Ok(metadata) => {
                    if let Some(avatar_store) = agent.store.table_option(&TABLE_AVATAR).await {
                        for info in metadata.infos {
                            let avatar_id = info.id.to_hex();
                            // Check if entry exists
                            if let Ok(true) = avatar_store.has(&avatar_id).await {
                                let entry = BlobEntry::new(TABLE_AVATAR, avatar_id);
                                events.push(Event::AvatarRetrieved(from.clone(), entry));
                            } else {
                                let iq = download_avatar(from);
                                let _ = agent.client.send_stanza(iq.into()).await;
                            }
                        }
                    }
                }

                Err(e) => error!("Error parsing avatar metadata: {}", e),
            }
        }
    }
    events
}

fn download_avatar(from: &Jid) -> Iq {
    Iq::from_get(
        "coucou",
        PubSub::Items(Items {
            max_items: None,
            node: NodeName(String::from(ns::AVATAR_DATA)),
            subid: None,
            items: Vec::new(),
        }),
    )
    .with_to(from.clone())
}

// The return value of this function will be simply pushed to a Vec in the caller function,
// so it makes no sense to allocate a Vec here - we're lazy instead
// NOTE: This function is called when:
// - in pubsub::handle_iq_result, ns::AVATAR_DATA is matched
// - called by Agent::handle_iq when IqType::Result and ns::PUBSUB is matched
// - called by Agent::wait_for_events when element is <iq>
pub(crate) async fn handle_data_pubsub_iq<'a, C: ServerConnector>(
    agent: &'a mut Agent<C>,
    from: &'a Jid,
    items: &'a Items,
) -> impl IntoIterator<Item = Event> + 'a {
    let from = from.clone();

    let mut events = vec![];
    for item in items.items.iter() {
        match (&item.id, &item.payload) {
            (Some(id), Some(payload)) => {
                let data = Data::try_from(payload.clone()).unwrap();

                // Try to save. We have Ok(Err(hash)) if hashes don't match
                // TODO: error handling
                match try_save_avatar(agent, &id.0, &data.data).await.unwrap() {
                    Ok(blob_entry) => {
                        events.push(Event::AvatarRetrieved(from.clone(), blob_entry));
                    }
                    Err(actual_hash) => {
                        // Hash mismatch
                        warn!(
                            "Received avatar id {} from {} with wrong SHA1 hash: {}",
                            &id.0, &from, actual_hash
                        );
                    }
                }
            }
            _ => {}
        }
    }
    events
}

/// Try save a received avatar to the DataStore
///
/// Returns outer DataStoreError if the store call failed
/// Returns inner String with actual hash if data does not match `sha1_hash`
/// Returns Ok(Ok(TableKey)) when successful.
pub async fn try_save_avatar<C: ServerConnector>(
    agent: &mut Agent<C>,
    sha1_hash: &str,
    data: &[u8],
) -> Result<Result<BlobEntry, String>, StoreError> {
    let mut hasher = Sha1::new();
    hasher.update(data);
    let hasher = hasher.finalize();
    let data_hash = format!("{:x}", hasher);

    if sha1_hash != data_hash {
        return Ok(Err(data_hash));
    }

    let entry = BlobEntry::new(TABLE_AVATAR, sha1_hash.to_string());
    agent.store.set_in_table(&entry, data).await?;

    Ok(Ok(entry))
}