From e59bfa0953f87ad422162ecf7dc1f178878c1b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Moreno?= Date: Wed, 23 Aug 2017 20:25:16 +0200 Subject: [PATCH] Getting default user avatar if it's not defined --- Cargo.toml | 4 ++ src/app.rs | 35 +++++------ src/backend.rs | 154 +++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 143 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9f260a49..09fbc491 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,7 @@ xdg = "2.1.0" [dependencies.gtk] features = ["v3_22"] version = "0.2.0" + +[dependencies.cairo-rs] +version = "0.2.0" +features = ["png"] diff --git a/src/app.rs b/src/app.rs index 9e4dc8fc..b29288e9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -294,30 +294,23 @@ impl AppOp { } fn build_room_msg_avatar(&self, sender: &str) -> gtk::Image { - let avatar_url = match self.members.get(sender) { - Some(m) => m.avatar.clone(), - None => String::new() - }; - let avatar = gtk::Image::new_from_icon_name("image-missing", 5); + let a = avatar.clone(); - if !avatar_url.is_empty() { - let a = avatar.clone(); - - let (tx, rx): (Sender, Receiver) = channel(); - self.backend.get_media_async(avatar_url, tx).unwrap(); - gtk::timeout_add(50, move || { - match rx.try_recv() { - Err(_) => gtk::Continue(true), - Ok(fname) => { - if let Ok(pixbuf) = Pixbuf::new_from_file_at_size(&fname, 32, 32) { - a.set_from_pixbuf(&pixbuf); - } - gtk::Continue(false) + let (tx, rx): (Sender, Receiver) = channel(); + self.backend.get_avatar_async(sender, tx).unwrap(); + gtk::timeout_add(50, move || { + match rx.try_recv() { + Err(_) => gtk::Continue(true), + Ok(fname) => { + if let Ok(pixbuf) = Pixbuf::new_from_file_at_size(&fname, 32, 32) { + a.set_from_pixbuf(&pixbuf); } + gtk::Continue(false) } - }); - } + } + }); + avatar.set_alignment(0.5, 0.); avatar } @@ -404,7 +397,7 @@ impl AppOp { let msg_widget = gtk::Box::new(gtk::Orientation::Horizontal, 5); let content = self.build_room_msg_content(&msg); - msg_widget.pack_start(&avatar, false, true, 5); + msg_widget.pack_start(&avatar, false, false, 5); msg_widget.pack_start(&content, true, true, 0); msg_widget.show_all(); diff --git a/src/backend.rs b/src/backend.rs index 160616f8..751df5a5 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -5,6 +5,7 @@ extern crate xdg; extern crate serde_json; extern crate chrono; extern crate time; +extern crate cairo; use self::regex::Regex; @@ -21,6 +22,9 @@ use std::fs::File; use std::io::prelude::*; use std::io; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; + use self::chrono::prelude::*; use self::time::Duration; @@ -100,6 +104,8 @@ impl From for Error { derror!(url::ParseError, Error::BackendError); derror!(io::Error, Error::BackendError); derror!(regex::Error, Error::BackendError); +derror!(cairo::Status, Error::BackendError); +derror!(cairo::IoError, Error::BackendError); pub struct BackendData { user_id: String, @@ -339,22 +345,22 @@ impl Backend { pub fn get_room_avatar(&self, roomid: String) -> Result<(), Error> { let baseu = self.get_base_url()?; let tk = self.data.lock().unwrap().access_token.clone(); - let roomu = baseu.join("/_matrix/client/r0/rooms/")?.join(&(roomid + "/"))?; + let userid = self.data.lock().unwrap().user_id.clone(); + let roomu = baseu.join("/_matrix/client/r0/rooms/")?.join(&(roomid.clone() + "/"))?; let mut url = roomu.join("state/m.room.avatar")?; url = url.join(&format!("?access_token={}", tk))?; let tx = self.tx.clone(); get!(&url, |r: JsonValue| { - let mut avatar = String::from(""); + let avatar; match r["url"].as_str() { Some(u) => { - avatar = thumb!(&baseu, u).unwrap(); + avatar = thumb!(&baseu, u).unwrap_or(String::from("")); }, None => { - // TODO: use identicon API - // /_matrix/media/v1/identicon/$ident + avatar = get_room_avatar(&baseu, &tk, &userid, &roomid).unwrap_or(String::from("")); } } tx.send(BKResponse::RoomAvatar(avatar)).unwrap(); @@ -472,6 +478,20 @@ impl Backend { Ok(()) } + + pub fn get_avatar_async(&self, uid: &str, tx: Sender) -> Result<(), Error> { + let baseu = self.get_base_url()?; + + let u = String::from(uid); + thread::spawn(move || { + match get_user_avatar(&baseu, &u) { + Ok(fname) => { tx.send(fname).unwrap(); }, + Err(_) => { tx.send(String::from("")).unwrap(); } + }; + }); + + Ok(()) + } } fn get_rooms_from_json(r: JsonValue, userid: &str) -> Result, Error> { @@ -568,17 +588,22 @@ fn json_q(method: &str, url: &Url, attrs: &HashMap) -> Result Result { - let id = format!("{}/", userid); - let url = baseu.join("/_matrix/client/r0/profile/")?.join(&id)?.join("avatar_url")?; + let url = baseu.join("/_matrix/client/r0/profile/")?.join(userid)?; let attrs: HashMap = HashMap::new(); match json_q("get", &url, &attrs) { Ok(js) => { - let url = String::from(js["avatar_url"].as_str().unwrap_or("")); - let fname = thumb!(baseu, &url)?; - Ok(fname) + match js["avatar_url"].as_str() { + Some(url) => Ok(thumb!(baseu, &url)?), + None => { + let name = js["displayname"].as_str().unwrap_or("@"); + Ok(draw_identicon(userid, String::from(name))?) + }, + } }, - Err(_) => { Err(Error::BackendError) } + Err(_) => { + Ok(draw_identicon(userid, String::from(&userid[1..2]))?) + } } } @@ -592,14 +617,91 @@ fn get_room_st(base: &Url, tk: &str, roomid: &str) -> Result { } fn get_room_avatar(base: &Url, tk: &str, userid: &str, roomid: &str) -> Result { - Ok(String::from("TODO")) + let st = get_room_st(base, tk, roomid)?; + let events = st.as_array().ok_or(Error::BackendError)?; + + // we look for members that aren't me + let filter = |x: &&JsonValue| { + (x["type"] == "m.room.member" && + x["content"]["membership"] == "join" && + x["sender"] != userid) + }; + let members = events.iter().filter(&filter); + let mut members2 = events.iter().filter(&filter); + + let m1 = match members2.nth(0) { + Some(m) => m["content"]["avatar_url"].as_str().unwrap_or(""), + None => "" + }; + + let mut fname = match members.count() { + 1 => thumb!(&base, m1).unwrap_or(String::new()), + _ => {String::new()}, + }; + + if fname.is_empty() { + let roomname = calculate_room_name(&st, userid)?; + fname = draw_identicon(roomid, roomname)?; + } + + Ok(fname) } +struct Color { + r: i32, + g: i32, + b: i32, +} -fn get_room_name(base: &Url, tk: &str, userid: &str, roomid: &str) -> Result { - let st = get_room_st(base, tk, roomid)?; - let rname = calculate_room_name(&st, userid)?; - Ok(rname) +fn calculate_hash(t: &T) -> u64 { + let mut s = DefaultHasher::new(); + t.hash(&mut s); + s.finish() +} + +fn draw_identicon(fname: &str, name: String) -> Result { + let colors = vec![ + Color{r: 69, g: 189, b: 243}, + Color{r: 224, g: 143, b: 112}, + Color{r: 77, g: 182, b: 172}, + Color{r: 149, g: 117, b: 205}, + Color{r: 176, g: 133, b: 94}, + Color{r: 240, g: 98, b: 146}, + Color{r: 163, g: 211, b: 108}, + Color{r: 121, g: 134, b: 203}, + Color{r: 241, g: 185, b: 29}, + ]; + + let xdg_dirs = xdg::BaseDirectories::with_prefix("guillotine").unwrap(); + let fname = String::from(xdg_dirs.place_cache_file(fname)?.to_str().ok_or(Error::BackendError)?); + + let image = cairo::ImageSurface::create(cairo::Format::ARgb32, 40, 40)?; + let g = cairo::Context::new(&image); + + let c = &colors[calculate_hash(&fname) as usize % colors.len() as usize]; + g.set_source_rgba(c.r as f64 / 256., + c.g as f64 / 256., + c.b as f64 / 256., 1.); + g.rectangle(0., 0., 40., 40.); + g.fill(); + + g.set_font_size(24.); + g.set_source_rgb(1.0, 1.0, 1.0); + + let first = match &name.chars().nth(0) { + &Some(f) if f == '#' => String::from(&name.to_uppercase()[1..2]), + &Some(_) => String::from(&name.to_uppercase()[0..1]), + &None => String::from("X"), + }; + + let te = g.text_extents(&first); + g.move_to(20. - te.width / 2., 20. + te.height / 2.); + g.show_text(&first); + + let mut buffer = File::create(&fname)?; + image.write_to_png(&mut buffer)?; + + Ok(fname) } fn calculate_room_name(roomst: &JsonValue, userid: &str) -> Result { @@ -615,19 +717,13 @@ fn calculate_room_name(roomst: &JsonValue, userid: &str) -> Result m["content"]["displayname"].as_str().unwrap_or(""),