"use strict"; let identityName = undefined; let socket = null; let notificationAllowed = false; let localIps = []; let currentSessionId = -1; let sessionsData = new Map(); let msgHistory = new Map(); let pendingFilesTransfers = new Map(); function onClickSession(event) { let sessionId = event.currentTarget.getAttribute("data-sessionId"); if (sessionId != null) { currentSessionId = sessionId; let session = sessionsData.get(sessionId); if (!session.seen) { session.seen = true; socket.send("set_seen "+sessionId); } displaySessions(); displayHeader(); displayChatBottom(); dislayHistory(); } } let ip_input = document.getElementById("ip_input"); ip_input.addEventListener("keyup", function(event) { if (event.key === "Enter") { socket.send("connect "+ip_input.value); ip_input.value = ""; } }); document.getElementById("show_local_ips").onclick = function() { let mainDiv = document.createElement("div"); let h2Title = document.createElement("h2"); h2Title.textContent = "Your IP addresses:"; mainDiv.appendChild(h2Title); let ul = document.createElement("ul"); ul.classList.add("ips"); for (let i=0; i 32760000) { useLargeFileTransfer = true; break; } } if (useLargeFileTransfer) { if (pendingFilesTransfers.has(currentSessionId)) { let mainDiv = document.createElement("div"); mainDiv.appendChild(generatePopupWarningTitle()); let p = document.createElement("p"); p.textContent = "Another file transfer is already in progress."; mainDiv.appendChild(p); showPopup(mainDiv); } else { let fileTransfers = []; let fileInfo = ""; for (let i=0; i { if (response.ok) { response.text().then(uuid => onFileSent(currentSessionId, uuid, files[i].name)); } else { console.log(response); } }); }; } } 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 profile_div = document.querySelector("#me>div"); profile_div.onclick = function() { let mainDiv = document.createElement("div"); let avatar = generateAvatar(identityName); mainDiv.appendChild(avatar); let fingerprint = document.createElement("pre"); fingerprint.id = "identity_fingerprint"; fingerprint.textContent = beautifyFingerprint(identityFingerprint); mainDiv.appendChild(fingerprint); let sectionName = document.createElement("section"); sectionName.textContent = "Name:"; let inputName = document.createElement("input"); inputName.id = "new_name"; inputName.type = "text"; inputName.value = identityName; sectionName.appendChild(inputName); let saveNameButton = document.createElement("button"); saveNameButton.textContent = "Save"; saveNameButton.onclick = function() { socket.send("change_name "+document.getElementById("new_name").value); }; sectionName.appendChild(saveNameButton); mainDiv.appendChild(sectionName); let sectionPassword = document.createElement("section"); sectionPassword.textContent = "Change your password:"; sectionPassword.style.paddingTop = "1em"; sectionPassword.style.borderTop = "1px solid black"; if (isIdentityProtected) { let input_old_password = document.createElement("input"); input_old_password.type = "password"; input_old_password.placeholder = "Current password"; sectionPassword.appendChild(input_old_password); } let inputPassword1 = document.createElement("input"); let inputPassword2 = document.createElement("input"); inputPassword1.type = "password"; inputPassword1.placeholder = "New password (empty for no password)"; inputPassword2.type = "password"; inputPassword2.placeholder = "New password (confirmation)"; sectionPassword.appendChild(inputPassword1); sectionPassword.appendChild(inputPassword2); let errorMsg = document.createElement("p"); errorMsg.id = "password_errorMsg"; errorMsg.style.color = "red"; sectionPassword.appendChild(errorMsg); let changePasswordButton = document.createElement("button"); changePasswordButton.textContent = "Change password"; changePasswordButton.onclick = function() { let inputs = document.querySelectorAll("input[type=\"password\"]"); let newPassword, newPasswordConfirm; if (isIdentityProtected) { newPassword = inputs[1]; newPasswordConfirm = inputs[2]; } else { newPassword = inputs[0]; newPasswordConfirm = inputs[1]; } 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 let msg = "change_password"; if (isIdentityProtected) { msg += " "+b64EncodeUnicode(inputs[0].value); } if (newPassword_set) { msg += " "+b64EncodeUnicode(newPassword.value); } socket.send(msg); } else { removePopup(); } } else { newPassword.value = ""; newPasswordConfirm.value = ""; errorMsg.textContent = "Passwords don't match"; } }; sectionPassword.appendChild(changePasswordButton); mainDiv.appendChild(sectionPassword); let sectionDelete = document.createElement("section"); sectionDelete.textContent = "Delete identity:"; sectionDelete.style.paddingTop = "1em"; sectionDelete.style.borderTop = "1px solid red"; let p = document.createElement("p"); p.textContent = "Deleting your identity will delete all your conversations (messages and files), all your contacts, and your private key. You won't be able to be recognized by your contacts anymore."; p.style.color = "red"; sectionDelete.appendChild(p); let deleteButton = document.createElement("button"); deleteButton.textContent = "Delete"; deleteButton.style.backgroundColor = "red"; deleteButton.onclick = function() { let mainDiv = document.createElement("div"); mainDiv.appendChild(generatePopupWarningTitle()); let p = document.createElement("p"); p.textContent = "This action is irreversible. Are you sure you want to delete all your data ?"; mainDiv.appendChild(p); let deleteButton = document.createElement("button"); deleteButton.style.backgroundColor = "red"; deleteButton.textContent = "Delete"; deleteButton.onclick = function() { socket.send("disappear"); } mainDiv.appendChild(deleteButton); showPopup(mainDiv); } sectionDelete.appendChild(deleteButton); mainDiv.appendChild(sectionDelete); showPopup(mainDiv); } let chatHeader = document.getElementById("chat_header"); chatHeader.children[0].onclick = function() { let session = sessionsData.get(currentSessionId); if (typeof session !== "undefined") { let mainDiv = document.createElement("div"); mainDiv.classList.add("session_info"); mainDiv.appendChild(generateAvatar(session.name)); let h2 = document.createElement("h2"); h2.textContent = session.name; mainDiv.appendChild(h2); let pFingerprint = document.createElement("p"); pFingerprint.textContent = "Fingerprint:"; mainDiv.appendChild(pFingerprint); let pre = document.createElement("pre"); pre.textContent = ' '+beautifyFingerprint(session.fingerprint); mainDiv.appendChild(pre); if (session.isOnline) { let pIp = document.createElement("p"); pIp.textContent = "IP: "+session.ip; mainDiv.appendChild(pIp); let pConnection = document.createElement("p"); pConnection.textContent = "Connection: "; if (session.outgoing) { pConnection.textContent += "outgoing"; } else { pConnection.textContent += "incomming"; } mainDiv.appendChild(pConnection); } showPopup(mainDiv); } } document.querySelector("#refresher button").onclick = function() { socket.send("refresh"); } //source: https://stackoverflow.com/a/14919494 function humanFileSize(bytes, dp=1) { const thresh = 1000; if (Math.abs(bytes) < thresh) { return bytes + " B"; } const units = ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; let u = -1; const r = 10**dp; do { bytes /= thresh; ++u; } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); return bytes.toFixed(dp) + ' ' + units[u]; } //source: https://stackoverflow.com/a/30106551 function b64EncodeUnicode(str) { return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function toSolidBytes(match, p1) { return String.fromCharCode('0x' + p1); })); } function b64DecodeUnicode(str) { return decodeURIComponent(atob(str).split('').map(function(c) { return '%' + ("00" + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); } //source: https://www.w3schools.com/js/js_cookies.asp function getCookie(cname) { var name = cname + "="; var decodedCookie = decodeURIComponent(document.cookie); var ca = decodedCookie.split(';'); for(var i = 0; i = fileTransfer.size) { if (filesTransfer.index == filesTransfer.files.length-1) { filesTransfer.state = "completed"; socket.send("sending_ended "+sessionId); } else { filesTransfer.index += 1; if (typeof fileTransfer.file !== "undefined") { sendNextLargeFile(sessionId); } } } if (currentSessionId == sessionId) { displayChatBottom(speed); } } } function onMsgLoad(sessionId, outgoing, msg) { msgHistory.get(sessionId).unshift([outgoing, false, msg]); if (currentSessionId == sessionId) { dislayHistory(false); } } function onFileLoad(sessionId, outgoing, uuid, fileName) { msgHistory.get(sessionId).unshift([outgoing, true, [uuid, fileName]]); if (currentSessionId == sessionId) { dislayHistory(false); } } function onDisconnected(sessionId) { pendingFilesTransfers.delete(sessionId); let session = sessionsData.get(sessionId); if (session.isContact) { session.isOnline = false; } else { sessionsData.delete(sessionId); } if (currentSessionId == sessionId) { displayChatBottom(); } if (currentSessionId == sessionId && !session.isContact) { currentSessionId = -1; chatHeader.classList.add("offline"); } 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) { dislayHistory(); } } function onNameSet(newName) { removePopup(); identityName = newName; displayProfile(); } function onPasswordChanged(success, isProtected) { if (success) { removePopup(); isIdentityProtected = isProtected; } else { let input = document.querySelector("input[type=\"password\"]"); input.value = ""; let errorMsg = document.getElementById("password_errorMsg"); errorMsg.textContent = "Operation failed. Please check your old password."; } } function sendNextLargeFile(sessionId) { let filesTransfer = pendingFilesTransfers.get(sessionId); filesTransfer.state = "transferring"; let fileTransfer = filesTransfer.files[filesTransfer.index]; fileTransfer.lastChunk = Date.now(); if (currentSessionId == sessionId) { displayChatBottom(); } let formData = new FormData(); formData.append("session_id", currentSessionId); formData.append("", fileTransfer.file); fetch("/send_large_file", {method: "POST", body: formData}).then(response => { if (!response.ok) { console.log(response); } }); } function beautifyFingerprint(f) { for (let i=4; i 0) { popups[popups.length-1].remove(); } } function generatePopupWarningTitle() { let h2 = document.createElement("h2"); h2.classList.add("warning"); h2.textContent = "Warning!"; return h2; } function generateName(name) { let p = document.createElement("p"); if (typeof name == "undefined") { p.appendChild(document.createTextNode("Unknown")); } else { p.appendChild(document.createTextNode(name)); } return p; } function generateSession(sessionId, session) { let li = document.createElement("li"); li.setAttribute("data-sessionId", sessionId); li.appendChild(generateAvatar(session.name)); li.appendChild(generateName(session.name)); if (session.isContact) { li.classList.add("is_contact"); } if (session.isVerified) { li.classList.add("is_verified"); } if (!session.seen) { let marker = document.createElement("div"); marker.classList.add("not_seen_marker"); li.appendChild(marker); } if (sessionId == currentSessionId) { li.classList.add("current"); } li.onclick = onClickSession; return li; } function generateMsgHeader(name) { let p = document.createElement("p"); p.appendChild(document.createTextNode(name)); let div = document.createElement("div"); div.classList.add("header"); div.appendChild(generateAvatar(name)); div.appendChild(p); return div; } function generateMessage(name, 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"); if (typeof name !== "undefined") { li.appendChild(generateMsgHeader(name)); } li.appendChild(div); return li; } function generateFile(name, outgoing, file_info) { let div1 = document.createElement("div"); div1.classList.add("file"); div1.classList.add("content"); let div2 = document.createElement("div"); let h4 = document.createElement("h4"); if (outgoing) { h4.textContent = "File sent:"; } else { 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)); } li.appendChild(div1); return li; } function generateFileInfo(fileName, fileSize, p) { let span = document.createElement("span"); span.textContent = fileName; p.appendChild(span); p.appendChild(document.createTextNode(" ("+humanFileSize(fileSize)+")")); } function displayChatBottom(speed = undefined) { let msgBox = document.getElementById("message_box"); let fileTransfer = document.getElementById("file_transfer"); let session = sessionsData.get(currentSessionId); if (typeof session === "undefined") { msgBox.removeAttribute("style"); fileTransfer.classList.remove("active"); } else { if (session.isOnline) { msgBox.style.display = "flex"; } else { msgBox.removeAttribute("style"); } if (pendingFilesTransfers.has(currentSessionId)) { let fileInfo = document.getElementById("file_info"); fileInfo.innerHTML = ""; let filesTransfer = pendingFilesTransfers.get(currentSessionId); let file = filesTransfer.files[filesTransfer.index]; generateFileInfo(file.name, file.size, fileInfo); let fileProgress = document.getElementById("file_progress"); fileProgress.style.display = "none"; //hide by default let fileStatus = document.getElementById("file_status"); fileStatus.removeAttribute("style"); //show by default let fileCancel = document.getElementById("file_cancel"); fileCancel.style.display = "none"; //hide by default document.querySelector("#file_progress_bar>div").style.width = 0; switch (filesTransfer.state) { case "transferring": fileCancel.removeAttribute("style"); //show fileStatus.style.display = "none"; fileProgress.removeAttribute("style"); //show let percent = (file.transferred/file.size)*100; document.getElementById("file_percent").textContent = percent.toFixed(2)+"%"; if (typeof speed !== "undefined") { document.getElementById("file_speed").textContent = humanFileSize(speed)+"/s"; } document.querySelector("#file_progress_bar>div").style.width = Math.round(percent)+"%"; break; case "waiting": fileStatus.textContent = "Waiting for peer confirmation..."; break; case "aborted": fileStatus.textContent = "Transfer aborted."; pendingFilesTransfers.delete(currentSessionId); break; case "completed": fileStatus.textContent = "Transfer completed."; pendingFilesTransfers.delete(currentSessionId); } fileTransfer.classList.add("active"); } else { fileTransfer.classList.remove("active"); } } } function dislayHistory(scrollToBottom = true) { msg_log.style.display = "block"; msg_log.innerHTML = ""; let previousOutgoing = undefined; msgHistory.get(currentSessionId).forEach(entry => { let name = undefined; if (previousOutgoing != entry[0]) { previousOutgoing = entry[0]; if (entry[0]) { //outgoing msg name = identityName; } else { name = sessionsData.get(currentSessionId).name; } } if (entry[1]) { //is file msg_log.appendChild(generateFile(name, entry[0], entry[2])); } else { msg_log.appendChild(generateMessage(name, entry[2])); } }); if (scrollToBottom) { msg_log.scrollTop = msg_log.scrollHeight; } }