From 60c22a282e1a8239b41b702c4ba7a269961c318f Mon Sep 17 00:00:00 2001 From: xmppftw Date: Tue, 4 Jun 2024 19:20:54 +0200 Subject: [PATCH] Load avatars from xmpp --- src/main.rs | 38 +++++++++++++----- src/tab.rs | 5 ++- src/widgets/chat_tab.rs | 16 ++++++-- src/xmpp_client.rs | 85 ++++++++++++++++++++++++++++++++++------- 4 files changed, 115 insertions(+), 29 deletions(-) diff --git a/src/main.rs b/src/main.rs index fa8c30d..3748822 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,21 +55,32 @@ fn main() { .build(); let tabs_store = gio::ListStore::new::(); - - let (xmpp_receiver, cmd_sender) = xmpp_client::client(&username, &password); let tabs_store_copy = tabs_store.clone(); + let (xmpp_receiver, cmd_sender) = xmpp_client::client(&username, &password); glib::spawn_future_local(async move { while let Ok(event) = xmpp_receiver.recv().await { match event { - xmpp::Event::ContactAdded(jid) => { - let tab = Tab::new(jid.jid.as_str(), jid.jid.as_str()); + xmpp_client::XMPPEvent::Avatar(jid, avatar) => { + log::info!("AVATAR"); + if let Some(tab) = tabs_store_copy.iter().find(|tab| { + let tab: &glib::object::Object = tab.as_ref().unwrap(); + let tab: &Tab = tab.downcast_ref().unwrap(); + tab.jid() == jid.as_str() + }) { + let tab: &Tab = tab.as_ref().unwrap().downcast_ref().unwrap(); + tab.set_avatar_hash(avatar.clone()); + } + } + xmpp_client::XMPPEvent::Contact(jid, avatar) => { + log::info!("CONTACT"); + let tab = Tab::new(jid.as_str(), jid.as_str()); + + if let Some(avatar) = avatar { + tab.set_avatar_hash(avatar); + } + tabs_store_copy.append(&tab); } - xmpp::Event::ChatMessage(_id, _from, _body, _time) => { - // TODO: Insert message into tab history - continue; - } - _ => continue, } } }); @@ -128,9 +139,18 @@ fn main() { list_item.set_child(Some(&tab_widget)); }); tabs_factory.connect_bind(move |_, list_item| { + // executed when switch tab, one time for previous tab and one time for new tab + // but also when create new tab let list_item: >k::ListItem = list_item.downcast_ref().unwrap(); let tab: Tab = list_item.item().and_downcast().unwrap(); let tab_widget: widgets::ChatTab = list_item.child().and_downcast().unwrap(); + tab.bind_property( + "avatar_hash", + &tab_widget.imp().avatar.try_get().unwrap(), + "file", + ) + .sync_create() + .build(); tab_widget.set_name(&tab.name()); tab_widget.set_jid(&tab.jid()); diff --git a/src/tab.rs b/src/tab.rs index 5ee3f9e..dc1a307 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -25,8 +25,8 @@ mod imp { #[derive(glib::Properties)] #[properties(wrapper_type = super::Tab)] pub struct Tab { - //#[property(get, set)] - //avatar: RefCell, + #[property(get, set)] + avatar_hash: RefCell>, #[property(get, construct_only)] jid: RefCell, #[property(get, set)] @@ -36,6 +36,7 @@ mod imp { impl Default for Tab { fn default() -> Self { Tab { + avatar_hash: RefCell::new(None), jid: RefCell::new(String::new()), name: RefCell::new(String::new()), } diff --git a/src/widgets/chat_tab.rs b/src/widgets/chat_tab.rs index cee8853..9e0f7b2 100644 --- a/src/widgets/chat_tab.rs +++ b/src/widgets/chat_tab.rs @@ -68,13 +68,21 @@ impl ChatTab { } pub fn set_jid(&self, jid: &str) { - let hash = "123456789abcdef123456789abcdef123456789a"; - self.imp().avatar.set_from_file(Some(format!( - "/home/linkmauve/cache/poezio/avatars/{jid}/{hash}" - ))); self.set_tooltip_text(Some(jid)); } + pub fn set_avatar_hash(&self, hash: &str) { + // let xdg_cache = std::env::var("XDG_CACHE_HOME").unwrap_or(format!( + // "{}/.cache", + // std::env::var("HOME").expect("NO $HOME?!") + // )); + // let xdg_cache = PathBuf::from(xdg_cache); + // let entry = xdg_cache.join("poezio/avatars").join(self.tooltip_text().unwrap()).join(hash); + + // self.imp().avatar.set_from_file(Some(entry.to_str().unwrap().to_string())); + self.imp().avatar.set_from_file(Some(hash)); + } + pub fn set_name(&self, name: &str) { self.imp().name.set_label(name); } diff --git a/src/xmpp_client.rs b/src/xmpp_client.rs index 7fe350c..d9c51e5 100644 --- a/src/xmpp_client.rs +++ b/src/xmpp_client.rs @@ -1,7 +1,8 @@ use async_channel::{Receiver, Sender}; use tokio::runtime::Runtime; -use xmpp::{BareJid, ClientBuilder, ClientFeature, Event}; +use xmpp::{BareJid, ClientBuilder, ClientFeature}; +use std::path::PathBuf; use std::sync::OnceLock; #[derive(Clone, Debug)] @@ -9,13 +10,76 @@ pub enum XMPPCommand { SendPM(BareJid, String), } -fn tokio_runtime() -> &'static Runtime { +#[derive(Clone, Debug)] +pub enum XMPPEvent { + Avatar(BareJid, String), + Contact(BareJid, Option), +} + +pub fn tokio_runtime() -> &'static Runtime { static RUNTIME: OnceLock = OnceLock::new(); RUNTIME.get_or_init(|| Runtime::new().expect("Setting up tokio runtime needs to succeed.")) } -pub(crate) fn client(jid: &str, password: &str) -> (Receiver, Sender) { - let (event_sender, event_receiver) = async_channel::bounded::(1); +pub async fn handle_xmpp_event(event: xmpp::Event) -> Option { + match event { + xmpp::Event::AvatarRetrieved(jid, hash) => { + // need update latest symlink? + // xmpp-rs stores avatar in data/JID/HASH... we create data/JID/latest symlink points to relative HASH + + let mut latest_link = PathBuf::from(&hash); + let hash_name = latest_link + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(); + latest_link.set_file_name("latest"); + + if let Ok(metadata) = tokio::fs::metadata(&latest_link).await { + if metadata.is_symlink() { + if let Ok(previous_latest) = tokio::fs::read_link(&latest_link).await { + if previous_latest == PathBuf::from(&hash_name) { + // We already have latest symlink + log::info!("avatar Already have latest link {jid} to hash {hash}"); + return None; + } + } + } + } + + // Setting new latest symlink + let _ = tokio::fs::symlink(hash_name, latest_link).await; + + if jid.is_bare() { + Some(XMPPEvent::Avatar(jid.to_bare(), hash)) + } else { + // no avatar for FullJid (yet) + None + } + } + xmpp::Event::ContactAdded(jid) => { + let avatar_jid = jid.jid.clone(); + if let Ok(hash_name) = + tokio::fs::read_link(&format!("data/{}/latest", &avatar_jid)).await + { + let avatar = format!("data/{}/{}", &avatar_jid, hash_name.display()); + log::info!("Found existing avatar for contact: {avatar_jid}"); + Some(XMPPEvent::Contact(avatar_jid, Some(avatar))) + } else { + Some(XMPPEvent::Contact(avatar_jid, None)) + } + } + xmpp::Event::ChatMessage(_id, _from, _body, _time) => { + // TODO: Insert message into tab history + None + } + _ => None, + } +} + +pub(crate) fn client(jid: &str, password: &str) -> (Receiver, Sender) { + let (event_sender, event_receiver) = async_channel::bounded::(1); let (cmd_sender, cmd_receiver) = async_channel::bounded::(1); let jid = jid.to_string(); @@ -24,6 +88,7 @@ pub(crate) fn client(jid: &str, password: &str) -> (Receiver, Sende tokio_runtime().spawn(async move { let mut client = ClientBuilder::new(BareJid::new(&jid).unwrap(), &password) .set_default_nick("xmpp-client") + .enable_feature(ClientFeature::Avatars) .enable_feature(ClientFeature::ContactList) .build(); @@ -31,16 +96,8 @@ pub(crate) fn client(jid: &str, password: &str) -> (Receiver, Sende tokio::select! { Some(events) = client.wait_for_events() => { for event in events { - match event { - Event::ContactAdded(_) => { - event_sender.send(event).await.expect("BOOOOOOHOOOO"); - } - Event::ChatMessage(_, _, _, _) => { - event_sender.send(event).await.expect("BOOOHOOOO"); - } - _ => { - continue; - } + if let Some(parsed_event) = handle_xmpp_event(event).await { + let _ = event_sender.send(parsed_event).await; } } }