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"
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"

View File

@ -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"
yaml-rust = "0.4"
linked-hash-map = "0.5"

View File

@ -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<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))]
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() {

View File

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

View File

@ -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;
}

View File

@ -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%;
}

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;
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;

View File

@ -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<sessionIds.length; ++i) {
@ -787,7 +840,7 @@ function showSessionInfoPopup() {
if (typeof session !== "undefined") {
let mainDiv = document.createElement("div");
mainDiv.id = "session_info";
mainDiv.appendChild(generateAvatar(session.name));
mainDiv.appendChild(generateAvatar(currentSessionId, session.name, avatarTimestamps.get(currentSessionId)));
let nameDiv = document.createElement("div");
nameDiv.classList.add("name");
let h2 = document.createElement("h2");
@ -796,7 +849,7 @@ function showSessionInfoPopup() {
if (session.isOnline) {
let button = document.createElement("button");
button.onclick = function() {
socket.send("ask_name "+currentSessionId);
socket.send("refresh_profile "+currentSessionId);
};
nameDiv.appendChild(button);
}
@ -839,6 +892,7 @@ function addSession(sessionId, name, outgoing, fingerprint, ip, isContact, isVer
"isOnline": isOnline,
});
msgHistory.set(sessionId, []);
avatarTimestamps.set(sessionId, Date.now());
displaySessions();
}
function displaySessions() {
@ -860,7 +914,7 @@ function logout() {
}
function displayProfile() {
profile_div.innerHTML = "";
profile_div.appendChild(generateAvatar(identityName));
profile_div.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
let p = document.createElement("p");
p.textContent = identityName;
profile_div.appendChild(p);
@ -872,7 +926,7 @@ function displayHeader() {
if (typeof session === "undefined") {
chatHeader.style.display = "none";
} 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.style.display = "flex";
if (session.isContact) {
@ -954,7 +1008,7 @@ function generateName(name) {
function generateSession(sessionId, session) {
let li = document.createElement("li");
li.setAttribute("data-sessionId", sessionId);
li.appendChild(generateAvatar(session.name));
li.appendChild(generateAvatar(sessionId, session.name, avatarTimestamps.get(sessionId)));
li.appendChild(generateName(session.name));
if (session.isContact) {
li.classList.add("is_contact");
@ -973,16 +1027,23 @@ function generateSession(sessionId, session) {
li.onclick = onClickSession;
return li;
}
function generateMsgHeader(name) {
function generateMsgHeader(name, sessionId) {
let p = document.createElement("p");
p.appendChild(document.createTextNode(name));
let div = document.createElement("div");
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);
return div;
}
function generateMessage(name, msg) {
function generateMessage(name, sessionId, msg) {
let p = document.createElement("p");
p.appendChild(document.createTextNode(msg));
let div = document.createElement("div");
@ -990,12 +1051,12 @@ function generateMessage(name, msg) {
div.appendChild(linkifyElement(p));
let li = document.createElement("li");
if (typeof name !== "undefined") {
li.appendChild(generateMsgHeader(name));
li.appendChild(generateMsgHeader(name, sessionId));
}
li.appendChild(div);
return li;
}
function generateFile(name, outgoing, file_info) {
function generateFile(name, sessionId, outgoing, file_info) {
let div1 = document.createElement("div");
div1.classList.add("file");
div1.classList.add("content");
@ -1017,7 +1078,7 @@ function generateFile(name, outgoing, file_info) {
div1.appendChild(a);
let li = document.createElement("li");
if (typeof name !== "undefined") {
li.appendChild(generateMsgHeader(name));
li.appendChild(generateMsgHeader(name, sessionId));
}
li.appendChild(div1);
return li;
@ -1090,18 +1151,20 @@ function displayHistory(scrollToBottom = true) {
let previousOutgoing = undefined;
msgHistory.get(currentSessionId).forEach(entry => {
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) {

View File

@ -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);

View File

@ -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<Uuid>,
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<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())?;
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<usize, rusqlite::Error> {
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<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()])
}
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> {
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<Vec<Contact>> {
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<u8> = 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<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) {
Ok(name) => {
let encrypted_verified: Vec<u8> = row.get(3).unwrap();
match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) {
Ok(verified) => {
let encrypted_seen: Vec<u8> = row.get(4).unwrap();
match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) {
Ok(seen) => {
let uuid: Vec<u8> = 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<u8> = row.get(3).unwrap();
match crypto::decrypt_data(encrypted_public_key.as_slice(), &self.master_key) {
Ok(public_key) => {
let encrypted_name: Vec<u8> = row.get(1).unwrap();
match crypto::decrypt_data(encrypted_name.as_slice(), &self.master_key) {
Ok(name) => {
let encrypted_verified: Vec<u8> = row.get(4).unwrap();
match crypto::decrypt_data(encrypted_verified.as_slice(), &self.master_key) {
Ok(verified) => {
let encrypted_seen: Vec<u8> = row.get(5).unwrap();
match crypto::decrypt_data(encrypted_seen.as_slice(), &self.master_key) {
Ok(seen) => {
let contact_uuid: Vec<u8> = row.get(0).unwrap();
let avatar_result: Result<Vec<u8>, 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<usize, rusqlite::Error> {
#[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<Vec<u8>> {
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<u8> = 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<u8> = 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<u8> = 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<u8> = 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<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>)>> {
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<u8> = 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<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(_) => {}
}
}
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<u8> = 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<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)
}
}
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<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){
self.master_key.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> {
std::fs::remove_file(get_database_path())
}