diff --git a/Cargo.lock b/Cargo.lock index 717c981..0347d5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,6 +285,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "aead" version = "0.4.1" @@ -372,8 +378,10 @@ dependencies = [ "html-escape", "html-minifier", "if-addrs", + "image", "lazy_static", "libmdns", + "linked-hash-map", "multicast_dns", "platform-dirs", "rand 0.7.3", @@ -395,9 +403,9 @@ dependencies = [ [[package]] name = "async-psec" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ab60657d7b3949c3bdb06570fc4855a4e83dbe6c9da73902c67355dd8f37a8" +checksum = "31dcd9dc064b59d84e03e9c42c97ca8bba139ff04a874afc0e93944d7f62e235" dependencies = [ "aes-gcm", "async-trait", @@ -510,6 +518,12 @@ version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" +[[package]] +name = "bytemuck" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58" + [[package]] name = "byteorder" version = "1.4.3" @@ -564,6 +578,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "const_fn" version = "0.4.7" @@ -633,6 +653,51 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "lazy_static", +] + [[package]] name = "crypto-mac" version = "0.11.0" @@ -665,6 +730,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + [[package]] name = "derive_more" version = "0.99.14" @@ -809,7 +884,7 @@ dependencies = [ "cfg-if 1.0.0", "crc32fast", "libc", - "miniz_oxide", + "miniz_oxide 0.4.4", ] [[package]] @@ -1004,6 +1079,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "h2" version = "0.2.7" @@ -1173,6 +1258,25 @@ dependencies = [ "libc", ] +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + [[package]] name = "indexmap" version = "1.6.2" @@ -1228,6 +1332,15 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.51" @@ -1351,6 +1464,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +[[package]] +name = "memoffset" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.16" @@ -1366,6 +1488,15 @@ dependencies = [ "macro-utils", ] +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + [[package]] name = "miniz_oxide" version = "0.4.4" @@ -1518,6 +1649,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -1706,6 +1859,18 @@ dependencies = [ "dirs-next", ] +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + [[package]] name = "polyval" version = "0.5.0" @@ -1840,6 +2005,31 @@ dependencies = [ "rand_core 0.6.2", ] +[[package]] +name = "rayon" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.8" @@ -1954,6 +2144,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.1.0" @@ -2265,6 +2461,17 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + [[package]] name = "time" version = "0.2.26" @@ -2664,6 +2871,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "weezl" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" + [[package]] name = "widestring" version = "0.4.3" diff --git a/Cargo.toml b/Cargo.toml index 00f0c62..c57cc70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,8 +36,10 @@ if-addrs = "0.6" base64 = "0.13" scrypt = "0.7" zeroize = "1.2" +image = "0.23" yaml-rust = "0.4" #only in debug mode [build-dependencies] html-minifier = "3.0" -yaml-rust = "0.4" \ No newline at end of file +yaml-rust = "0.4" +linked-hash-map = "0.5" \ No newline at end of file diff --git a/build.rs b/build.rs index 194a168..e3b726d 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,9 @@ #[cfg(not(debug_assertions))] -use std::{env, fs::{File, read_to_string, create_dir}, path::Path, io::{Write, ErrorKind}}; +use { + std::{env, fs::{File, read_to_string, create_dir}, path::Path, io::{Write, ErrorKind}}, + yaml_rust::{YamlLoader, Yaml}, + linked_hash_map::LinkedHashMap, +}; #[cfg(not(debug_assertions))] fn minify_content(content: &str, language: &str) -> Option { @@ -11,10 +15,15 @@ fn minify_content(content: &str, language: &str) -> Option { } } +#[cfg(not(debug_assertions))] +fn replace_fields(content: &mut String, fields: &LinkedHashMap) { + fields.into_iter().for_each(|field| { + *content = content.replace(field.0.as_str().unwrap(), field.1.as_str().unwrap()); + }); +} + #[cfg(not(debug_assertions))] fn generate_web_files() { - use yaml_rust::YamlLoader; - let out_dir = env::var("OUT_DIR").unwrap(); let out_dir = Path::new(&out_dir); let src_dir = Path::new("src/frontend"); @@ -26,7 +35,7 @@ fn generate_web_files() { } let config = &YamlLoader::load_from_str(&read_to_string("config.yml").unwrap()).unwrap()[0]; - let css_values = config["css"].as_hash().unwrap(); + let fields = config.as_hash().unwrap(); [ "login.html", @@ -40,9 +49,7 @@ fn generate_web_files() { let extension = path.extension().unwrap().to_str().unwrap(); let mut content = read_to_string(src_dir.join(path)).unwrap(); if extension == "css" { - css_values.into_iter().for_each(|entry| { - content = content.replace(entry.0.as_str().unwrap(), entry.1.as_str().unwrap()); - }); + replace_fields(&mut content, fields); } if file_name == &"index.html" { content = content.replace("AIRA_VERSION", env!("CARGO_PKG_VERSION")); @@ -51,6 +58,10 @@ fn generate_web_files() { let mut dst = File::create(out_dir.join(path)).unwrap(); dst.write(minified_content.as_bytes()).unwrap(); }); + + let mut text_avatar = read_to_string("src/frontend/imgs/text_avatar.svg").unwrap(); + replace_fields(&mut text_avatar, fields); + File::create(out_dir.join("text_avatar.svg")).unwrap().write(text_avatar.as_bytes()).unwrap(); } fn main() { diff --git a/config.yml b/config.yml index 67a14f4..85ab221 100644 --- a/config.yml +++ b/config.yml @@ -1,2 +1 @@ -css: - ACCENT_COLOR: "19a52c" \ No newline at end of file +ACCENT_COLOR: "19a52c" \ No newline at end of file diff --git a/src/frontend/commons/script.js b/src/frontend/commons/script.js index 5a89c6b..278c951 100644 --- a/src/frontend/commons/script.js +++ b/src/frontend/commons/script.js @@ -1,13 +1,17 @@ -function generateAvatar(name) { - let span = document.createElement("span"); - if (typeof name == "undefined") { - span.appendChild(document.createTextNode("?")); - } else if (name.length > 0) { - span.appendChild(document.createTextNode(name[0].toUpperCase())); - } - let div = document.createElement("div"); - div.classList.add("avatar"); - div.appendChild(span); - div.appendChild(document.createElement("div")); //used for background - return div; +function generateImgAvatar() { + let img = document.createElement("img"); + img.classList.add("avatar"); + return img; +} + +function generateSelfAvatar(timestamp) { + let img = generateImgAvatar(); + img.src = "/avatar/self?"+timestamp; + return img; +} + +function generateAvatar(sessionId, name, timestamp) { + let img = generateImgAvatar(); + img.src = "/avatar/"+sessionId+"/"+name+"?"+timestamp; + return img; } \ No newline at end of file diff --git a/src/frontend/commons/style.css b/src/frontend/commons/style.css index 14bf268..82224f7 100644 --- a/src/frontend/commons/style.css +++ b/src/frontend/commons/style.css @@ -26,24 +26,8 @@ input[type="text"], input[type="password"] { } .avatar { - position: relative; margin-right: .5em; width: 1.5em; height: 1.5em; -} -.avatar div { - width: 100%; - height: 100%; - background-color: var(--accent); - border-radius: 100%; -} -.avatar span { - position: absolute; - z-index: 1; - display: flex; - align-items: center; - justify-content: center; - height: 100%; - width: 100%; - margin: 0; + border-radius: 50%; } \ No newline at end of file diff --git a/src/frontend/imgs/text_avatar.svg b/src/frontend/imgs/text_avatar.svg new file mode 100644 index 0000000..1c7a3a0 --- /dev/null +++ b/src/frontend/imgs/text_avatar.svg @@ -0,0 +1,4 @@ + + + LETTER + \ No newline at end of file diff --git a/src/frontend/index.css b/src/frontend/index.css index c70c700..9ef469a 100644 --- a/src/frontend/index.css +++ b/src/frontend/index.css @@ -67,7 +67,7 @@ input[type="file"] { left: 0; right: 0; margin: auto; - width: 40%; + width: 40vw; max-height: 90vh; overflow: auto; box-sizing: border-box; @@ -102,29 +102,6 @@ input[type="file"] { content: url("/static/imgs/icons/cancel"); background-color: unset; } -.popup .avatar { - font-size: 4em; - margin: auto; -} -.popup section { - display: block; - margin-bottom: 20px; - border-top: 1px solid black; -} -.popup section:first-of-type { - border-top: unset; -} -.popup input { - display: block; - margin: 10px; -} -.popup span { - font-weight: bold; -} -.popup>div>div p { - font-weight: normal; - font-size: 0.9em; -} .popup h2.warning::before { content: url("/static/imgs/icons/warning/ACCENT_COLOR"); width: 9%; @@ -187,6 +164,57 @@ label { .switch input:checked + span:before { transform: translateX(26px); } +#avatarContainer { + display: flex; + justify-content: center; +} +#avatarContainer .avatar { + font-size: 4em; + margin-right: unset; +} +#avatarContainer label:hover .avatar { + opacity: .4; +} +#avatarContainer>label { + position: relative; +} +#avatarContainer .avatar + p { + display: none; + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 0; + right: 0; + margin: auto; + text-align: center; +} +#avatarContainer label:hover p { + display: block; +} +#profile_info section { + display: block; + margin-bottom: 20px; + border-top: 1px solid black; +} +#profile_info section:first-of-type { + border-top: unset; +} +#profile_info input { + margin: 10px; +} +#profile_info span { + font-weight: bold; +} +#profile_info>div>div p { + font-weight: normal; + font-size: 0.9em; +} +#session_info .avatar { + width: 6em; + height: 6em; + display: block; + margin: auto; +} #session_info .name { display: flex; justify-content: center; diff --git a/src/frontend/index.js b/src/frontend/index.js index c6e29ab..8de35d3 100644 --- a/src/frontend/index.js +++ b/src/frontend/index.js @@ -8,6 +8,9 @@ let currentSessionId = -1; let sessionsData = new Map(); let msgHistory = new Map(); let pendingFilesTransfers = new Map(); +let avatarTimestamps = new Map([ + ["self", Date.now()] +]); function onClickSession(event) { let sessionId = event.currentTarget.getAttribute("data-sessionId"); @@ -75,7 +78,7 @@ document.getElementById("delete_conversation").onclick = function() { showPopup(mainDiv); }; document.getElementById("add_contact").onclick = function() { - socket.send("contact "+currentSessionId+" "+sessionsData.get(currentSessionId).name); + socket.send("contact "+currentSessionId); sessionsData.get(currentSessionId).isContact = true; displayHeader(); displaySessions(); @@ -234,8 +237,47 @@ msg_log.onscroll = function() { let profile_div = document.querySelector("#me>div"); profile_div.onclick = function() { let mainDiv = document.createElement("div"); - let avatar = generateAvatar(identityName); - mainDiv.appendChild(avatar); + mainDiv.id = "profile_info"; + let avatarContainer = document.createElement("div"); + avatarContainer.id = "avatarContainer"; + let labelAvatar = document.createElement("label"); + labelAvatar.setAttribute("for", "avatar_input"); + let inputAvatar = document.createElement("input"); + inputAvatar.type = "file"; + inputAvatar.id = "avatar_input"; + inputAvatar.onchange = function(event) { + let file = event.target.files[0]; + if (file.size < 10000000) { + let formData = new FormData(); + formData.append("avatar", file); + fetch("/set_avatar", {method: "POST", body: formData}).then(response => { + if (response.ok) { + avatarTimestamps.set("self", Date.now()); + document.querySelector("#avatarContainer .avatar").src = "/avatar/self?"+avatarTimestamps.get("self"); + displayProfile(); + if (currentSessionId != -1) { + displayHistory(); + } + } else { + console.log(response); + } + }); + } else { + let mainDiv = document.createElement("div"); + mainDiv.appendChild(generatePopupWarningTitle()); + let p = document.createElement("p"); + p.textContent = "Avatar cannot be larger than 10MB."; + mainDiv.appendChild(p); + showPopup(mainDiv); + } + }; + labelAvatar.appendChild(inputAvatar); + labelAvatar.appendChild(generateSelfAvatar(avatarTimestamps.get("self"))); + let uploadP = document.createElement("p"); + uploadP.textContent = "Upload"; + labelAvatar.appendChild(uploadP); + avatarContainer.appendChild(labelAvatar); + mainDiv.appendChild(avatarContainer); let fingerprint = document.createElement("pre"); fingerprint.id = "identity_fingerprint"; fingerprint.textContent = beautifyFingerprint(identityFingerprint); @@ -427,8 +469,8 @@ socket.onmessage = function(msg) { console.log("Message: "+msg.data); let args = msg.data.split(" "); switch (args[0]) { - case "disconnected": - onDisconnected(args[1]); + case "inc_file_transfer": + onIncFilesTransfer(args[1], parseInt(args[2])); break; case "new_session": onNewSession(args[1], args[2] === "true", args[3], args[4], msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+args[4].length+5)); @@ -451,15 +493,15 @@ socket.onmessage = function(msg) { case "aborted": onFilesTransferAborted(args[1]); break; - case "inc_file_transfer": - onIncFilesTransfer(args[1], parseInt(args[2])); - break; case "load_msgs": onMsgsLoad(args[1], msg.data.slice(args[0].length+args[1].length+2)); break; case "name_told": onNameTold(args[1], msg.data.slice(args[0].length+args[1].length+2)); break; + case "avatar_set": + onAvatarSet(args[1]); + break; case "is_contact": onIsContact(args[1], args[2] === "true", args[3], msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+4)); break; @@ -475,6 +517,9 @@ socket.onmessage = function(msg) { case "password_changed": onPasswordChanged(args[1] === "true", args[2] === "true"); break; + case "disconnected": + onDisconnected(args[1]); + break; case "logout": logout(); } @@ -509,6 +554,14 @@ function onNameTold(sessionId, name) { } displaySessions(); } +function onAvatarSet(sessionId) { + avatarTimestamps.set(sessionId, Date.now()); + displaySessions(); + if (sessionId === currentSessionId) { + displayHeader(); + displayHistory(false); + } +} function setNotSeen(strSessionIds) { let sessionIds = strSessionIds.split(' '); for (let i=0; i { let name = undefined; + let sessionId = undefined; if (previousOutgoing != entry[0]) { previousOutgoing = entry[0]; if (entry[0]) { //outgoing msg name = identityName; } else { name = session.name; + sessionId = currentSessionId; } } if (entry[1]) { //is file - msg_log.appendChild(generateFile(name, entry[0], entry[2])); + msg_log.appendChild(generateFile(name, sessionId, entry[0], entry[2])); } else { - msg_log.appendChild(generateMessage(name, entry[2])); + msg_log.appendChild(generateMessage(name, sessionId, entry[2])); } }); if (scrollToBottom) { diff --git a/src/frontend/login.html b/src/frontend/login.html index 07a9c23..0fbb3c9 100644 --- a/src/frontend/login.html +++ b/src/frontend/login.html @@ -63,9 +63,9 @@ padding: 10px 50px; } .avatar { - width: 2em; - height: 2em; - font-size: 3em; + width: 7em; + height: 7em; + display: block; margin: auto; } #identity h2 { @@ -157,7 +157,7 @@ } } else { let identity = document.getElementById("identity"); - identity.appendChild(generateAvatar(identity_name)); + identity.appendChild(generateSelfAvatar(Date.now())); let h2 = document.createElement("h2"); h2.textContent = identity_name; identity.appendChild(h2); diff --git a/src/identity.rs b/src/identity.rs index 41a090a..9925208 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -11,6 +11,7 @@ use crate::{constants, crypto, key_value_table::KeyValueTable, print_error, util const MAIN_TABLE: &str = "main"; const CONTACTS_TABLE: &str = "contacts"; const FILES_TABLE: &str = "files"; +const AVATARS_TABLE: &str = "avatars"; const DATABASE_CORRUPED_ERROR: &str = "Database corrupted"; @@ -21,6 +22,7 @@ impl<'a> DBKeys { pub const SALT: &'a str = "salt"; pub const MASTER_KEY: &'a str = "master_key"; pub const USE_PADDING: &'a str = "use_padding"; + pub const AVATAR: &'a str = "avatar"; } fn bool_to_byte(b: bool) -> u8 { @@ -53,6 +55,7 @@ pub struct Contact { pub uuid: Uuid, pub public_key: [u8; PUBLIC_KEY_LENGTH], pub name: String, + pub avatar: Option, pub verified: bool, pub seen: bool, } @@ -70,19 +73,23 @@ impl Identity { self.keypair.public.to_bytes() } - pub fn add_contact(&self, name: String, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result { + pub fn add_contact(&self, name: String, avatar_uuid: Option, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result { let db = Connection::open(get_database_path())?; - db.execute(&("CREATE TABLE IF NOT EXISTS ".to_owned()+CONTACTS_TABLE+"(uuid BLOB PRIMARY KEY, name BLOB, key BLOB, verified BLOB, seen BLOB)"), [])?; + db.execute(&("CREATE TABLE IF NOT EXISTS ".to_owned()+CONTACTS_TABLE+"(uuid BLOB PRIMARY KEY, name BLOB, avatar BLOB, key BLOB, verified BLOB, seen BLOB)"), [])?; let contact_uuid = Uuid::new_v4(); let encrypted_name = crypto::encrypt_data(name.as_bytes(), &self.master_key).unwrap(); let encrypted_public_key = crypto::encrypt_data(&public_key, &self.master_key).unwrap(); let encrypted_verified = crypto::encrypt_data(&[bool_to_byte(false)], &self.master_key).unwrap(); let encrypted_seen = crypto::encrypt_data(&[bool_to_byte(true)], &self.master_key).unwrap(); - db.execute(&("INSERT INTO ".to_owned()+CONTACTS_TABLE+" (uuid, name, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5)"), params![&contact_uuid.as_bytes()[..], encrypted_name, encrypted_public_key, encrypted_verified, encrypted_seen])?; + match avatar_uuid { + Some(avatar_uuid) => db.execute(&format!("INSERT INTO {} (uuid, name, avatar, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..], encrypted_name, &avatar_uuid.as_bytes()[..], encrypted_public_key, encrypted_verified, encrypted_seen])?, + None => db.execute(&format!("INSERT INTO {} (uuid, name, key, verified, seen) VALUES (?1, ?2, ?3, ?4, ?5)", CONTACTS_TABLE), params![&contact_uuid.as_bytes()[..], encrypted_name, encrypted_public_key, encrypted_verified, encrypted_seen])? + }; Ok(Contact { uuid: contact_uuid, public_key: public_key, name: name, + avatar: avatar_uuid, verified: false, seen: true, }) @@ -91,7 +98,7 @@ impl Identity { pub fn remove_contact(uuid: &Uuid) -> Result { let db = Connection::open(get_database_path())?; Identity::delete_conversation(uuid)?; - db.execute(&("DELETE FROM ".to_owned()+CONTACTS_TABLE+" WHERE uuid=?"), [&uuid.as_bytes()[..]]) + db.execute(&format!("DELETE FROM {} WHERE uuid=?", CONTACTS_TABLE), [&uuid.as_bytes()[..]]) } pub fn set_verified(&self, uuid: &Uuid) -> Result { @@ -106,6 +113,11 @@ impl Identity { db.execute(&format!("UPDATE {} SET name=?1 WHERE uuid=?2", CONTACTS_TABLE), [encrypted_name.as_slice(), uuid.as_bytes()]) } + pub fn set_contact_avatar(&self, contact_uuid: &Uuid, avatar_uuid: &Uuid) -> Result { + let db = Connection::open(get_database_path())?; + db.execute(&format!("UPDATE {} SET avatar=?1 WHERE uuid=?2", CONTACTS_TABLE), params![&avatar_uuid.as_bytes()[..], &contact_uuid.as_bytes()[..]]) + } + pub fn set_contact_seen(&self, uuid: &Uuid, seen: bool) -> Result { let db = Connection::open(get_database_path())?; let encrypted_seen = crypto::encrypt_data(&[bool_to_byte(seen)], &self.master_key).unwrap(); @@ -115,107 +127,89 @@ impl Identity { pub fn load_contacts(&self) -> Option> { match Connection::open(get_database_path()) { Ok(db) => { - match db.prepare(&("SELECT uuid, name, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) { - Ok(mut stmt) => { - let mut rows = stmt.query([]).unwrap(); - let mut contacts = Vec::new(); - while let Some(row) = rows.next().unwrap() { - let encrypted_public_key: Vec = row.get(2).unwrap(); - match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) { - Ok(public_key) => { - if public_key.len() == PUBLIC_KEY_LENGTH { - let encrypted_name: Vec = row.get(1).unwrap(); - match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) { - Ok(name) => { - let encrypted_verified: Vec = row.get(3).unwrap(); - match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) { - Ok(verified) => { - let encrypted_seen: Vec = row.get(4).unwrap(); - match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) { - Ok(seen) => { - let uuid: Vec = row.get(0).unwrap(); - match to_uuid_bytes(&uuid) { - Some(uuid_bytes) => { - contacts.push(Contact { - uuid: Uuid::from_bytes(uuid_bytes), - public_key: public_key.try_into().unwrap(), - name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(), - verified: byte_to_bool(verified[0]).unwrap(), - seen: byte_to_bool(seen[0]).unwrap(), - }) - } - None => {} - } - } - Err(e) => print_error!(e) - } + if let Ok(mut stmt) = db.prepare(&("SELECT uuid, name, avatar, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) { + let mut rows = stmt.query([]).unwrap(); + let mut contacts = Vec::new(); + while let Ok(Some(row)) = rows.next() { + let encrypted_public_key: Vec = row.get(3).unwrap(); + match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) { + Ok(public_key) => { + let encrypted_name: Vec = row.get(1).unwrap(); + match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) { + Ok(name) => { + let encrypted_verified: Vec = row.get(4).unwrap(); + match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) { + Ok(verified) => { + let encrypted_seen: Vec = row.get(5).unwrap(); + match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) { + Ok(seen) => { + let contact_uuid: Vec = row.get(0).unwrap(); + let avatar_result: Result, rusqlite::Error> = row.get(2); + let avatar = match avatar_result { + Ok(avatar_uuid) => Some(Uuid::from_bytes(to_uuid_bytes(&avatar_uuid).unwrap())), + Err(_) => None + }; + contacts.push(Contact { + uuid: Uuid::from_bytes(to_uuid_bytes(&contact_uuid).unwrap()), + public_key: public_key.try_into().unwrap(), + name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(), + avatar, + verified: byte_to_bool(verified[0]).unwrap(), + seen: byte_to_bool(seen[0]).unwrap(), + }) } Err(e) => print_error!(e) } } Err(e) => print_error!(e) } - } else { - print_error!("Invalid public key length: database corrupted"); } + Err(e) => print_error!(e) } - Err(e) => print_error!(e) } + Err(e) => print_error!(e) } - Some(contacts) } - Err(e) => { - print_error!(e); - None - } - } - } - Err(e) => { - print_error!(e); - None + return Some(contacts); + } } + Err(e) => print_error!(e) } + None } - pub fn clear_temporary_files() -> Result { + #[allow(unused_must_use)] + pub fn clear_cache() -> Result<(), rusqlite::Error> { let db = Connection::open(get_database_path())?; - db.execute(&format!("DELETE FROM {} WHERE contact_uuid IS NULL", FILES_TABLE), []) + db.execute(&format!("DELETE FROM {} WHERE contact_uuid IS NULL", FILES_TABLE), []); + db.execute(&format!("DELETE FROM {} WHERE uuid NOT IN (SELECT avatar FROM {})", AVATARS_TABLE, CONTACTS_TABLE), []); + Ok(()) } pub fn load_file(&self, uuid: Uuid) -> Option> { match Connection::open(get_database_path()) { Ok(db) => { - match db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)) { - Ok(mut stmt) => { - let mut rows = stmt.query([]).unwrap(); - while let Some(row) = rows.next().unwrap() { - let encrypted_uuid: Vec = row.get(0).unwrap(); - match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){ - Ok(test_uuid) => { - if test_uuid == uuid.as_bytes() { - let encrypted_data: Vec = row.get(1).unwrap(); - match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) { - Ok(data) => return Some(data), - Err(e) => print_error!(e) - } - } + let mut stmt = db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)).unwrap(); + let mut rows = stmt.query([]).unwrap(); + while let Ok(Some(row)) = rows.next() { + let encrypted_uuid: Vec = row.get(0).unwrap(); + match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){ + Ok(test_uuid) => { + if test_uuid == uuid.as_bytes() { + let encrypted_data: Vec = row.get(1).unwrap(); + match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) { + Ok(data) => return Some(data), + Err(e) => print_error!(e) } - Err(e) => print_error!(e) } } - None - } - Err(e) => { - print_error!(e); - None + Err(e) => print_error!(e) } } } - Err(e) => { - print_error!(e); - None - } + Err(e) => print_error!(e) } + None } pub fn store_file(&self, contact_uuid: Option, data: &[u8]) -> Result { @@ -244,73 +238,44 @@ impl Identity { pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option)>> { match Connection::open(get_database_path()) { Ok(db) => { - match db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) { - Ok(mut stmt) => { - let mut rows = stmt.query([]).unwrap(); - match rows.next() { - Ok(row) => if row.is_some() { - let total: usize = row.unwrap().get(0).unwrap(); - if offset >= total { - None - } else { - if offset+count >= total { - count = total-offset; - } - match db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)) { - Ok(mut stmt) => { - let mut rows = stmt.query([]).unwrap(); - let mut msgs = Vec::new(); - while let Some(row) = rows.next().unwrap() { - let encrypted_outgoing: Vec = row.get(0).unwrap(); - match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){ - Ok(outgoing) => { - match byte_to_bool(outgoing[0]) { - Ok(outgoing) => { - let encrypted_data: Vec = row.get(1).unwrap(); - match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) { - Ok(data) => { - msgs.push( - ( - outgoing, - data - ) - ) - }, - Err(e) => print_error!(e) - } - } - Err(_) => {} - } - - } + if let Ok(mut stmt) = db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) { + let mut rows = stmt.query([]).unwrap(); + if let Ok(Some(row)) = rows.next() { + let total: usize = row.get(0).unwrap(); + if offset < total { + if offset+count >= total { + count = total-offset; + } + let mut stmt = db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap(); + let mut rows = stmt.query([]).unwrap(); + let mut msgs = Vec::new(); + while let Ok(Some(row)) = rows.next() { + let encrypted_outgoing: Vec = row.get(0).unwrap(); + match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){ + Ok(outgoing) => { + match byte_to_bool(outgoing[0]) { + Ok(outgoing) => { + let encrypted_data: Vec = row.get(1).unwrap(); + match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) { + Ok(data) => msgs.push((outgoing, data)), Err(e) => print_error!(e) } } - Some(msgs) - } - Err(e) => { - print_error!(e); - None + Err(_) => {} } + } + Err(e) => print_error!(e) } - } else { - None } - Err(_) => None + return Some(msgs); } } - Err(e) => { - print_error!(e); - None - } } } - Err(e) => { - print_error!(e); - None - } + Err(e) => print_error!(e) } + None } #[allow(unused_must_use)] @@ -336,6 +301,29 @@ impl Identity { db.update(DBKeys::USE_PADDING, &encrypted_use_padding) } + pub fn store_avatar(&self, avatar: &[u8]) -> Result { + let db = Connection::open(get_database_path())?; + db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (uuid BLOB PRIMARY KEY, data BLOB)", AVATARS_TABLE), [])?; + let uuid = Uuid::new_v4(); + let encrypted_avatar = crypto::encrypt_data(avatar, &self.master_key).unwrap(); + db.execute(&format!("INSERT INTO {} (uuid, data) VALUES (?1, ?2)", AVATARS_TABLE), params![&uuid.as_bytes()[..], encrypted_avatar])?; + Ok(uuid) + } + + pub fn get_avatar(&self, avatar_uuid: &Uuid) -> Option> { + let db = Connection::open(get_database_path()).ok()?; + let mut stmt = db.prepare(&format!("SELECT data FROM {} WHERE uuid=?", AVATARS_TABLE)).unwrap(); + let mut rows = stmt.query(params![&avatar_uuid.as_bytes()[..]]).unwrap(); + let encrypted_avatar: Vec = rows.next().ok()??.get(0).unwrap(); + match crypto::decrypt_data(&encrypted_avatar, &self.master_key) { + Ok(avatar) => Some(avatar), + Err(e) => { + print_error!(e); + None + } + } + } + pub fn zeroize(&mut self){ self.master_key.zeroize(); self.keypair.secret.zeroize(); @@ -480,6 +468,16 @@ impl Identity { } } + pub fn set_identity_avatar(avatar: &[u8]) -> Result { + let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?; + db.upsert(DBKeys::AVATAR, avatar) + } + + pub fn get_identity_avatar() -> Result, rusqlite::Error> { + let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?; + db.get(DBKeys::AVATAR) + } + pub fn delete_identity() -> Result<(), std::io::Error> { std::fs::remove_file(get_database_path()) } diff --git a/src/key_value_table.rs b/src/key_value_table.rs index 88c88c5..1a07122 100644 --- a/src/key_value_table.rs +++ b/src/key_value_table.rs @@ -28,7 +28,7 @@ impl<'a> KeyValueTable<'a> { pub fn update(&self, key: &str, value: &[u8]) -> Result { self.db.execute(&format!("UPDATE {} SET value=? WHERE key=\"{}\"", self.table_name, key), params![value]) } - /*pub fn upsert(&self, key: &str, value: &[u8]) -> Result { + pub fn upsert(&self, key: &str, value: &[u8]) -> Result { self.db.execute(&format!("INSERT INTO {} (key, value) VALUES(?1, ?2) ON CONFLICT(key) DO UPDATE SET value=?3", self.table_name), params![key, value, value]) - }*/ + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 249f265..fa87056 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod constants; mod discovery; use std::{env, fs, io, net::SocketAddr, str::{FromStr, from_utf8}, sync::{Arc, RwLock}}; +use image::GenericImageView; use tokio::{net::TcpListener, runtime::Handle, sync::mpsc}; use actix_web::{App, HttpMessage, HttpRequest, HttpResponse, HttpServer, http::{header, CookieBuilder}, web, web::Data}; use actix_multipart::Multipart; @@ -198,7 +199,7 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc { let session_id: usize = args[1].parse().unwrap(); - match session_manager.add_contact(session_id, msg[args[0].len()+args[1].len()+2..].to_string()) { + match session_manager.add_contact(session_id) { Ok(_) => {}, Err(e) => print_error!(e) } @@ -224,10 +225,10 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc print_error!(e) } } - "ask_name" => { + "refresh_profile" => { let session_id: usize = args[1].parse().unwrap(); session_manager.send_command(&session_id, SessionCommand::Send { - buff: protocol::ask_name() + buff: protocol::ask_profile_info() }).await; } "set_use_padding" => { @@ -303,12 +304,86 @@ fn is_authenticated(req: &HttpRequest) -> bool { false } +async fn handle_set_avatar(req: HttpRequest, mut payload: Multipart) -> HttpResponse { + if is_authenticated(&req) { + while let Ok(Some(mut field)) = payload.try_next().await { + let content_disposition = field.content_disposition().unwrap(); + if let Some(name) = content_disposition.get_name() { + if name == "avatar" { + let mut buffer = Vec::new(); + while let Some(Ok(chunk)) = field.next().await { + buffer.extend(chunk); + } + match image::guess_format(&buffer) { + Ok(format) => { + match image::load_from_memory_with_format(&buffer, format) { + Ok(image) => { + let (width, height) = image.dimensions(); + let image = if width < height { + image.crop_imm(0, (height-width)/2, width, width) + } else if width > height { + image.crop_imm((width-height)/2, 0, height, height) + } else { + image + }; + let mut avatar = Vec::new(); + image.write_to(&mut avatar, format).unwrap(); + let global_vars = req.app_data::>>>().unwrap(); + match global_vars.read().unwrap().session_manager.set_avatar(&avatar).await { + Ok(_) => { + return HttpResponse::Ok().finish(); + } + Err(e) => { + print_error!(e); + return HttpResponse::InternalServerError().finish(); + } + } + } + Err(e) => print_error!(e) + } + } + Err(e) => print_error!(e) + } + } + } + } + } + HttpResponse::BadRequest().finish() +} + +fn reply_with_avatar(avatar: Option>, name: &str) -> HttpResponse { + match avatar { + Some(avatar) => HttpResponse::Ok().content_type("image/*").body(avatar), + None => { + #[cfg(not(debug_assertions))] + let svg = include_str!(concat!(env!("OUT_DIR"), "/text_avatar.svg")); + #[cfg(debug_assertions)] + let svg = replace_fields("src/frontend/imgs/text_avatar.svg"); + HttpResponse::Ok().content_type("image/svg+xml").body(svg.replace("LETTER", &name.chars().nth(0).unwrap().to_string())) + } + } +} + +fn handle_avatar(req: HttpRequest) -> HttpResponse { + let splits: Vec<&str> = req.path()[1..].split("/").collect(); + if splits.len() == 2 { + if splits[1] == "self" { + return reply_with_avatar(Identity::get_identity_avatar().ok(), &Identity::get_identity_name().unwrap()); + } + } else if splits.len() == 3 { + if let Ok(session_id) = splits[1].parse() { + let global_vars = req.app_data::>>>().unwrap(); + return reply_with_avatar(global_vars.read().unwrap().session_manager.get_avatar(&session_id), splits[2]); + } + } + HttpResponse::BadRequest().finish() +} + #[derive(Deserialize, Serialize, Debug)] struct FileInfo { uuid: String, file_name: String, } - fn handle_load_file(req: HttpRequest, file_info: web::Query) -> HttpResponse { if is_authenticated(&req) { match Uuid::from_str(&file_info.uuid) { @@ -368,7 +443,9 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo loop { chunk_buffer.extend(&pending_buffer); pending_buffer.clear(); + println!("Calling next()"); while let Some(Ok(chunk)) = field.next().await { + println!("Not None"); if chunk_buffer.len()+chunk.len() <= constants::FILE_CHUNK_SIZE { chunk_buffer.extend(chunk); } else if chunk_buffer.len() == constants::FILE_CHUNK_SIZE { @@ -381,6 +458,7 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo break; } } + println!("May be None"); if !global_vars_read.session_manager.send_command(&session_id, SessionCommand::EncryptFileChunk{ plain_text: chunk_buffer.clone() }).await { @@ -457,7 +535,7 @@ fn login(identity: Identity, global_vars: &Arc>) -> HttpRespo } fn on_identity_loaded(identity: Identity, global_vars: &Arc>) -> HttpResponse { - match Identity::clear_temporary_files() { + match Identity::clear_cache() { Ok(_) => {}, Err(e) => print_error!(e) } @@ -571,13 +649,13 @@ async fn handle_index(req: HttpRequest) -> HttpResponse { const JS_CONTENT_TYPE: &str = "text/javascript"; #[cfg(debug_assertions)] -fn replace_css(file_path: &str) -> String { +fn replace_fields(file_path: &str) -> String { use yaml_rust::YamlLoader; let mut content = fs::read_to_string(file_path).unwrap(); let config = &YamlLoader::load_from_str(&fs::read_to_string("config.yml").unwrap()).unwrap()[0]; - let css_values = config["css"].as_hash().unwrap(); - css_values.into_iter().for_each(|entry| { - content = content.replace(entry.0.as_str().unwrap(), entry.1.as_str().unwrap()); + let fields = config.as_hash().unwrap(); + fields.into_iter().for_each(|field| { + content = content.replace(field.0.as_str().unwrap(), field.1.as_str().unwrap()); }); content } @@ -596,7 +674,7 @@ fn handle_static(req: HttpRequest) -> HttpResponse { } "index.css" => { #[cfg(debug_assertions)] - return response_builder.body(replace_css("src/frontend/index.css")); + return response_builder.body(replace_fields("src/frontend/index.css")); #[cfg(not(debug_assertions))] return response_builder.body(include_str!(concat!(env!("OUT_DIR"), "/index.css"))); } @@ -655,7 +733,7 @@ fn handle_static(req: HttpRequest) -> HttpResponse { } "style.css" => { #[cfg(debug_assertions)] - return response_builder.body(replace_css("src/frontend/commons/style.css")); + return response_builder.body(replace_fields("src/frontend/commons/style.css")); #[cfg(not(debug_assertions))] return response_builder.body(include_str!(concat!(env!("OUT_DIR"), "/commons/style.css"))); } @@ -700,6 +778,8 @@ async fn start_http_server(global_vars: Arc>) -> io::Result<( .route("/send_file", web::post().to(handle_send_file)) .route("/send_large_file", web::post().to(handle_send_file)) .route("/load_file", web::get().to(handle_load_file)) + .route("/set_avatar", web::post().to(handle_set_avatar)) + .route("/avatar/*", web::get().to(handle_avatar)) .route("/static/.*", web::get().to(handle_static)) .route("/logout", web::get().to(handle_logout)) } diff --git a/src/protocol.rs b/src/protocol.rs index 24c61b9..e182aba 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -5,26 +5,27 @@ pub struct Headers; impl Headers { pub const MESSAGE: u8 = 0x00; - pub const ASK_NAME: u8 = 0x01; - pub const TELL_NAME: u8 = 0x02; - pub const FILE: u8 = 0x03; - pub const ASK_LARGE_FILES: u8 = 0x04; - pub const ACCEPT_LARGE_FILES: u8 = 0x05; - pub const LARGE_FILE_CHUNK: u8 = 0x06; - pub const ACK_CHUNK: u8 = 0x07; - pub const ABORT_FILES_TRANSFER: u8 = 0x08; + pub const FILE: u8 = 0x01; + pub const ASK_PROFILE_INFO: u8 = 0x02; + pub const NAME: u8 = 0x03; + pub const AVATAR: u8 = 0x04; + pub const ASK_LARGE_FILES: u8 = 0x05; + pub const ACCEPT_LARGE_FILES: u8 = 0x06; + pub const LARGE_FILE_CHUNK: u8 = 0x07; + pub const ACK_CHUNK: u8 = 0x08; + pub const ABORT_FILES_TRANSFER: u8 = 0x09; } pub fn new_message(message: String) -> Vec { [&[Headers::MESSAGE], message.as_bytes()].concat() } -pub fn ask_name() -> Vec { - vec![Headers::ASK_NAME] +pub fn ask_profile_info() -> Vec { + vec![Headers::ASK_PROFILE_INFO] } -pub fn tell_name(name: &str) -> Vec { - [&[Headers::TELL_NAME], name.as_bytes()].concat() +pub fn name(name: &str) -> Vec { + [&[Headers::NAME], name.as_bytes()].concat() } pub fn file(file_name: &str, buffer: &[u8]) -> Vec { @@ -79,4 +80,8 @@ pub fn parse_ask_files(buffer: &[u8]) -> Option> { } } Some(files_info) +} + +pub fn avatar(avatar: &[u8]) -> Vec { + [&[Headers::AVATAR], avatar].concat() } \ No newline at end of file diff --git a/src/session_manager.rs b/src/session_manager.rs index 94daa5f..a9cc164 100644 --- a/src/session_manager.rs +++ b/src/session_manager.rs @@ -40,6 +40,7 @@ pub struct LargeFilesDownload { #[derive(Clone)] pub struct SessionData { pub name: String, + avatar: Option, pub outgoing: bool, pub peer_public_key: [u8; PUBLIC_KEY_LENGTH], pub ip: IpAddr, @@ -54,7 +55,7 @@ pub struct SessionManager { ui_connection: Mutex>, loaded_contacts: RwLock>, pub last_loaded_msg_offsets: RwLock>, - pub saved_msgs: Mutex)>>>, + pub saved_msgs: RwLock)>>>, pub not_seen: RwLock>, mdns_service: Mutex>, listener_stop_signal: Mutex>>, @@ -72,6 +73,10 @@ impl SessionManager { } } + fn get_all_senders(&self) -> Vec> { + self.sessions.read().unwrap().iter().map(|i| i.1.sender.clone()).collect() + } + async fn encrypt_and_send(&self, writer: &mut T, buff: &[u8]) -> Result<(), PsecError> { let use_padding = self.identity.read().unwrap().as_ref().unwrap().use_padding; writer.encrypt_and_send(buff, use_padding).await @@ -96,7 +101,7 @@ impl SessionManager { } } if !msg_saved { - self.saved_msgs.lock().unwrap().get_mut(&session_id).unwrap().push((false, buffer)); + self.saved_msgs.write().unwrap().get_mut(&session_id).unwrap().push((outgoing, buffer)); } } @@ -127,7 +132,7 @@ impl SessionManager { ui_connection.on_disconnected(&session_id); }); self.sessions.write().unwrap().remove(session_id); - self.saved_msgs.lock().unwrap().remove(session_id); + self.saved_msgs.write().unwrap().remove(session_id); self.not_seen.write().unwrap().retain(|x| x != session_id); } @@ -172,64 +177,6 @@ impl SessionManager { let session_read = result.1; receiving.set(session_read.into_receive_and_decrypt()); match buffer[0] { - protocol::Headers::ASK_NAME => { - let name = { - self.identity.read().unwrap().as_ref().and_then(|identity| Some(identity.name.clone())) - }; - if name.is_some() { //can be None if we log out just before locking the identity mutex - if let Err(e) = self.encrypt_and_send(&mut session_write, &protocol::tell_name(&name.unwrap())).await { - print_error!(e); - break; - } - } - } - protocol::Headers::TELL_NAME => { - match from_utf8(&buffer[1..]) { - Ok(new_name) => { - let new_name = new_name.replace('\n', " "); - self.with_ui_connection(|ui_connection| { - ui_connection.on_name_told(&session_id, &new_name); - }); - let mut loaded_contacts = self.loaded_contacts.write().unwrap(); - if let Some(contact) = loaded_contacts.get_mut(&session_id) { - contact.name = new_name.to_string(); - if let Err(e) = self.identity.read().unwrap().as_ref().unwrap().change_contact_name(&contact.uuid, &new_name) { - print_error!(e); - } - } else { - self.sessions.write().unwrap().get_mut(&session_id).unwrap().name = new_name.to_string(); - } - } - Err(e) => print_error!(e) - } - } - protocol::Headers::ASK_LARGE_FILES => { - if self.sessions.read().unwrap().get(&session_id).unwrap().files_download.is_none() && !is_sending { //don't accept 2 file transfers at the same time - if let Some(files_info) = protocol::parse_ask_files(&buffer) { - let download_location = UserDirs::new().unwrap().download_dir; - let files: Vec = files_info.into_iter().map(|info| { - LargeFileDownload { - file_name: info.1, - file_size: info.0, - transferred: 0, - last_chunk: get_unix_timestamp(), - } - }).collect(); - self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = Some(LargeFilesDownload { - download_location: download_location.clone(), - accepted: false, - index: 0, - files: files.clone(), - }); - self.with_ui_connection(|ui_connection| { - ui_connection.on_ask_large_files(&session_id, &files, download_location.to_str().unwrap()); - }) - } - } else if let Err(e) = self.encrypt_and_send(&mut session_write, &[protocol::Headers::ABORT_FILES_TRANSFER]).await { - print_error!(e); - break; - } - } protocol::Headers::LARGE_FILE_CHUNK => { let mut should_accept_chunk = false; { @@ -322,6 +269,103 @@ impl SessionManager { ui_connection.on_received(&session_id, &buffer); }); } + protocol::Headers::ASK_LARGE_FILES => { + if self.sessions.read().unwrap().get(&session_id).unwrap().files_download.is_none() && !is_sending { //don't accept 2 file transfers at the same time + if let Some(files_info) = protocol::parse_ask_files(&buffer) { + let download_location = UserDirs::new().unwrap().download_dir; + let files: Vec = files_info.into_iter().map(|info| { + LargeFileDownload { + file_name: info.1, + file_size: info.0, + transferred: 0, + last_chunk: get_unix_timestamp(), + } + }).collect(); + self.sessions.write().unwrap().get_mut(&session_id).unwrap().files_download = Some(LargeFilesDownload { + download_location: download_location.clone(), + accepted: false, + index: 0, + files: files.clone(), + }); + self.with_ui_connection(|ui_connection| { + ui_connection.on_ask_large_files(&session_id, &files, download_location.to_str().unwrap()); + }) + } + } else if let Err(e) = self.encrypt_and_send(&mut session_write, &[protocol::Headers::ABORT_FILES_TRANSFER]).await { + print_error!(e); + break; + } + } + protocol::Headers::ASK_PROFILE_INFO => { + let identity = { + self.identity.read().unwrap().clone() + }; + if let Some(identity) = identity { //can be None if we log out just before locking the identity mutex + match self.encrypt_and_send(&mut session_write, &protocol::name(&identity.name)).await { + Ok(_) => { + if let Ok(avatar) = Identity::get_identity_avatar() { + if let Err(e) = self.encrypt_and_send(&mut session_write, &protocol::avatar(&avatar)).await { + print_error!(e); + break; + } + } + } + Err(e) => { + print_error!(e); + break; + } + } + } + } + protocol::Headers::NAME => { + match from_utf8(&buffer[1..]) { + Ok(new_name) => { + let new_name = new_name.replace('\n', " "); + self.with_ui_connection(|ui_connection| { + ui_connection.on_name_told(&session_id, &new_name); + }); + let mut loaded_contacts = self.loaded_contacts.write().unwrap(); + if let Some(contact) = loaded_contacts.get_mut(&session_id) { + contact.name = new_name.to_string(); + if let Err(e) = self.identity.read().unwrap().as_ref().unwrap().change_contact_name(&contact.uuid, &new_name) { + print_error!(e); + } + } else { + self.sessions.write().unwrap().get_mut(&session_id).unwrap().name = new_name.to_string(); + } + } + Err(e) => print_error!(e) + } + } + protocol::Headers::AVATAR => { + if buffer.len() < 10000000 { + match image::load_from_memory(&buffer[1..]) { + Ok(image) => { + drop(image); + let identity_opt = self.identity.read().unwrap(); + let identity = identity_opt.as_ref().unwrap(); + match identity.store_avatar(&buffer[1..]) { + Ok(avatar_uuid) => { + let mut loaded_contacts = self.loaded_contacts.write().unwrap(); + if let Some(contact) = loaded_contacts.get_mut(&session_id) { + contact.avatar = Some(avatar_uuid); + if let Err(e) = identity.set_contact_avatar(&contact.uuid, &avatar_uuid) { + print_error!(e); + } + } else { + self.sessions.write().unwrap().get_mut(&session_id).unwrap().avatar = Some(avatar_uuid); + } + self.with_ui_connection(|ui_connection| { + ui_connection.on_avatar_set(&session_id); + }); + } + Err(e) => print_error!(e) + } + } + Err(e) => print_error!(e) + } + } + } _ => { let header = buffer[0]; let buffer = match header { @@ -421,11 +465,7 @@ impl SessionManager { let mut peer_public_key = [0; PUBLIC_KEY_LENGTH]; let session = { let identity = { - let identity_opt = session_manager.identity.read().unwrap(); - match identity_opt.as_ref() { - Some(identity) => Some(identity.clone()), - None => None - } + session_manager.identity.read().unwrap().clone() }; match identity { Some(identity) => { @@ -461,8 +501,9 @@ impl SessionManager { } if is_new_session && session_manager.is_identity_loaded() { //check if we didn't logged out during the handshake let (sender, receiver) = mpsc::channel(32); - let session_data = SessionData{ + let session_data = SessionData { name: ip.to_string(), + avatar: None, outgoing, peer_public_key, ip, @@ -485,7 +526,7 @@ impl SessionManager { *session_counter += 1; } let session_id = session_id.unwrap(); - session_manager.saved_msgs.lock().unwrap().insert(session_id, Vec::new()); + session_manager.saved_msgs.write().unwrap().insert(session_id, Vec::new()); Some((session_id, receiver)) } else { None @@ -497,7 +538,7 @@ impl SessionManager { ui_connection.on_new_session(&session_id, &ip.to_string(), outgoing, &crypto::generate_fingerprint(&peer_public_key), ip, None); }); if !is_contact { - match session_manager.encrypt_and_send(&mut session, &protocol::ask_name()).await { + match session_manager.encrypt_and_send(&mut session, &protocol::ask_profile_info()).await { Ok(_) => {} Err(e) => { print_error!(e); @@ -542,7 +583,7 @@ impl SessionManager { } pub fn get_saved_msgs(&self) -> HashMap)>> { - self.saved_msgs.lock().unwrap().clone() + self.saved_msgs.read().unwrap().clone() } pub fn set_seen(&self, session_id: usize, seen: bool) { @@ -567,8 +608,10 @@ impl SessionManager { } } - pub fn add_contact(&self, session_id: usize, name: String) -> Result<(), rusqlite::Error> { - let contact = self.identity.read().unwrap().as_ref().unwrap().add_contact(name, self.sessions.read().unwrap().get(&session_id).unwrap().peer_public_key)?; + pub fn add_contact(&self, session_id: usize) -> Result<(), rusqlite::Error> { + let sessions = self.sessions.read().unwrap(); + let session = sessions.get(&session_id).unwrap(); + let contact = self.identity.read().unwrap().as_ref().unwrap().add_contact(session.name.clone(), session.avatar, session.peer_public_key)?; self.loaded_contacts.write().unwrap().insert(session_id, contact); self.last_loaded_msg_offsets.write().unwrap().insert(session_id, 0); Ok(()) @@ -581,6 +624,7 @@ impl SessionManager { if let Some(contact) = loaded_contacts.remove(&session_id) { if let Some(session) = self.sessions.write().unwrap().get_mut(&session_id) { session.name = contact.name; + session.avatar = contact.avatar; } } self.last_loaded_msg_offsets.write().unwrap().remove(&session_id); @@ -602,7 +646,7 @@ impl SessionManager { let result = Identity::delete_conversation(&self.loaded_contacts.read().unwrap().get(&session_id).unwrap().uuid); if result.is_ok() { self.last_loaded_msg_offsets.write().unwrap().insert(session_id, 0); - self.saved_msgs.lock().unwrap().insert(session_id, Vec::new()); + self.saved_msgs.write().unwrap().insert(session_id, Vec::new()); } result } @@ -627,15 +671,32 @@ impl SessionManager { msgs } + pub fn get_avatar(&self, session_id: &usize) -> Option> { + let avatar_uuid = match self.loaded_contacts.read().unwrap().get(session_id) { + Some(contact) => contact.avatar?, + None => self.sessions.read().unwrap().get(session_id)?.avatar? + }; + self.identity.read().unwrap().as_ref().unwrap().get_avatar(&avatar_uuid) + } + + #[allow(unused_must_use)] + pub async fn set_avatar(&self, avatar: &[u8]) -> Result<(), rusqlite::Error> { + Identity::set_identity_avatar(&avatar)?; + let avatar_msg = protocol::avatar(&avatar); + for sender in self.get_all_senders().into_iter() { + sender.send(SessionCommand::Send { + buff: avatar_msg.clone() + }).await; + } + Ok(()) + } + #[allow(unused_must_use)] pub async fn change_name(&self, new_name: String) -> Result { - let telling_name = protocol::tell_name(&new_name); + let telling_name = protocol::name(&new_name); let result = self.identity.write().unwrap().as_mut().unwrap().change_name(new_name); if result.is_ok() { - let senders: Vec> = { - self.sessions.read().unwrap().iter().map(|i| i.1.sender.clone()).collect() - }; - for sender in senders.into_iter() { + for sender in self.get_all_senders().into_iter() { sender.send(SessionCommand::Send { buff: telling_name.clone() }).await; @@ -659,7 +720,7 @@ impl SessionManager { *self.ui_connection.lock().unwrap() = None; *self.session_counter.write().unwrap() = 0; self.loaded_contacts.write().unwrap().clear(); - self.saved_msgs.lock().unwrap().clear(); + self.saved_msgs.write().unwrap().clear(); } pub fn is_identity_loaded(&self) -> bool { @@ -703,7 +764,7 @@ impl SessionManager { ui_connection: Mutex::new(None), loaded_contacts: RwLock::new(HashMap::new()), last_loaded_msg_offsets: RwLock::new(HashMap::new()), - saved_msgs: Mutex::new(HashMap::new()), + saved_msgs: RwLock::new(HashMap::new()), not_seen: RwLock::new(Vec::new()), mdns_service: Mutex::new(None), listener_stop_signal: Mutex::new(None), diff --git a/src/ui_interface.rs b/src/ui_interface.rs index edd9fcd..07bb8e0 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -112,6 +112,9 @@ mod ui_messages { pub fn on_name_told(session_id: &usize, name: &str) -> Message { Message::from(format!("name_told {} {}", session_id, name)) } + pub fn on_avatar_set(session_id: &usize) -> Message { + simple_event("avatar_set", session_id) + } pub fn set_as_contact(session_id: usize, name: &str, verified: bool, fingerprint: &str) -> Message { Message::from(format!("is_contact {} {} {} {}", session_id, verified, fingerprint, name)) } @@ -179,6 +182,9 @@ impl UiConnection { pub fn on_name_told(&mut self, session_id: &usize, name: &str) { self.write_message(ui_messages::on_name_told(session_id, name)); } + pub fn on_avatar_set(&mut self, session_id: &usize) { + self.write_message(ui_messages::on_avatar_set(session_id)); + } pub fn inc_files_transfer(&mut self, session_id: &usize, chunk_size: u64) { self.write_message(ui_messages::inc_files_transfer(session_id, chunk_size));