Compare commits

...

17 Commits

Author SHA1 Message Date
0bd49c092d
Update dependencies 2022-05-04 13:31:02 +02:00
ede41e5715
Add AIRA logo 2021-09-02 19:45:32 +02:00
6c5bbc3f64
Better concurrency management 2021-08-29 21:19:59 +02:00
5ae61222f4
Make cargo clippy happy 2021-08-18 16:09:52 +02:00
dce526922e
Log websocket messages in debug build 2021-08-02 21:22:56 +02:00
8c8ede431c
Bug fix: set not seen before notifying UI 2021-08-02 21:19:50 +02:00
c4417cf802
Bug fixes: load old messages on scroll & show right avatar in pending messages 2021-08-02 21:18:06 +02:00
0e1c17973b
Update to PSEC v0.4 2021-07-25 16:02:17 +02:00
f2cfb6bda0
Refactoring ui_interface.rs 2021-07-23 12:36:37 +02:00
dda30bf5af
Pending messages 2021-07-23 12:04:29 +02:00
5081f9dbf4
Update screenshot 2021-06-20 15:32:40 +02:00
6b81cec83a
Decrease max message size to 16MB 2021-06-18 20:02:48 +02:00
40cab75f96
Add avatar as notifications icon 2021-06-16 16:19:40 +02:00
03a9df81ec
Message timestamps 2021-06-16 16:17:08 +02:00
3543ef2824
Handle websocket disconnection on UI 2021-06-04 13:14:29 +02:00
496c115fa9
Hide chat scrollbar when not needed & Always optimize scrypt crate 2021-06-04 12:40:31 +02:00
dc9fde39be
Adjust avatar sizes & Fix horizontal overflow in chat history 2021-05-31 15:45:24 +02:00
18 changed files with 1857 additions and 1579 deletions

1736
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "aira"
version = "0.0.3"
version = "0.1.1"
authors = ["Hardcore Sushi <hardcore.sushi@disroot.org>"]
edition = "2018"
exclude = ["src/frontend"]
@ -9,37 +9,41 @@ exclude = ["src/frontend"]
rand = "0.8"
rand-7 = { package = "rand", version = "0.7.3" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "net", "io-util"] }
async-psec = { version = "0.3", features = ["split"] }
async-psec = { version = "0.4", features = ["split"] }
lazy_static = "1.4"
socket2 = "0.4"
rusqlite = { version = "0.25.1", features = ["bundled"] }
ed25519-dalek = "1" #for singing
sha2 = "0.9"
rusqlite = { version = "0.27", features = ["bundled"] }
ed25519-dalek = "1" #for singatures
sha2 = "0.10"
aes-gcm = "0.9"
aes-gcm-siv = "0.10" #Database
hkdf = "0.11"
aes-gcm-siv = "0.10" #database encryption
hkdf = "0.12"
hex = "0.4"
actix-web = "3"
actix-multipart = "0.3"
time = "0.2" #needed for actix cookies
actix-web = "4"
env_logger = "0.9"
actix-multipart = "0.4"
time = "0.3" #needed for actix cookies
futures = "0.3"
tungstenite = "0.13" #websocket
serde = "1.0" #serialization
tungstenite = "0.17" #websocket
serde = { version = "1.0", features = ["derive"] } #serialization
html-escape = "0.2"
sanitize-filename = "0.3"
platform-dirs = "0.3"
uuid = { version = "0.8", features = ["v4"] }
webbrowser = "0.5"
uuid = { version = "1.0", features = ["v4"] }
webbrowser = "0.7"
libmdns = "0.6" #mDNS advertiser
multicast_dns = "0.5" #mDNS browser
if-addrs = "0.6"
if-addrs = "0.7"
base64 = "0.13"
scrypt = "0.7"
zeroize = "1.2"
image = "0.23"
scrypt = "0.10"
zeroize = "1.5"
image = "0.24"
yaml-rust = "0.4" #only in debug mode
[build-dependencies]
html-minifier = "3.0"
yaml-rust = "0.4"
linked-hash-map = "0.5"
[profile.dev.package.scrypt]
opt-level = 3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -4,4 +4,5 @@ pub const APPLICATION_FOLDER: &str = "AIRA";
pub const DB_NAME: &str = "AIRA.db";
pub const HTTP_COOKIE_NAME: &str = "aira_auth";
pub const MSG_LOADING_COUNT: usize = 20;
pub const FILE_CHUNK_SIZE: usize = 1023996;
pub const FILE_CHUNK_SIZE: usize = 1_023_996;
pub const MAX_RECV_SIZE: usize = 16_383_996;

View File

@ -34,8 +34,8 @@ label {
.avatar {
margin-right: .5em;
width: 1.5em;
height: 1.5em;
width: 2.5em;
height: 2.5em;
border-radius: 50%;
}

View File

@ -0,0 +1,79 @@
<svg width="191.7mm" height="168.39mm" version="1.1" viewBox="0 0 191.7 168.39" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-9.1475 -20.806)">
<path d="m81.908 189.19c14.507-12.545 13.641-67.907 13.641-67.907l18.56 2e-5s-0.86524 55.362 13.641 67.907z" fill="#803300"/>
<g fill="#19f52c">
<g fill-rule="evenodd">
<circle cx="19.053" cy="67.31" r="8.6808"/>
<circle cx="40.006" cy="88.061" r="8.6808"/>
<circle cx="79.997" cy="99.739" r="8.6808"/>
<circle cx="68.203" cy="133.44" r="8.6808"/>
<circle cx="105.86" cy="74.321" r="8.6808"/>
<circle cx="38.754" cy="131.9" r="8.6808"/>
<circle cx="127.11" cy="124.1" r="8.6808"/>
<circle cx="97.819" cy="119.49" r="8.6808"/>
<circle cx="19.67" cy="109.42" r="8.6808"/>
<circle cx="48.138" cy="59.715" r="8.6808"/>
<circle cx="95.703" cy="44.581" r="8.6808"/>
<circle cx="77.134" cy="67.671" r="8.6808"/>
<circle cx="121.04" cy="29.486" r="8.6808"/>
<circle cx="119.73" cy="98.423" r="8.6808"/>
<circle cx="51.945" cy="108.83" r="8.6808"/>
</g>
<g stroke="#19f52c" stroke-width="1.8939">
<path d="m38.754 131.9-19.084-22.481"/>
<path d="m19.67 109.42 20.336-21.356"/>
<path d="m19.053 67.31 20.953 20.75"/>
<path d="m51.945 108.83 16.258 24.602"/>
<path d="m51.945 108.83 28.052-9.0944"/>
<path d="m48.138 59.715-8.132 28.346"/>
<path d="m48.138 59.715 28.996 7.956"/>
<path d="m77.134 67.671 28.73 6.6502"/>
<path d="m77.134 67.671 18.569-23.089"/>
<path d="m95.703 44.581 25.333-15.095"/>
<path d="m105.86 74.321 13.863 24.103"/>
<path d="m73.582 133.41 24.237-13.916"/>
<path d="m97.819 119.49 21.907-21.071"/>
<path d="m97.819 119.49 29.289 4.6056"/>
<path d="m17.828 153.33 20.926-21.434"/>
</g>
<circle cx="17.828" cy="153.33" r="8.6808" fill-rule="evenodd"/>
<circle cx="53.865" cy="159.2" r="8.6808" fill-rule="evenodd"/>
<path d="m38.754 131.9 15.111 27.303" stroke="#19f52c" stroke-width="1.8939"/>
<path d="m127.11 124.1 13.863 24.103" stroke="#19f52c" stroke-width="1.8939"/>
<circle cx="140.97" cy="148.2" r="8.6808" fill-rule="evenodd"/>
<path d="m140.97 148.2 20.559-17.279" stroke="#19f52c" stroke-width="1.8939"/>
<g fill-rule="evenodd">
<circle cx="161.53" cy="130.92" r="8.6808"/>
<circle cx="148.77" cy="105.42" r="8.6808"/>
<circle cx="161.93" cy="80.961" r="8.6808"/>
<circle cx="134.44" cy="73.351" r="8.6808"/>
<circle cx="172.46" cy="44.584" r="8.6808"/>
<circle cx="192.17" cy="132.75" r="8.6808"/>
<circle cx="175.39" cy="155.03" r="8.6808"/>
<circle cx="68.229" cy="33.867" r="8.6808"/>
<circle cx="143.95" cy="45.376" r="8.6808"/>
</g>
<g stroke="#19f52c" stroke-width="1.8939">
<path d="m161.53 130.92 13.863 24.103"/>
<path d="m175.39 155.03 16.778-22.271"/>
<path d="m119.73 98.423 29.041 6.9934"/>
<path d="m148.77 105.42 12.763 25.506"/>
<path d="m105.86 74.321 28.58-0.96983"/>
<path d="m148.77 105.42 29.455-1.4191"/>
<path d="m143.95 45.376 28.511-0.79158"/>
<path d="m121.04 29.486 22.914 15.89"/>
<path d="m38.754 131.9 29.449 1.5377"/>
<path d="m77.134 67.671 2.8632 32.068"/>
<path d="m68.229 33.867 27.474 10.715"/>
<path d="m48.138 59.715 20.091-25.848"/>
<path d="m143.95 45.376-9.5064 27.975"/>
</g>
<circle cx="178.22" cy="104" r="8.6808" fill-rule="evenodd"/>
<path d="m172.46 44.584 16.663 26.606" stroke="#19f52c" stroke-width="1.8939"/>
<path d="m134.44 73.351 27.488 7.6103" stroke="#19f52c" stroke-width="1.8939"/>
<circle cx="189.12" cy="71.19" r="8.6808" fill-rule="evenodd"/>
<path d="m189.12 71.19-27.193 9.7712" stroke="#19f52c" stroke-width="1.8939"/>
<path d="m161.93 80.961 16.292 23.036" stroke="#19f52c" stroke-width="1.8939"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -120,7 +120,7 @@ button:hover::after {
position: relative;
}
#avatarContainer .avatar {
font-size: 4em;
font-size: 2.5em;
}
#removeAvatar {
position: absolute;
@ -149,8 +149,7 @@ button:hover::after {
font-size: 0.9em;
}
#session_info .avatar {
width: 6em;
height: 6em;
font-size: 2.5em;
display: block;
margin: auto;
}
@ -198,7 +197,6 @@ button:hover::after {
padding: 10px;
display: flex;
align-items: center;
font-size: 1.7em;
cursor: pointer;
}
#me>div {
@ -207,6 +205,7 @@ button:hover::after {
flex-grow: 1;
}
#me p {
font-size: 1.7em;
margin: 0;
font-weight: bold;
display: inline;
@ -214,6 +213,9 @@ button:hover::after {
#me>div:hover p {
color: var(--accent);
}
#me .avatar {
font-size: 1.2em;
}
#left_panel ul:last-of-type, #msg_log {
flex-grow: 1;
}
@ -235,8 +237,7 @@ button:hover::after {
flex-grow: 1;
}
#left_panel ul li .avatar {
width: 2em;
height: 2em;
font-size: .9em;
}
#left_panel ul li:hover, #left_panel ul li.current {
background-color: #333940;
@ -254,7 +255,8 @@ button:hover::after {
#left_panel ul li.is_verified p::after {
content: url("/static/imgs/icons/verified/ACCENT_COLOR");
}
#left_panel ul li .not_seen_marker {
#left_panel ul li.not_seen::after {
content: "";
width: 12px;
height: 12px;
background-color: var(--accent);
@ -295,7 +297,6 @@ button:hover::after {
flex-direction: row;
align-items: center;
padding: 20px 20px;
font-size: 1.5em;
}
#chat_header>div {
display: flex;
@ -307,6 +308,7 @@ button:hover::after {
color: var(--accent);
}
#chat_header>div>p { /*name*/
font-size: 1.5em;
font-weight: bold;
margin: 0;
}
@ -354,7 +356,6 @@ button:hover::after {
}
#file_transfer {
border-top: 2px solid var(--accent);
display: none;
position: relative;
}
#file_transfer.active {
@ -401,31 +402,46 @@ button:hover::after {
height: 100%;
background-color: var(--accent);
}
#message_box {
border-top: 2px solid var(--accent);
margin-bottom: 0;
}
#msg_log {
font-size: 1.1em;
overflow-y: scroll;
white-space: pre;
overflow-y: auto;
white-space: pre-wrap;
}
#msg_log li {
display: flex;
align-items: end;
gap: 10px;
margin-bottom: 10px;
padding-right: 10px;
}
#msg_log li>div {
flex-grow: 1;
}
#msg_log li .timestamp {
opacity: .5;
font-family: "Liberation Sans", Arial, sans-serif;
font-size: .8em;
}
#msg_log p {
font-size: 1.1em;
margin: 0;
}
#msg_log .avatar {
font-size: .8em;
}
#msg_log li .header {
display: flex;
align-items: center;
margin-top: 15px;
}
#msg_log li .header p {
color: var(--accent);
font-weight: bold;
margin-left: .5em;
}
#msg_log li .content {
margin-left: 2em;
margin-top: 5px;
margin-bottom: 10px;
margin-left: 3em;
}
#msg_log li .content p {
word-break: break-word;
}
#msg_log a {
color: #238cf5;
@ -452,6 +468,149 @@ button:hover::after {
width: 2em;
margin-left: 15px;
}
#message_box, #chat_header, #msg_log {
#message_box, #message_box.online #offline_warning, #chat_header, #msg_log, #file_transfer {
display: none;
}
#message_box.active {
display: block;
}
#message_box {
border-top: 2px solid red;
margin-bottom: 0;
}
#message_box>div:nth-child(2) {
display: flex;
}
#message_box.online {
border-top-color: var(--accent);
}
#offline_warning {
margin-left: 20px;
display: flex;
align-items: center;
gap: 25px;
}
#offline_warning::before {
content: url("/static/imgs/icons/warning/ff0000");
display: block;
width: 2em;
}
#offline_warning h3 {
color: red;
display: inline-block;
margin-bottom: .3em;
}
#offline_warning p {
margin-top: 0;
}
#msg_log li.pending_msgs_divider {
border-top: 1px solid grey;
padding-top: 10px;
margin-top: 30px;
margin-left: 100px;
margin-right: 100px;
}
#msg_log li.pending_msgs_divider h4 {
margin: auto;
opacity: .5;
}
.lds-spinner {
color: official;
position: relative;
width: 82px;
height: 82px;
}
.lds-spinner div {
transform-origin: 40px 40px;
animation: lds-spinner 1.2s linear infinite;
}
.lds-spinner div:after {
content: " ";
display: block;
position: absolute;
top: 3px;
left: 37px;
width: 6px;
height: 18px;
border-radius: 20%;
background: #fff;
}
.lds-spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -1.1s;
}
.lds-spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -1s;
}
.lds-spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.9s;
}
.lds-spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.8s;
}
.lds-spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.7s;
}
.lds-spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.6s;
}
.lds-spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.4s;
}
.lds-spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.3s;
}
.lds-spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.2s;
}
.lds-spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.1s;
}
.lds-spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
#pending_msgs_indicator {
display: none;
align-items: center;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
#pending_msgs_indicator.sending {
display: flex;
}
#disconnected {
display: none;
height: 100%;
align-items: center;
justify-content: center;
}
#disconnected.disconnected {
display: flex;
}
#disconnected img {
width: 70px;
height: 70px;
}

View File

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<title>AIRA</title>
<link rel="icon" type="image/svg" href="/static/imgs/icons/logo">
<link rel="stylesheet" href="/static/commons/style.css">
<link rel="stylesheet" href="/static/index.css">
</head>
@ -41,6 +42,10 @@
</div>
<ul id="msg_log">
</ul>
<div id="pending_msgs_indicator">
<div class="lds-spinner"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
<h3>Sending pending messages...</h3>
</div>
<div id="file_transfer">
<div id="file_control">
<button id="file_cancel" title="Cancel"></button>
@ -56,10 +61,22 @@
</div>
</div>
<div id="message_box">
<input type="text" id="message_input" placeholder="Send a message...">
<label title="Send file" class="file_picker">
<input type="file" id="attach_file" multiple>
</label>
<div id="offline_warning">
<div>
<h3>Your contact seems to be offline.</h3>
<p>Sent messages will be stored until a connection is established.</p>
</div>
</div>
<div>
<input type="text" id="message_input" placeholder="Send a message...">
<label title="Send file" class="file_picker">
<input type="file" id="attach_file" multiple>
</label>
</div>
</div>
<div id="disconnected">
<img src="/static/imgs/icons/warning/ff0000">
<h1>Websocket connection closed</h1>
</div>
</div>
</main>
@ -76,4 +93,3 @@
<script src="/static/index.js"></script>
</body>
</html>

View File

@ -1,12 +1,13 @@
"use strict";
let identityName = undefined;
let socket = null;
let socket = new WebSocket("ws://"+location.hostname+":"+websocketPort+"/ws");;
let notificationAllowed = false;
let localIps = [];
let currentSessionId = -1;
let sessionsData = new Map();
let msgHistory = new Map();
let pendingMsgs = new Map();
let pendingFilesTransfers = new Map();
let avatarTimestamps = new Map([
["self", Date.now()]
@ -14,7 +15,7 @@ let avatarTimestamps = new Map([
function onClickSession(event) {
let sessionId = event.currentTarget.getAttribute("data-sessionId");
if (sessionId != null) {
if (sessionId != null && socket.readyState === WebSocket.OPEN) {
currentSessionId = sessionId;
let session = sessionsData.get(sessionId);
if (!session.seen) {
@ -80,6 +81,7 @@ document.getElementById("delete_conversation").onclick = function() {
document.getElementById("add_contact").onclick = function() {
socket.send("contact "+currentSessionId);
sessionsData.get(currentSessionId).isContact = true;
pendingMsgs.set(currentSessionId, []);
displayHeader();
displaySessions();
};
@ -103,7 +105,9 @@ document.getElementById("remove_contact").onclick = function() {
if (!session.isOnline) {
sessionsData.delete(currentSessionId);
msgHistory.get(currentSessionId).length = 0;
displayChatBottom();
}
pendingMsgs.delete(currentSessionId);
displayHeader();
displaySessions();
displayHistory();
@ -174,7 +178,7 @@ document.getElementById("attach_file").onchange = function(event) {
let files = event.target.files;
let useLargeFileTransfer = false;
for (let i=0; i<files.length; ++i) {
if (files[i].size > 32760000) {
if (files[i].size > 16380000) {
useLargeFileTransfer = true;
break;
}
@ -215,7 +219,11 @@ document.getElementById("attach_file").onchange = function(event) {
formData.append("", files[i]);
fetch("/send_file", {method: "POST", body: formData}).then(response => {
if (response.ok) {
response.text().then(uuid => onFileSent(currentSessionId, uuid, files[i].name));
response.text().then(text => {
if (text === "pending") {
newPendingMsg(currentSessionId, true, files[i].name);
}
});
} else {
console.log(response);
}
@ -226,16 +234,19 @@ document.getElementById("attach_file").onchange = function(event) {
document.getElementById("file_cancel").onclick = function() {
socket.send("abort "+currentSessionId);
};
let msg_log = document.getElementById("msg_log");
msg_log.onscroll = function() {
if (sessionsData.get(currentSessionId).isContact) {
if (msg_log.scrollTop < 30) {
socket.send("load_msgs "+currentSessionId);
let msgLog = document.getElementById("msg_log");
msgLog.onscroll = function() {
let session = sessionsData.get(currentSessionId);
if (typeof session !== "undefined") {
if (session.isContact) {
if (msgLog.scrollTop < 30) {
socket.send("load_msgs "+currentSessionId);
}
}
}
};
let profile_div = document.querySelector("#me>div");
profile_div.onclick = function() {
let profileDiv = document.querySelector("#me>div");
profileDiv.onclick = function() {
let mainDiv = document.createElement("div");
mainDiv.id = "profile_info";
let avatarContainer = document.createElement("div");
@ -324,32 +335,35 @@ profile_div.onclick = function() {
changePasswordButton.textContent = "Change password";
changePasswordButton.onclick = function() {
let inputs = document.querySelectorAll("input[type=\"password\"]");
let newPassword, newPasswordConfirm;
let newPassword, newPasswordConfirm, oldPassword;
if (isIdentityProtected) {
newPassword = inputs[1];
newPasswordConfirm = inputs[2];
oldPassword = inputs[0].value;
newPassword = inputs[1].value;
newPasswordConfirm = inputs[2].value;
} else {
newPassword = inputs[0];
newPasswordConfirm = inputs[1];
newPassword = inputs[0].value;
newPasswordConfirm = inputs[1].value;
}
if (newPassword.value == newPasswordConfirm.value) {
let newPassword_set = newPassword.value.length > 0;
if (isIdentityProtected || newPassword_set) { //don't change password if identity is not protected and new password is blank
if (newPassword == newPasswordConfirm) {
let newPasswordSet = newPassword.length > 0;
if (isIdentityProtected && oldPassword.length == 0) {
errorMsg.textContent = "Current password cannot be empty.";
} else if (isIdentityProtected || newPasswordSet) { //don't change password if identity is not protected and new password is blank
let msg = "change_password";
if (isIdentityProtected) {
msg += " "+b64EncodeUnicode(inputs[0].value);
}
if (newPassword_set) {
msg += " "+b64EncodeUnicode(newPassword.value);
if (newPasswordSet) {
msg += " "+b64EncodeUnicode(newPassword);
}
socket.send(msg);
} else {
removePopup();
}
} else {
newPassword.value = "";
newPasswordConfirm.value = "";
errorMsg.textContent = "Passwords don't match";
newPassword = "";
newPasswordConfirm = "";
errorMsg.textContent = "Passwords don't match.";
}
};
sectionPassword.appendChild(changePasswordButton);
@ -436,8 +450,17 @@ function getCookie(cname) {
}
return "";
}
function parseTimestamp(timestamp) {
return new Date(Number(timestamp) * 1000);
}
function toTwoNumbers(n) {
if (n < 10) {
return '0'+n;
} else {
return n;
}
}
socket = new WebSocket("ws://"+location.hostname+":"+websocketPort+"/ws");
socket.onopen = function() {
console.log("Connected");
socket.send(getCookie("aira_auth")); //authenticating websocket connection
@ -468,10 +491,10 @@ socket.onmessage = function(msg) {
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));
break;
case "new_message":
onNewMessage(args[1], args[2] === "true", msg.data.slice(args[0].length+args[1].length+args[2].length+3));
onNewMessage(args[1], args[2] === "true", parseTimestamp(args[3]), msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+4));
break;
case "file":
onFileReceived(args[1], args[2], msg.data.slice(args[0].length+args[1].length+args[2].length+3));
onNewFile(args[1], args[2] === "true", parseTimestamp(args[3]), args[4], msg.data.slice(args[0].length+args[1].length+args[2].length+args[3].length+args[4].length+5));
break;
case "files_transfer":
onNewFilesTransfer(args[1], args[2], msg.data.slice(args[0].length+args[1].length+args[2].length+3));
@ -500,6 +523,15 @@ socket.onmessage = function(msg) {
case "not_seen":
setNotSeen(msg.data.slice(args[0].length+1));
break;
case "pending":
newPendingMsg(args[1], args[2] === "true", msg.data.slice(args[0].length+args[1].length+args[2].length+3));
break;
case "sending_pending_msgs":
onSendingPendingMsgs(args[1]);
break;
case "pending_msgs_sent":
onPendingMsgsSent(args[1]);
break;
case "local_ips":
setLocalIps(msg.data.slice(args[0].length+1));
break;
@ -519,6 +551,12 @@ socket.onmessage = function(msg) {
};
socket.onclose = function() {
console.log("Disconnected");
currentSessionId = -1;
displayHistory();
displayHeader();
displayChatBottom();
displaySessions();
document.getElementById("disconnected").classList.add("disconnected");
};
function onNewSession(sessionId, outgoing, fingerprint, ip, name) {
@ -564,6 +602,22 @@ function setNotSeen(strSessionIds) {
}
displaySessions();
}
function newPendingMsg(sessionId, isFile, data) {
pendingMsgs.get(sessionId).push([isFile, data]);
if (sessionId == currentSessionId) {
displayHistory();
}
}
function onSendingPendingMsgs(sessionId) {
document.getElementById("pending_msgs_indicator").classList.add("sending");
pendingMsgs.get(sessionId).length = 0;
if (sessionId == currentSessionId) {
displayHistory();
}
}
function onPendingMsgsSent(sessionId) {
document.getElementById("pending_msgs_indicator").classList.remove("sending");
}
function setLocalIps(strIPs) {
localIps = strIPs.split(' ');
}
@ -576,6 +630,7 @@ function onIsContact(sessionId, verified, fingerprint, name) {
} else {
addSession(sessionId, name, undefined, fingerprint, undefined, true, verified, false);
}
pendingMsgs.set(sessionId, []);
}
function onMsgOrFileReceived(sessionId, outgoing, body) {
if (currentSessionId == sessionId) {
@ -589,16 +644,22 @@ function onMsgOrFileReceived(sessionId, outgoing, body) {
}
if (document.hidden && !outgoing) {
if (notificationAllowed) {
new Notification(sessionsData.get(sessionId).name, {
"body": body
let sessionName = sessionsData.get(sessionId).name;
new Notification(sessionName, {
"body": body,
"icon": "/avatar/"+sessionId+"/"+sessionName+"?"+avatarTimestamps.get(sessionId)
});
}
}
}
function onNewMessage(sessionId, outgoing, msg) {
msgHistory.get(sessionId).push([outgoing, false, msg]);
function onNewMessage(sessionId, outgoing, timestamp, msg) {
msgHistory.get(sessionId).push([outgoing, timestamp, false, msg]);
onMsgOrFileReceived(sessionId, outgoing, msg);
}
function onNewFile(sessionId, outgoing, timestamp, uuid, filename) {
msgHistory.get(sessionId).push([outgoing, timestamp, true, [uuid, filename]]);
onMsgOrFileReceived(sessionId, outgoing, filename);
}
function onNewFilesTransfer(sessionId, index, filesInfo) {
let split = filesInfo.split(' ');
let files = [];
@ -683,7 +744,8 @@ function onAskLargeFiles(sessionId, encodedDownloadLocation, filesInfo) {
showPopup(mainDiv, false);
if (document.hidden && notificationAllowed) {
new Notification(sessionName, {
"body": "Files download request"
"body": "Files download request",
"icon": "/avatar/"+sessionId+"/"+sessionName+"?"+avatarTimestamps.get(sessionId)
});
}
}
@ -726,29 +788,32 @@ function onIncFilesTransfer(sessionId, chunkSize) {
}
function onMsgsLoad(sessionId, strMsgs) {
let msgs = strMsgs.split(' ');
let n = 0;
while (n < msgs.length) {
let outgoing = msgs[n+1] === "true";
switch (msgs[n]) {
case 'm':
let msg = b64DecodeUnicode(msgs[n+2]);
msgHistory.get(sessionId).unshift([outgoing, false, msg]);
n += 3;
break;
case 'f':
let uuid = msgs[n+2];
let fileName = b64DecodeUnicode(msgs[n+3]);
msgHistory.get(sessionId).unshift([outgoing, true, [uuid, fileName]]);
n += 4;
if (msgs.length > 3) {
let n = 0;
while (n < msgs.length) {
let outgoing = msgs[n+1] === "true";
let timestamp = parseTimestamp(msgs[n+2]);
switch (msgs[n]) {
case 'm':
let msg = b64DecodeUnicode(msgs[n+3]);
msgHistory.get(sessionId).unshift([outgoing, timestamp, false, msg]);
n += 4;
break;
case 'f':
let uuid = msgs[n+3];
let fileName = b64DecodeUnicode(msgs[n+4]);
msgHistory.get(sessionId).unshift([outgoing, timestamp, true, [uuid, fileName]]);
n += 5;
}
}
}
if (currentSessionId == sessionId) {
if (msg_log.scrollHeight - msg_log.scrollTop === msg_log.clientHeight) {
displayHistory();
} else {
let backupHeight = msg_log.scrollHeight;
displayHistory(false);
msg_log.scrollTop = msg_log.scrollHeight-backupHeight;
if (currentSessionId == sessionId) {
if (msgLog.scrollHeight - msgLog.scrollTop === msgLog.clientHeight) {
displayHistory();
} else {
let backupHeight = msgLog.scrollHeight;
displayHistory(false);
msgLog.scrollTop = msgLog.scrollHeight-backupHeight;
}
}
}
}
@ -762,6 +827,7 @@ function onDisconnected(sessionId) {
}
if (currentSessionId == sessionId) {
displayChatBottom();
scrollHistoryToBottom();
}
if (currentSessionId == sessionId && !session.isContact) {
currentSessionId = -1;
@ -769,16 +835,6 @@ function onDisconnected(sessionId) {
}
displaySessions();
}
function onFileReceived(sessionId, uuid, file_name) {
msgHistory.get(sessionId).push([false, true, [uuid, file_name]]);
onMsgOrFileReceived(sessionId, false, file_name);
}
function onFileSent(sessionId, uuid, file_name) {
msgHistory.get(sessionId).push([true, true, [uuid, file_name]]);
if (currentSessionId == sessionId) {
displayHistory();
}
}
function onNameSet(newName) {
removePopup();
identityName = newName;
@ -925,11 +981,11 @@ function logout() {
window.location = "/logout";
}
function displayProfile() {
profile_div.innerHTML = "";
profile_div.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
profileDiv.innerHTML = "";
profileDiv.appendChild(generateSelfAvatar(avatarTimestamps.get("self")));
let p = document.createElement("p");
p.textContent = identityName;
profile_div.appendChild(p);
profileDiv.appendChild(p);
}
function displayHeader() {
chatHeader.children[0].innerHTML = "";
@ -1002,9 +1058,7 @@ function generateSession(sessionId, session) {
li.classList.add("is_verified");
}
if (!session.seen) {
let marker = document.createElement("div");
marker.classList.add("not_seen_marker");
li.appendChild(marker);
li.classList.add("not_seen");
}
if (sessionId == currentSessionId) {
li.classList.add("current");
@ -1027,20 +1081,27 @@ function generateMsgHeader(name, sessionId) {
div.appendChild(p);
return div;
}
function generateMessageTimestamp(timestamp) {
let p = document.createElement("p");
p.classList.add("timestamp");
p.title = timestamp;
p.textContent = toTwoNumbers(timestamp.getHours())+":"+toTwoNumbers(timestamp.getMinutes());
return p;
}
function generateMessage(name, sessionId, msg) {
let p = document.createElement("p");
p.appendChild(document.createTextNode(msg));
let div = document.createElement("div");
div.classList.add("content");
div.appendChild(linkifyElement(p));
let li = document.createElement("li");
let divContainer = document.createElement("div");
if (typeof name !== "undefined") {
li.appendChild(generateMsgHeader(name, sessionId));
divContainer.appendChild(generateMsgHeader(name, sessionId));
}
li.appendChild(div);
return li;
divContainer.appendChild(div);
return divContainer;
}
function generateFile(name, sessionId, outgoing, file_info) {
function generateFile(name, sessionId, outgoing, fileInfo) {
let div1 = document.createElement("div");
div1.classList.add("file");
div1.classList.add("content");
@ -1052,20 +1113,24 @@ function generateFile(name, sessionId, outgoing, file_info) {
h4.textContent = "File received:";
}
div2.appendChild(h4);
let p = document.createElement("p");
p.textContent = file_info[1];
div2.appendChild(p);
div1.appendChild(div2);
let a = document.createElement("a");
a.href = "/load_file?uuid="+file_info[0]+"&file_name="+encodeURIComponent(file_info[1]);
a.target = "_blank";
div1.appendChild(a);
let li = document.createElement("li");
if (typeof name !== "undefined") {
li.appendChild(generateMsgHeader(name, sessionId));
let p = document.createElement("p");
if (typeof fileInfo === "string") { //pending
p.textContent = fileInfo;
} else {
p.textContent = fileInfo[1];
let a = document.createElement("a");
a.href = "/load_file?uuid="+fileInfo[0]+"&file_name="+encodeURIComponent(fileInfo[1]);
a.target = "_blank";
div1.appendChild(a);
}
li.appendChild(div1);
return li;
div2.appendChild(p);
let divContainer = document.createElement("div");
if (typeof name !== "undefined") {
divContainer.appendChild(generateMsgHeader(name, sessionId));
}
divContainer.appendChild(div1);
return divContainer;
}
function generateFileInfo(fileName, fileSize, p) {
let span = document.createElement("span");
@ -1078,13 +1143,16 @@ function displayChatBottom(speed = undefined) {
let fileTransfer = document.getElementById("file_transfer");
let session = sessionsData.get(currentSessionId);
if (typeof session === "undefined") {
msgBox.removeAttribute("style");
msgBox.classList.remove("active");
fileTransfer.classList.remove("active");
} else {
if (session.isContact || session.isOnline) {
msgBox.classList.add("active");
}
if (session.isOnline) {
msgBox.style.display = "flex";
msgBox.classList.add("online");
} else {
msgBox.removeAttribute("style");
msgBox.classList.remove("online");
}
if (pendingFilesTransfers.has(currentSessionId)) {
let fileInfo = document.getElementById("file_info");
@ -1129,35 +1197,72 @@ function displayChatBottom(speed = undefined) {
}
}
}
function scrollHistoryToBottom() {
msgLog.scrollTop = msgLog.scrollHeight;
}
function displayHistory(scrollToBottom = true) {
msg_log.style.display = "block";
msg_log.innerHTML = "";
msgLog.innerHTML = "";
let session = sessionsData.get(currentSessionId);
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;
if (typeof session === "undefined") {
msgLog.style.display = "none";
} else {
msgLog.style.display = "block";
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;
}
}
let div;
if (entry[2]) { //is file
div = generateFile(name, sessionId, entry[0], entry[3]);
} else {
name = session.name;
sessionId = currentSessionId;
div = generateMessage(name, sessionId, entry[3]);
}
let li = document.createElement("li");
li.appendChild(div);
li.appendChild(generateMessageTimestamp(entry[1]));
msgLog.appendChild(li);
});
if (session.isContact) {
let msgs = pendingMsgs.get(currentSessionId);
if (msgs.length > 0) {
let li = document.createElement("li");
li.classList.add("pending_msgs_divider");
let h4 = document.createElement("h4");
h4.textContent = "Pending messages:";
li.appendChild(h4);
msgLog.appendChild(li);
msgs.forEach(entry => {
let name = undefined;
if (previousOutgoing != true) {
previousOutgoing = true;
name = identityName;
}
let div;
if (entry[0]) { //is file
div = generateFile(name, undefined, true, entry[1]);
} else {
div = generateMessage(name, undefined, entry[1]);
}
let li = document.createElement("li");
li.appendChild(div);
msgLog.appendChild(li);
});
}
}
if (entry[1]) { //is file
msg_log.appendChild(generateFile(name, sessionId, entry[0], entry[2]));
} else {
msg_log.appendChild(generateMessage(name, sessionId, entry[2]));
if (scrollToBottom) {
scrollHistoryToBottom();
}
});
if (scrollToBottom) {
msg_log.scrollTop = msg_log.scrollHeight;
}
if (typeof session !== "undefined") {
if (msg_log.scrollHeight <= msg_log.clientHeight && session.isContact) {
if (msgLog.scrollHeight <= msgLog.clientHeight && session.isContact) {
socket.send("load_msgs "+currentSessionId);
}
}
}
}

View File

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<title>AIRA - Login</title>
<link rel="icon" type="image/svg" href="/static/imgs/icons/logo">
<link rel="stylesheet" href="/static/commons/style.css">
<style>
body {
@ -58,8 +59,7 @@
padding: 10px 50px;
}
.avatar {
width: 7em;
height: 7em;
font-size: 3em;
}
#identity h2 {
text-align: center;

View File

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8">
<title>AIRA - Logged out</title>
<link rel="icon" type="image/svg" href="/static/imgs/icons/logo">
<link rel="stylesheet" href="/static/commons/style.css">
<style>
body {

View File

@ -43,12 +43,11 @@ fn get_database_path() -> String {
AppDirs::new(Some(constants::APPLICATION_FOLDER), false).unwrap().data_dir.join(constants::DB_NAME).to_str().unwrap().to_owned()
}
struct EncryptedIdentity {
name: String,
encrypted_keypair: Vec<u8>,
salt: Vec<u8>,
encrypted_master_key: Vec<u8>,
encrypted_use_padding: Vec<u8>,
#[derive(Debug, Clone)]
pub struct Message {
pub outgoing: bool,
pub timestamp: u64,
pub data: Vec<u8>,
}
pub struct Contact {
@ -60,6 +59,14 @@ pub struct Contact {
pub seen: bool,
}
struct EncryptedIdentity {
name: String,
encrypted_keypair: Vec<u8>,
salt: Vec<u8>,
encrypted_master_key: Vec<u8>,
encrypted_use_padding: Vec<u8>,
}
pub struct Identity {
pub name: String,
pub keypair: Keypair,
@ -87,8 +94,8 @@ impl Identity {
};
Ok(Contact {
uuid: contact_uuid,
public_key: public_key,
name: name,
public_key,
name,
avatar: avatar_uuid,
verified: false,
seen: true,
@ -242,16 +249,17 @@ impl Identity {
Ok(file_uuid)
}
pub fn store_msg(&self, contact_uuid: &Uuid, outgoing: bool, data: &[u8]) -> Result<usize, rusqlite::Error> {
pub fn store_msg(&self, contact_uuid: &Uuid, message: &Message) -> Result<usize, rusqlite::Error> {
let db = Connection::open(get_database_path())?;
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, data BLOB)", contact_uuid), [])?;
let outgoing_byte: u8 = bool_to_byte(outgoing);
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, timestamp BLOB, data BLOB)", contact_uuid), [])?;
let outgoing_byte: u8 = bool_to_byte(message.outgoing);
let encrypted_outgoing = crypto::encrypt_data(&[outgoing_byte], &self.master_key).unwrap();
let encrypted_data = crypto::encrypt_data(data, &self.master_key).unwrap();
db.execute(&format!("INSERT INTO \"{}\" (outgoing, data) VALUES (?1, ?2)", contact_uuid), params![encrypted_outgoing, encrypted_data])
let encrypted_timestamp = crypto::encrypt_data(&message.timestamp.to_be_bytes(), &self.master_key).unwrap();
let encrypted_data = crypto::encrypt_data(&message.data, &self.master_key).unwrap();
db.execute(&format!("INSERT INTO \"{}\" (outgoing, timestamp, data) VALUES (?1, ?2, ?3)", contact_uuid), params![encrypted_outgoing, encrypted_timestamp, encrypted_data])