This commit is contained in:
Matéo Duparc 2021-05-25 21:59:16 +02:00
parent da4cc4995f
commit 8cc6f6b50f
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
16 changed files with 786 additions and 328 deletions

219
Cargo.lock generated
View File

@ -285,6 +285,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]] [[package]]
name = "aead" name = "aead"
version = "0.4.1" version = "0.4.1"
@ -372,8 +378,10 @@ dependencies = [
"html-escape", "html-escape",
"html-minifier", "html-minifier",
"if-addrs", "if-addrs",
"image",
"lazy_static", "lazy_static",
"libmdns", "libmdns",
"linked-hash-map",
"multicast_dns", "multicast_dns",
"platform-dirs", "platform-dirs",
"rand 0.7.3", "rand 0.7.3",
@ -395,9 +403,9 @@ dependencies = [
[[package]] [[package]]
name = "async-psec" name = "async-psec"
version = "0.2.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58ab60657d7b3949c3bdb06570fc4855a4e83dbe6c9da73902c67355dd8f37a8" checksum = "31dcd9dc064b59d84e03e9c42c97ca8bba139ff04a874afc0e93944d7f62e235"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"async-trait", "async-trait",
@ -510,6 +518,12 @@ version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe"
[[package]]
name = "bytemuck"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.4.3" version = "1.4.3"
@ -564,6 +578,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]] [[package]]
name = "const_fn" name = "const_fn"
version = "0.4.7" version = "0.4.7"
@ -633,6 +653,51 @@ dependencies = [
"cfg-if 1.0.0", "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]] [[package]]
name = "crypto-mac" name = "crypto-mac"
version = "0.11.0" version = "0.11.0"
@ -665,6 +730,16 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "deflate"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
dependencies = [
"adler32",
"byteorder",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.14" version = "0.99.14"
@ -809,7 +884,7 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"crc32fast", "crc32fast",
"libc", "libc",
"miniz_oxide", "miniz_oxide 0.4.4",
] ]
[[package]] [[package]]
@ -1004,6 +1079,16 @@ dependencies = [
"polyval", "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]] [[package]]
name = "h2" name = "h2"
version = "0.2.7" version = "0.2.7"
@ -1173,6 +1258,25 @@ dependencies = [
"libc", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.6.2" version = "1.6.2"
@ -1228,6 +1332,15 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" 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]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.51" version = "0.3.51"
@ -1351,6 +1464,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "memoffset"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.16" version = "0.3.16"
@ -1366,6 +1488,15 @@ dependencies = [
"macro-utils", "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]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.4.4" version = "0.4.4"
@ -1518,6 +1649,28 @@ dependencies = [
"num-traits", "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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.14" version = "0.2.14"
@ -1706,6 +1859,18 @@ dependencies = [
"dirs-next", "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]] [[package]]
name = "polyval" name = "polyval"
version = "0.5.0" version = "0.5.0"
@ -1840,6 +2005,31 @@ dependencies = [
"rand_core 0.6.2", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.2.8" version = "0.2.8"
@ -1954,6 +2144,12 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@ -2265,6 +2461,17 @@ dependencies = [
"num_cpus", "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]] [[package]]
name = "time" name = "time"
version = "0.2.26" version = "0.2.26"
@ -2664,6 +2871,12 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "weezl"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e"
[[package]] [[package]]
name = "widestring" name = "widestring"
version = "0.4.3" version = "0.4.3"

View File

@ -36,8 +36,10 @@ if-addrs = "0.6"
base64 = "0.13" base64 = "0.13"
scrypt = "0.7" scrypt = "0.7"
zeroize = "1.2" zeroize = "1.2"
image = "0.23"
yaml-rust = "0.4" #only in debug mode yaml-rust = "0.4" #only in debug mode
[build-dependencies] [build-dependencies]
html-minifier = "3.0" html-minifier = "3.0"
yaml-rust = "0.4" yaml-rust = "0.4"
linked-hash-map = "0.5"

View File

@ -1,5 +1,9 @@
#[cfg(not(debug_assertions))] #[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))] #[cfg(not(debug_assertions))]
fn minify_content(content: &str, language: &str) -> Option<String> { fn minify_content(content: &str, language: &str) -> Option<String> {
@ -11,10 +15,15 @@ fn minify_content(content: &str, language: &str) -> Option<String> {
} }
} }
#[cfg(not(debug_assertions))]
fn replace_fields(content: &mut String, fields: &LinkedHashMap<Yaml, Yaml>) {
fields.into_iter().for_each(|field| {
*content = content.replace(field.0.as_str().unwrap(), field.1.as_str().unwrap());
});
}
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
fn generate_web_files() { fn generate_web_files() {
use yaml_rust::YamlLoader;
let out_dir = env::var("OUT_DIR").unwrap(); let out_dir = env::var("OUT_DIR").unwrap();
let out_dir = Path::new(&out_dir); let out_dir = Path::new(&out_dir);
let src_dir = Path::new("src/frontend"); 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 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", "login.html",
@ -40,9 +49,7 @@ fn generate_web_files() {
let extension = path.extension().unwrap().to_str().unwrap(); let extension = path.extension().unwrap().to_str().unwrap();
let mut content = read_to_string(src_dir.join(path)).unwrap(); let mut content = read_to_string(src_dir.join(path)).unwrap();
if extension == "css" { if extension == "css" {
css_values.into_iter().for_each(|entry| { replace_fields(&mut content, fields);
content = content.replace(entry.0.as_str().unwrap(), entry.1.as_str().unwrap());
});
} }
if file_name == &"index.html" { if file_name == &"index.html" {
content = content.replace("AIRA_VERSION", env!("CARGO_PKG_VERSION")); 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(); let mut dst = File::create(out_dir.join(path)).unwrap();
dst.write(minified_content.as_bytes()).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() { fn main() {

View File

@ -1,2 +1 @@
css: ACCENT_COLOR: "19a52c"
ACCENT_COLOR: "19a52c"

View File

@ -1,13 +1,17 @@
function generateAvatar(name) { function generateImgAvatar() {
let span = document.createElement("span"); let img = document.createElement("img");
if (typeof name == "undefined") { img.classList.add("avatar");
span.appendChild(document.createTextNode("?")); return img;
} else if (name.length > 0) { }
span.appendChild(document.createTextNode(name[0].toUpperCase()));
} function generateSelfAvatar(timestamp) {
let div = document.createElement("div"); let img = generateImgAvatar();
div.classList.add("avatar"); img.src = "/avatar/self?"+timestamp;
div.appendChild(span); return img;
div.appendChild(document.createElement("div")); //used for background }
return div;
function generateAvatar(sessionId, name, timestamp) {
let img = generateImgAvatar();
img.src = "/avatar/"+sessionId+"/"+name+"?"+timestamp;
return img;
} }

View File

@ -26,24 +26,8 @@ input[type="text"], input[type="password"] {
} }
.avatar { .avatar {
position: relative;
margin-right: .5em; margin-right: .5em;
width: 1.5em; width: 1.5em;
height: 1.5em; height: 1.5em;
} border-radius: 50%;
.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;
} }

View File

@ -0,0 +1,4 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" fill="#ACCENT_COLOR"/>
<text x="50" y="50" text-anchor="middle" dominant-baseline="middle" fill="white" font-weight="bold" font-size="60" dy=".1em" font-family="Arial,Helvetica,Sans-Serif">LETTER</text>
</svg>

After

Width:  |  Height:  |  Size: 305 B

View File

@ -67,7 +67,7 @@ input[type="file"] {
left: 0; left: 0;
right: 0; right: 0;
margin: auto; margin: auto;
width: 40%; width: 40vw;
max-height: 90vh; max-height: 90vh;
overflow: auto; overflow: auto;
box-sizing: border-box; box-sizing: border-box;
@ -102,29 +102,6 @@ input[type="file"] {
content: url("/static/imgs/icons/cancel"); content: url("/static/imgs/icons/cancel");
background-color: unset; 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 { .popup h2.warning::before {
content: url("/static/imgs/icons/warning/ACCENT_COLOR"); content: url("/static/imgs/icons/warning/ACCENT_COLOR");
width: 9%; width: 9%;
@ -187,6 +164,57 @@ label {
.switch input:checked + span:before { .switch input:checked + span:before {
transform: translateX(26px); 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 { #session_info .name {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -8,6 +8,9 @@ let currentSessionId = -1;
let sessionsData = new Map(); let sessionsData = new Map();
let msgHistory = new Map(); let msgHistory = new Map();
let pendingFilesTransfers = new Map(); let pendingFilesTransfers = new Map();
let avatarTimestamps = new Map([
["self", Date.now()]
]);
function onClickSession(event) { function onClickSession(event) {
let sessionId = event.currentTarget.getAttribute("data-sessionId"); let sessionId = event.currentTarget.getAttribute("data-sessionId");
@ -75,7 +78,7 @@ document.getElementById("delete_conversation").onclick = function() {
showPopup(mainDiv); showPopup(mainDiv);
}; };
document.getElementById("add_contact").onclick = function() { document.getElementById("add_contact").onclick = function() {
socket.send("contact "+currentSessionId+" "+sessionsData.get(currentSessionId).name); socket.send("contact "+currentSessionId);
sessionsData.get(currentSessionId).isContact = true; sessionsData.get(currentSessionId).isContact = true;
displayHeader(); displayHeader();
displaySessions(); displaySessions();
@ -234,8 +237,47 @@ msg_log.onscroll = function() {
let profile_div = document.querySelector("#me>div"); let profile_div = document.querySelector("#me>div");
profile_div.onclick = function() { profile_div.onclick = function() {
let mainDiv = document.createElement("div"); let mainDiv = document.createElement("div");
let avatar = generateAvatar(identityName); mainDiv.id = "profile_info";
mainDiv.appendChild(avatar); 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"); let fingerprint = document.createElement("pre");
fingerprint.id = "identity_fingerprint"; fingerprint.id = "identity_fingerprint";
fingerprint.textContent = beautifyFingerprint(identityFingerprint); fingerprint.textContent = beautifyFingerprint(identityFingerprint);
@ -427,8 +469,8 @@ socket.onmessage = function(msg) {
console.log("Message: "+msg.data); console.log("Message: "+msg.data);
let args = msg.data.split(" "); let args = msg.data.split(" ");
switch (args[0]) { switch (args[0]) {
case "disconnected": case "inc_file_transfer":
onDisconnected(args[1]); onIncFilesTransfer(args[1], parseInt(args[2]));
break; break;
case "new_session": 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)); 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": case "aborted":
onFilesTransferAborted(args[1]); onFilesTransferAborted(args[1]);
break; break;
case "inc_file_transfer":
onIncFilesTransfer(args[1], parseInt(args[2]));
break;
case "load_msgs": case "load_msgs":
onMsgsLoad(args[1], msg.data.slice(args[0].length+args[1].length+2)); onMsgsLoad(args[1], msg.data.slice(args[0].length+args[1].length+2));
break; break;
case "name_told": case "name_told":
onNameTold(args[1], msg.data.slice(args[0].length+args[1].length+2)); onNameTold(args[1], msg.data.slice(args[0].length+args[1].length+2));
break; break;
case "avatar_set":
onAvatarSet(args[1]);
break;
case "is_contact": 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)); 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; break;
@ -475,6 +517,9 @@ socket.onmessage = function(msg) {
case "password_changed": case "password_changed":
onPasswordChanged(args[1] === "true", args[2] === "true"); onPasswordChanged(args[1] === "true", args[2] === "true");
break; break;
case "disconnected":
onDisconnected(args[1]);
break;
case "logout": case "logout":
logout(); logout();
} }
@ -509,6 +554,14 @@ function onNameTold(sessionId, name) {
} }
displaySessions(); displaySessions();
} }
function onAvatarSet(sessionId) {
avatarTimestamps.set(sessionId, Date.now());
displaySessions();
if (sessionId === currentSessionId) {
displayHeader();
displayHistory(false);
}
}
function setNotSeen(strSessionIds) { function setNotSeen(strSessionIds) {
let sessionIds = strSessionIds.split(' '); let sessionIds = strSessionIds.split(' ');
for (let i=0; i<sessionIds.length; ++i) { for (let i=0; i<sessionIds.length; ++i) {
@ -787,7 +840,7 @@ function showSessionInfoPopup() {
if (typeof session !== "undefined") { if (typeof session !== "undefined") {
let mainDiv = document.createElement("div"); let mainDiv = document.createElement("div");
mainDiv.id = "session_info"; mainDiv.id = "session_info";
mainDiv.appendChild(generateAvatar(session.name)); mainDiv.appendChild(generateAvatar(currentSessionId, session.name, avatarTimestamps.get(currentSessionId)));
let nameDiv = document.createElement("div"); let nameDiv = document.createElement("div");
nameDiv.classList.add("name"); nameDiv.classList.add("name");
let h2 = document.createElement("h2"); let h2 = document.createElement("h2");
@ -796,7 +849,7 @@ function showSessionInfoPopup() {
if (session.isOnline) { if (session.isOnline) {
let button = document.createElement("button"); let button = document.createElement("button");
button.onclick = function() { button.onclick = function() {
socket.send("ask_name "+currentSessionId); socket.send("refresh_profile "+currentSessionId);
}; };
nameDiv.appendChild(button); nameDiv.appendChild(button);
} }
@ -839,6 +892,7 @@ function addSession(sessionId, name, outgoing, fingerprint, ip, isContact, isVer
"isOnline": isOnline, "isOnline": isOnline,
}); });
msgHistory.set(sessionId, []); msgHistory.set(sessionId, []);
avatarTimestamps.set(sessionId, Date.now());
displaySessions(); displaySessions();
} }
function displaySessions() { function displaySessions() {
@ -860,7 +914,7 @@ function logout() {
} }
function displayProfile() { function displayProfile() {
profile_div.innerHTML = ""; profile_div.innerHTML = "";
profile_div.appendChild(generateAvatar(identityName)); profile_div.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
let p = document.createElement("p"); let p = document.createElement("p");
p.textContent = identityName; p.textContent = identityName;
profile_div.appendChild(p); profile_div.appendChild(p);
@ -872,7 +926,7 @@ function displayHeader() {
if (typeof session === "undefined") { if (typeof session === "undefined") {
chatHeader.style.display = "none"; chatHeader.style.display = "none";
} else { } else {
chatHeader.children[0].appendChild(generateAvatar(session.name)); chatHeader.children[0].appendChild(generateAvatar(currentSessionId, session.name, avatarTimestamps.get(currentSessionId)));
chatHeader.children[0].appendChild(generateName(session.name)); chatHeader.children[0].appendChild(generateName(session.name));
chatHeader.style.display = "flex"; chatHeader.style.display = "flex";
if (session.isContact) { if (session.isContact) {
@ -954,7 +1008,7 @@ function generateName(name) {
function generateSession(sessionId, session) { function generateSession(sessionId, session) {
let li = document.createElement("li"); let li = document.createElement("li");
li.setAttribute("data-sessionId", sessionId); li.setAttribute("data-sessionId", sessionId);
li.appendChild(generateAvatar(session.name)); li.appendChild(generateAvatar(sessionId, session.name, avatarTimestamps.get(sessionId)));
li.appendChild(generateName(session.name)); li.appendChild(generateName(session.name));
if (session.isContact) { if (session.isContact) {
li.classList.add("is_contact"); li.classList.add("is_contact");
@ -973,16 +1027,23 @@ function generateSession(sessionId, session) {
li.onclick = onClickSession; li.onclick = onClickSession;
return li; return li;
} }
function generateMsgHeader(name) { function generateMsgHeader(name, sessionId) {
let p = document.createElement("p"); let p = document.createElement("p");
p.appendChild(document.createTextNode(name)); p.appendChild(document.createTextNode(name));
let div = document.createElement("div"); let div = document.createElement("div");
div.classList.add("header"); div.classList.add("header");
div.appendChild(generateAvatar(name)); let timestamp = avatarTimestamps.get(sessionId);
let avatar;
if (typeof sessionId === "undefined") {
avatar = generateSelfAvatar(timestamp);
} else {
avatar = generateAvatar(sessionId, name, timestamp);
}
div.appendChild(avatar);
div.appendChild(p); div.appendChild(p);
return div; return div;
} }
function generateMessage(name, msg) { function generateMessage(name, sessionId, msg) {
let p = document.createElement("p"); let p = document.createElement("p");
p.appendChild(document.createTextNode(msg)); p.appendChild(document.createTextNode(msg));
let div = document.createElement("div"); let div = document.createElement("div");
@ -990,12 +1051,12 @@ function generateMessage(name, msg) {
div.appendChild(linkifyElement(p)); div.appendChild(linkifyElement(p));
let li = document.createElement("li"); let li = document.createElement("li");
if (typeof name !== "undefined") { if (typeof name !== "undefined") {
li.appendChild(generateMsgHeader(name)); li.appendChild(generateMsgHeader(name, sessionId));
} }
li.appendChild(div); li.appendChild(div);
return li; return li;
} }
function generateFile(name, outgoing, file_info) { function generateFile(name, sessionId, outgoing, file_info) {
let div1 = document.createElement("div"); let div1 = document.createElement("div");
div1.classList.add("file"); div1.classList.add("file");
div1.classList.add("content"); div1.classList.add("content");
@ -1017,7 +1078,7 @@ function generateFile(name, outgoing, file_info) {
div1.appendChild(a); div1.appendChild(a);
let li = document.createElement("li"); let li = document.createElement("li");
if (typeof name !== "undefined") { if (typeof name !== "undefined") {
li.appendChild(generateMsgHeader(name)); li.appendChild(generateMsgHeader(name, sessionId));
} }
li.appendChild(div1); li.appendChild(div1);
return li; return li;
@ -1090,18 +1151,20 @@ function displayHistory(scrollToBottom = true) {
let previousOutgoing = undefined; let previousOutgoing = undefined;
msgHistory.get(currentSessionId).forEach(entry => { msgHistory.get(currentSessionId).forEach(entry => {
let name = undefined; let name = undefined;
let sessionId = undefined;
if (previousOutgoing != entry[0]) { if (previousOutgoing != entry[0]) {
previousOutgoing = entry[0]; previousOutgoing = entry[0];
if (entry[0]) { //outgoing msg if (entry[0]) { //outgoing msg
name = identityName; name = identityName;
} else { } else {
name = session.name; name = session.name;
sessionId = currentSessionId;
} }
} }
if (entry[1]) { //is file 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 { } else {
msg_log.appendChild(generateMessage(name, entry[2])); msg_log.appendChild(generateMessage(name, sessionId, entry[2]));
} }
}); });
if (scrollToBottom) { if (scrollToBottom) {

View File

@ -63,9 +63,9 @@
padding: 10px 50px; padding: 10px 50px;
} }
.avatar { .avatar {
width: 2em; width: 7em;
height: 2em; height: 7em;
font-size: 3em; display: block;
margin: auto; margin: auto;
} }
#identity h2 { #identity h2 {
@ -157,7 +157,7 @@
} }
} else { } else {
let identity = document.getElementById("identity"); let identity = document.getElementById("identity");
identity.appendChild(generateAvatar(identity_name)); identity.appendChild(generateSelfAvatar(Date.now()));
let h2 = document.createElement("h2"); let h2 = document.createElement("h2");
h2.textContent = identity_name; h2.textContent = identity_name;
identity.appendChild(h2); identity.appendChild(h2);

View File

@ -11,6 +11,7 @@ use crate::{constants, crypto, key_value_table::KeyValueTable, print_error, util
const MAIN_TABLE: &str = "main"; const MAIN_TABLE: &str = "main";
const CONTACTS_TABLE: &str = "contacts"; const CONTACTS_TABLE: &str = "contacts";
const FILES_TABLE: &str = "files"; const FILES_TABLE: &str = "files";
const AVATARS_TABLE: &str = "avatars";
const DATABASE_CORRUPED_ERROR: &str = "Database corrupted"; const DATABASE_CORRUPED_ERROR: &str = "Database corrupted";
@ -21,6 +22,7 @@ impl<'a> DBKeys {
pub const SALT: &'a str = "salt"; pub const SALT: &'a str = "salt";
pub const MASTER_KEY: &'a str = "master_key"; pub const MASTER_KEY: &'a str = "master_key";
pub const USE_PADDING: &'a str = "use_padding"; pub const USE_PADDING: &'a str = "use_padding";
pub const AVATAR: &'a str = "avatar";
} }
fn bool_to_byte(b: bool) -> u8 { fn bool_to_byte(b: bool) -> u8 {
@ -53,6 +55,7 @@ pub struct Contact {
pub uuid: Uuid, pub uuid: Uuid,
pub public_key: [u8; PUBLIC_KEY_LENGTH], pub public_key: [u8; PUBLIC_KEY_LENGTH],
pub name: String, pub name: String,
pub avatar: Option<Uuid>,
pub verified: bool, pub verified: bool,
pub seen: bool, pub seen: bool,
} }
@ -70,19 +73,23 @@ impl Identity {
self.keypair.public.to_bytes() self.keypair.public.to_bytes()
} }
pub fn add_contact(&self, name: String, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result<Contact, rusqlite::Error> { pub fn add_contact(&self, name: String, avatar_uuid: Option<Uuid>, public_key: [u8; PUBLIC_KEY_LENGTH]) -> Result<Contact, rusqlite::Error> {
let db = Connection::open(get_database_path())?; 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 contact_uuid = Uuid::new_v4();
let encrypted_name = crypto::encrypt_data(name.as_bytes(), &self.master_key).unwrap(); 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_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_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(); 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 { Ok(Contact {
uuid: contact_uuid, uuid: contact_uuid,
public_key: public_key, public_key: public_key,
name: name, name: name,
avatar: avatar_uuid,
verified: false, verified: false,
seen: true, seen: true,
}) })
@ -91,7 +98,7 @@ impl Identity {
pub fn remove_contact(uuid: &Uuid) -> Result<usize, rusqlite::Error> { pub fn remove_contact(uuid: &Uuid) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?; let db = Connection::open(get_database_path())?;
Identity::delete_conversation(uuid)?; 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<usize, rusqlite::Error> { pub fn set_verified(&self, uuid: &Uuid) -> Result<usize, rusqlite::Error> {
@ -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()]) 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<usize, rusqlite::Error> {
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<usize, rusqlite::Error> { pub fn set_contact_seen(&self, uuid: &Uuid, seen: bool) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?; let db = Connection::open(get_database_path())?;
let encrypted_seen = crypto::encrypt_data(&[bool_to_byte(seen)], &self.master_key).unwrap(); 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<Vec<Contact>> { pub fn load_contacts(&self) -> Option<Vec<Contact>> {
match Connection::open(get_database_path()) { match Connection::open(get_database_path()) {
Ok(db) => { Ok(db) => {
match db.prepare(&("SELECT uuid, name, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) { if let Ok(mut stmt) = db.prepare(&("SELECT uuid, name, avatar, key, verified, seen FROM ".to_owned()+CONTACTS_TABLE)) {
Ok(mut stmt) => { let mut rows = stmt.query([]).unwrap();
let mut rows = stmt.query([]).unwrap(); let mut contacts = Vec::new();
let mut contacts = Vec::new(); while let Ok(Some(row)) = rows.next() {
while let Some(row) = rows.next().unwrap() { let encrypted_public_key: Vec<u8> = row.get(3).unwrap();
let encrypted_public_key: Vec<u8> = row.get(2).unwrap(); match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) {
match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) { Ok(public_key) => {
Ok(public_key) => { let encrypted_name: Vec<u8> = row.get(1).unwrap();
if public_key.len() == PUBLIC_KEY_LENGTH { match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) {
let encrypted_name: Vec<u8> = row.get(1).unwrap(); Ok(name) => {
match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) { let encrypted_verified: Vec<u8> = row.get(4).unwrap();
Ok(name) => { match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) {
let encrypted_verified: Vec<u8> = row.get(3).unwrap(); Ok(verified) => {
match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) { let encrypted_seen: Vec<u8> = row.get(5).unwrap();
Ok(verified) => { match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) {
let encrypted_seen: Vec<u8> = row.get(4).unwrap(); Ok(seen) => {
match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) { let contact_uuid: Vec<u8> = row.get(0).unwrap();
Ok(seen) => { let avatar_result: Result<Vec<u8>, rusqlite::Error> = row.get(2);
let uuid: Vec<u8> = row.get(0).unwrap(); let avatar = match avatar_result {
match to_uuid_bytes(&uuid) { Ok(avatar_uuid) => Some(Uuid::from_bytes(to_uuid_bytes(&avatar_uuid).unwrap())),
Some(uuid_bytes) => { Err(_) => None
contacts.push(Contact { };
uuid: Uuid::from_bytes(uuid_bytes), contacts.push(Contact {
public_key: public_key.try_into().unwrap(), uuid: Uuid::from_bytes(to_uuid_bytes(&contact_uuid).unwrap()),
name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(), public_key: public_key.try_into().unwrap(),
verified: byte_to_bool(verified[0]).unwrap(), name: std::str::from_utf8(name.as_slice()).unwrap().to_owned(),
seen: byte_to_bool(seen[0]).unwrap(), avatar,
}) verified: byte_to_bool(verified[0]).unwrap(),
} seen: byte_to_bool(seen[0]).unwrap(),
None => {} })
}
}
Err(e) => print_error!(e)
}
} }
Err(e) => print_error!(e) Err(e) => print_error!(e)
} }
} }
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) => { return Some(contacts);
print_error!(e); }
None
}
}
}
Err(e) => {
print_error!(e);
None
} }
Err(e) => print_error!(e)
} }
None
} }
pub fn clear_temporary_files() -> Result<usize, rusqlite::Error> { #[allow(unused_must_use)]
pub fn clear_cache() -> Result<(), rusqlite::Error> {
let db = Connection::open(get_database_path())?; 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<Vec<u8>> { pub fn load_file(&self, uuid: Uuid) -> Option<Vec<u8>> {
match Connection::open(get_database_path()) { match Connection::open(get_database_path()) {
Ok(db) => { Ok(db) => {
match db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)) { let mut stmt = db.prepare(&format!("SELECT uuid, data FROM \"{}\"", FILES_TABLE)).unwrap();
Ok(mut stmt) => { let mut rows = stmt.query([]).unwrap();
let mut rows = stmt.query([]).unwrap(); while let Ok(Some(row)) = rows.next() {
while let Some(row) = rows.next().unwrap() { let encrypted_uuid: Vec<u8> = row.get(0).unwrap();
let encrypted_uuid: Vec<u8> = row.get(0).unwrap(); match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){
match crypto::decrypt_data(encrypted_uuid.as_slice(), &self.master_key){ Ok(test_uuid) => {
Ok(test_uuid) => { if test_uuid == uuid.as_bytes() {
if test_uuid == uuid.as_bytes() { let encrypted_data: Vec<u8> = row.get(1).unwrap();
let encrypted_data: Vec<u8> = row.get(1).unwrap(); match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) { Ok(data) => return Some(data),
Ok(data) => return Some(data), Err(e) => print_error!(e)
Err(e) => print_error!(e)
}
}
} }
Err(e) => print_error!(e)
} }
} }
None Err(e) => print_error!(e)
}
Err(e) => {
print_error!(e);
None
} }
} }
} }
Err(e) => { Err(e) => print_error!(e)
print_error!(e);
None
}
} }
None
} }
pub fn store_file(&self, contact_uuid: Option<Uuid>, data: &[u8]) -> Result<Uuid, rusqlite::Error> { pub fn store_file(&self, contact_uuid: Option<Uuid>, data: &[u8]) -> Result<Uuid, rusqlite::Error> {
@ -244,73 +238,44 @@ impl Identity {
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<(bool, Vec<u8>)>> { pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<(bool, Vec<u8>)>> {
match Connection::open(get_database_path()) { match Connection::open(get_database_path()) {
Ok(db) => { Ok(db) => {
match db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) { if let Ok(mut stmt) = db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
Ok(mut stmt) => { let mut rows = stmt.query([]).unwrap();
let mut rows = stmt.query([]).unwrap(); if let Ok(Some(row)) = rows.next() {
match rows.next() { let total: usize = row.get(0).unwrap();
Ok(row) => if row.is_some() { if offset < total {
let total: usize = row.unwrap().get(0).unwrap(); if offset+count >= total {
if offset >= total { count = total-offset;
None }
} else { let mut stmt = db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap();
if offset+count >= total { let mut rows = stmt.query([]).unwrap();
count = total-offset; let mut msgs = Vec::new();
} while let Ok(Some(row)) = rows.next() {
match db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)) { let encrypted_outgoing: Vec<u8> = row.get(0).unwrap();
Ok(mut stmt) => { match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){
let mut rows = stmt.query([]).unwrap(); Ok(outgoing) => {
let mut msgs = Vec::new(); match byte_to_bool(outgoing[0]) {
while let Some(row) = rows.next().unwrap() { Ok(outgoing) => {
let encrypted_outgoing: Vec<u8> = row.get(0).unwrap(); let encrypted_data: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_outgoing.as_slice(), &self.master_key){ match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
Ok(outgoing) => { Ok(data) => msgs.push((outgoing, data)),
match byte_to_bool(outgoing[0]) {
Ok(outgoing) => {
let encrypted_data: Vec<u8> = 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(_) => {}
}
}
Err(e) => print_error!(e) Err(e) => print_error!(e)
} }
} }
Some(msgs) Err(_) => {}
}
Err(e) => {
print_error!(e);
None
} }
} }
Err(e) => print_error!(e)
} }
} else {
None
} }
Err(_) => None return Some(msgs);
} }
} }
Err(e) => {
print_error!(e);
None
}
} }
} }
Err(e) => { Err(e) => print_error!(e)
print_error!(e);
None
}
} }
None
} }
#[allow(unused_must_use)] #[allow(unused_must_use)]
@ -336,6 +301,29 @@ impl Identity {
db.update(DBKeys::USE_PADDING, &encrypted_use_padding) db.update(DBKeys::USE_PADDING, &encrypted_use_padding)
} }
pub fn store_avatar(&self, avatar: &[u8]) -> Result<Uuid, rusqlite::Error> {
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<Vec<u8>> {
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<u8> = 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){ pub fn zeroize(&mut self){
self.master_key.zeroize(); self.master_key.zeroize();
self.keypair.secret.zeroize(); self.keypair.secret.zeroize();
@ -480,6 +468,16 @@ impl Identity {
} }
} }
pub fn set_identity_avatar(avatar: &[u8]) -> Result<usize, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
db.upsert(DBKeys::AVATAR, avatar)
}
pub fn get_identity_avatar() -> Result<Vec<u8>, rusqlite::Error> {
let db = KeyValueTable::new(&get_database_path(), MAIN_TABLE)?;
db.get(DBKeys::AVATAR)
}
pub fn delete_identity() -> Result<(), std::io::Error> { pub fn delete_identity() -> Result<(), std::io::Error> {
std::fs::remove_file(get_database_path()) std::fs::remove_file(get_database_path())
} }

View File

@ -28,7 +28,7 @@ impl<'a> KeyValueTable<'a> {
pub fn update(&self, key: &str, value: &[u8]) -> Result<usize, Error> { pub fn update(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
self.db.execute(&format!("UPDATE {} SET value=? WHERE key=\"{}\"", self.table_name, key), params![value]) self.db.execute(&format!("UPDATE {} SET value=? WHERE key=\"{}\"", self.table_name, key), params![value])
} }
/*pub fn upsert(&self, key: &str, value: &[u8]) -> Result<usize, Error> { pub fn upsert(&self, key: &str, value: &[u8]) -> Result<usize, Error> {
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]) 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])
}*/ }
} }

View File

@ -9,6 +9,7 @@ mod constants;
mod discovery; mod discovery;
use std::{env, fs, io, net::SocketAddr, str::{FromStr, from_utf8}, sync::{Arc, RwLock}}; 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 tokio::{net::TcpListener, runtime::Handle, sync::mpsc};
use actix_web::{App, HttpMessage, HttpRequest, HttpResponse, HttpServer, http::{header, CookieBuilder}, web, web::Data}; use actix_web::{App, HttpMessage, HttpRequest, HttpResponse, HttpServer, http::{header, CookieBuilder}, web, web::Data};
use actix_multipart::Multipart; use actix_multipart::Multipart;
@ -198,7 +199,7 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
} }
"contact" => { "contact" => {
let session_id: usize = args[1].parse().unwrap(); 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(_) => {}, Ok(_) => {},
Err(e) => print_error!(e) Err(e) => print_error!(e)
} }
@ -224,10 +225,10 @@ async fn websocket_worker(mut ui_connection: UiConnection, global_vars: Arc<RwLo
Err(e) => print_error!(e) Err(e) => print_error!(e)
} }
} }
"ask_name" => { "refresh_profile" => {
let session_id: usize = args[1].parse().unwrap(); let session_id: usize = args[1].parse().unwrap();
session_manager.send_command(&session_id, SessionCommand::Send { session_manager.send_command(&session_id, SessionCommand::Send {
buff: protocol::ask_name() buff: protocol::ask_profile_info()
}).await; }).await;
} }
"set_use_padding" => { "set_use_padding" => {
@ -303,12 +304,86 @@ fn is_authenticated(req: &HttpRequest) -> bool {
false 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::<Data<Arc<RwLock<GlobalVars>>>>().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<Vec<u8>>, 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::<Data<Arc<RwLock<GlobalVars>>>>().unwrap();
return reply_with_avatar(global_vars.read().unwrap().session_manager.get_avatar(&session_id), splits[2]);
}
}
HttpResponse::BadRequest().finish()
}
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
struct FileInfo { struct FileInfo {
uuid: String, uuid: String,
file_name: String, file_name: String,
} }
fn handle_load_file(req: HttpRequest, file_info: web::Query<FileInfo>) -> HttpResponse { fn handle_load_file(req: HttpRequest, file_info: web::Query<FileInfo>) -> HttpResponse {
if is_authenticated(&req) { if is_authenticated(&req) {
match Uuid::from_str(&file_info.uuid) { match Uuid::from_str(&file_info.uuid) {
@ -368,7 +443,9 @@ async fn handle_send_file(req: HttpRequest, mut payload: Multipart) -> HttpRespo
loop { loop {
chunk_buffer.extend(&pending_buffer); chunk_buffer.extend(&pending_buffer);
pending_buffer.clear(); pending_buffer.clear();
println!("Calling next()");
while let Some(Ok(chunk)) = field.next().await { while let Some(Ok(chunk)) = field.next().await {
println!("Not None");
if chunk_buffer.len()+chunk.len() <= constants::FILE_CHUNK_SIZE { if chunk_buffer.len()+chunk.len() <= constants::FILE_CHUNK_SIZE {
chunk_buffer.extend(chunk); chunk_buffer.extend(chunk);
} else if chunk_buffer.len() == constants::FILE_CHUNK_SIZE { } 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; break;
} }
} }
println!("May be None");
if !global_vars_read.session_manager.send_command(&session_id, SessionCommand::EncryptFileChunk{ if !global_vars_read.session_manager.send_command(&session_id, SessionCommand::EncryptFileChunk{
plain_text: chunk_buffer.clone() plain_text: chunk_buffer.clone()
}).await { }).await {
@ -457,7 +535,7 @@ fn login(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpRespo
} }
fn on_identity_loaded(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse { fn on_identity_loaded(identity: Identity, global_vars: &Arc<RwLock<GlobalVars>>) -> HttpResponse {
match Identity::clear_temporary_files() { match Identity::clear_cache() {
Ok(_) => {}, Ok(_) => {},
Err(e) => print_error!(e) Err(e) => print_error!(e)
} }
@ -571,13 +649,13 @@ async fn handle_index(req: HttpRequest) -> HttpResponse {
const JS_CONTENT_TYPE: &str = "text/javascript"; const JS_CONTENT_TYPE: &str = "text/javascript";
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
fn replace_css(file_path: &str) -> String { fn replace_fields(file_path: &str) -> String {
use yaml_rust::YamlLoader; use yaml_rust::YamlLoader;
let mut content = fs::read_to_string(file_path).unwrap(); 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 config = &YamlLoader::load_from_str(&fs::read_to_string("config.yml").unwrap()).unwrap()[0];
let css_values = config["css"].as_hash().unwrap(); let fields = config.as_hash().unwrap();
css_values.into_iter().for_each(|entry| { fields.into_iter().for_each(|field| {
content = content.replace(entry.0.as_str().unwrap(), entry.1.as_str().unwrap()); content = content.replace(field.0.as_str().unwrap(), field.1.as_str().unwrap());
}); });
content content
} }
@ -596,7 +674,7 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
} }
"index.css" => { "index.css" => {
#[cfg(debug_assertions)] #[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))] #[cfg(not(debug_assertions))]
return response_builder.body(include_str!(concat!(env!("OUT_DIR"), "/index.css"))); return response_builder.body(include_str!(concat!(env!("OUT_DIR"), "/index.css")));
} }
@ -655,7 +733,7 @@ fn handle_static(req: HttpRequest) -> HttpResponse {
} }
"style.css" => { "style.css" => {
#[cfg(debug_assertions)] #[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))] #[cfg(not(debug_assertions))]
return response_builder.body(include_str!(concat!(env!("OUT_DIR"), "/commons/style.css"))); 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<RwLock<GlobalVars>>) -> io::Result<(
.route("/send_file", web::post().to(handle_send_file)) .route("/send_file", web::post().to(handle_send_file))
.route("/send_large_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("/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("/static/.*", web::get().to(handle_static))
.route("/logout", web::get().to(handle_logout)) .route("/logout", web::get().to(handle_logout))
} }

View File

@ -5,26 +5,27 @@ pub struct Headers;
impl Headers { impl Headers {
pub const MESSAGE: u8 = 0x00; pub const MESSAGE: u8 = 0x00;
pub const ASK_NAME: u8 = 0x01; pub const FILE: u8 = 0x01;
pub const TELL_NAME: u8 = 0x02; pub const ASK_PROFILE_INFO: u8 = 0x02;
pub const FILE: u8 = 0x03; pub const NAME: u8 = 0x03;
pub const ASK_LARGE_FILES: u8 = 0x04; pub const AVATAR: u8 = 0x04;
pub const ACCEPT_LARGE_FILES: u8 = 0x05; pub const ASK_LARGE_FILES: u8 = 0x05;
pub const LARGE_FILE_CHUNK: u8 = 0x06; pub const ACCEPT_LARGE_FILES: u8 = 0x06;
pub const ACK_CHUNK: u8 = 0x07; pub const LARGE_FILE_CHUNK: u8 = 0x07;
pub const ABORT_FILES_TRANSFER: u8 = 0x08; pub const ACK_CHUNK: u8 = 0x08;
pub const ABORT_FILES_TRANSFER: u8 = 0x09;
} }
pub fn new_message(message: String) -> Vec<u8> { pub fn new_message(message: String) -> Vec<u8> {
[&[Headers::MESSAGE], message.as_bytes()].concat() [&[Headers::MESSAGE], message.as_bytes()].concat()
} }
pub fn ask_name() -> Vec<u8> { pub fn ask_profile_info() -> Vec<u8> {
vec![Headers::ASK_NAME] vec![Headers::ASK_PROFILE_INFO]
} }
pub fn tell_name(name: &str) -> Vec<u8> { pub fn name(name: &str) -> Vec<u8> {
[&[Headers::TELL_NAME], name.as_bytes()].concat() [&[Headers::NAME], name.as_bytes()].concat()
} }
pub fn file(file_name: &str, buffer: &[u8]) -> Vec<u8> { pub fn file(file_name: &str, buffer: &[u8]) -> Vec<u8> {
@ -79,4 +80,8 @@ pub fn parse_ask_files(buffer: &[u8]) -> Option<Vec<(u64, String)>> {
} }
} }
Some(files_info) Some(files_info)
}
pub fn avatar(avatar: &[u8]) -> Vec<u8> {
[&[Headers::AVATAR], avatar].concat()
} }

View File

@ -40,6 +40,7 @@ pub struct LargeFilesDownload {
#[derive(Clone)] #[derive(Clone)]
pub struct SessionData { pub struct SessionData {
pub name: String, pub name: String,
avatar: Option<Uuid>,
pub outgoing: bool, pub outgoing: bool,
pub peer_public_key: [u8; PUBLIC_KEY_LENGTH], pub peer_public_key: [u8; PUBLIC_KEY_LENGTH],
pub ip: IpAddr, pub ip: IpAddr,
@ -54,7 +55,7 @@ pub struct SessionManager {
ui_connection: Mutex<Option<UiConnection>>, ui_connection: Mutex<Option<UiConnection>>,
loaded_contacts: RwLock<HashMap<usize, Contact>>, loaded_contacts: RwLock<HashMap<usize, Contact>>,
pub last_loaded_msg_offsets: RwLock<HashMap<usize, usize>>, pub last_loaded_msg_offsets: RwLock<HashMap<usize, usize>>,
pub saved_msgs: Mutex<HashMap<usize, Vec<(bool, Vec<u8>)>>>, pub saved_msgs: RwLock<HashMap<usize, Vec<(bool, Vec<u8>)>>>,
pub not_seen: RwLock<Vec<usize>>, pub not_seen: RwLock<Vec<usize>>,
mdns_service: Mutex<Option<Service>>, mdns_service: Mutex<Option<Service>>,
listener_stop_signal: Mutex<Option<Sender<()>>>, listener_stop_signal: Mutex<Option<Sender<()>>>,
@ -72,6 +73,10 @@ impl SessionManager {
} }
} }
fn get_all_senders(&self) -> Vec<Sender<SessionCommand>> {
self.sessions.read().unwrap().iter().map(|i| i.1.sender.clone()).collect()
}
async fn encrypt_and_send<T: PsecWriter>(&self, writer: &mut T, buff: &[u8]) -> Result<(), PsecError> { async fn encrypt_and_send<T: PsecWriter>(&self, writer: &mut T, buff: &[u8]) -> Result<(), PsecError> {
let use_padding = self.identity.read().unwrap().as_ref().unwrap().use_padding; let use_padding = self.identity.read().unwrap().as_ref().unwrap().use_padding;
writer.encrypt_and_send(buff, use_padding).await writer.encrypt_and_send(buff, use_padding).await
@ -96,7 +101,7 @@ impl SessionManager {
} }
} }
if !msg_saved { 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); ui_connection.on_disconnected(&session_id);
}); });
self.sessions.write().unwrap().remove(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); self.not_seen.write().unwrap().retain(|x| x != session_id);
} }
@ -172,64 +177,6 @@ impl SessionManager {
let session_read = result.1; let session_read = result.1;
receiving.set(session_read.into_receive_and_decrypt()); receiving.set(session_read.into_receive_and_decrypt());
match buffer[0] { 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<LargeFileDownload> = 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 => { protocol::Headers::LARGE_FILE_CHUNK => {
let mut should_accept_chunk = false; let mut should_accept_chunk = false;
{ {
@ -322,6 +269,103 @@ impl SessionManager {
ui_connection.on_received(&session_id, &buffer); 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<LargeFileDownload> = 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 header = buffer[0];
let buffer = match header { let buffer = match header {
@ -421,11 +465,7 @@ impl SessionManager {
let mut peer_public_key = [0; PUBLIC_KEY_LENGTH]; let mut peer_public_key = [0; PUBLIC_KEY_LENGTH];
let session = { let session = {
let identity = { let identity = {
let identity_opt = session_manager.identity.read().unwrap(); session_manager.identity.read().unwrap().clone()
match identity_opt.as_ref() {
Some(identity) => Some(identity.clone()),
None => None
}
}; };
match identity { match identity {
Some(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 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 (sender, receiver) = mpsc::channel(32);
let session_data = SessionData{ let session_data = SessionData {
name: ip.to_string(), name: ip.to_string(),
avatar: None,
outgoing, outgoing,
peer_public_key, peer_public_key,
ip, ip,
@ -485,7 +526,7 @@ impl SessionManager {
*session_counter += 1; *session_counter += 1;
} }
let session_id = session_id.unwrap(); 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)) Some((session_id, receiver))
} else { } else {
None 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); ui_connection.on_new_session(&session_id, &ip.to_string(), outgoing, &crypto::generate_fingerprint(&peer_public_key), ip, None);
}); });
if !is_contact { 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(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
print_error!(e); print_error!(e);
@ -542,7 +583,7 @@ impl SessionManager {
} }
pub fn get_saved_msgs(&self) -> HashMap<usize, Vec<(bool, Vec<u8>)>> { pub fn get_saved_msgs(&self) -> HashMap<usize, Vec<(bool, Vec<u8>)>> {
self.saved_msgs.lock().unwrap().clone() self.saved_msgs.read().unwrap().clone()
} }
pub fn set_seen(&self, session_id: usize, seen: bool) { 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> { pub fn add_contact(&self, session_id: usize) -> 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)?; 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.loaded_contacts.write().unwrap().insert(session_id, contact);
self.last_loaded_msg_offsets.write().unwrap().insert(session_id, 0); self.last_loaded_msg_offsets.write().unwrap().insert(session_id, 0);
Ok(()) Ok(())
@ -581,6 +624,7 @@ impl SessionManager {
if let Some(contact) = loaded_contacts.remove(&session_id) { if let Some(contact) = loaded_contacts.remove(&session_id) {
if let Some(session) = self.sessions.write().unwrap().get_mut(&session_id) { if let Some(session) = self.sessions.write().unwrap().get_mut(&session_id) {
session.name = contact.name; session.name = contact.name;
session.avatar = contact.avatar;
} }
} }
self.last_loaded_msg_offsets.write().unwrap().remove(&session_id); 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); let result = Identity::delete_conversation(&self.loaded_contacts.read().unwrap().get(&session_id).unwrap().uuid);
if result.is_ok() { if result.is_ok() {
self.last_loaded_msg_offsets.write().unwrap().insert(session_id, 0); 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 result
} }
@ -627,15 +671,32 @@ impl SessionManager {
msgs msgs
} }
pub fn get_avatar(&self, session_id: &usize) -> Option<Vec<u8>> {
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)] #[allow(unused_must_use)]
pub async fn change_name(&self, new_name: String) -> Result<usize, rusqlite::Error> { pub async fn change_name(&self, new_name: String) -> Result<usize, rusqlite::Error> {
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); let result = self.identity.write().unwrap().as_mut().unwrap().change_name(new_name);
if result.is_ok() { if result.is_ok() {
let senders: Vec<Sender<SessionCommand>> = { for sender in self.get_all_senders().into_iter() {
self.sessions.read().unwrap().iter().map(|i| i.1.sender.clone()).collect()
};
for sender in senders.into_iter() {
sender.send(SessionCommand::Send { sender.send(SessionCommand::Send {
buff: telling_name.clone() buff: telling_name.clone()
}).await; }).await;
@ -659,7 +720,7 @@ impl SessionManager {
*self.ui_connection.lock().unwrap() = None; *self.ui_connection.lock().unwrap() = None;
*self.session_counter.write().unwrap() = 0; *self.session_counter.write().unwrap() = 0;
self.loaded_contacts.write().unwrap().clear(); 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 { pub fn is_identity_loaded(&self) -> bool {
@ -703,7 +764,7 @@ impl SessionManager {
ui_connection: Mutex::new(None), ui_connection: Mutex::new(None),
loaded_contacts: RwLock::new(HashMap::new()), loaded_contacts: RwLock::new(HashMap::new()),
last_loaded_msg_offsets: 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()), not_seen: RwLock::new(Vec::new()),
mdns_service: Mutex::new(None), mdns_service: Mutex::new(None),
listener_stop_signal: Mutex::new(None), listener_stop_signal: Mutex::new(None),

View File

@ -112,6 +112,9 @@ mod ui_messages {
pub fn on_name_told(session_id: &usize, name: &str) -> Message { pub fn on_name_told(session_id: &usize, name: &str) -> Message {
Message::from(format!("name_told {} {}", session_id, name)) 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 { 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)) 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) { pub fn on_name_told(&mut self, session_id: &usize, name: &str) {
self.write_message(ui_messages::on_name_told(session_id, name)); 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) { 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)); self.write_message(ui_messages::inc_files_transfer(session_id, chunk_size));