"use strict"; let identityName = undefined; 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()] ]); function onClickSession(event) { let sessionId = event.currentTarget.getAttribute("data-sessionId"); if (sessionId != null && socket.readyState === WebSocket.OPEN) { currentSessionId = sessionId; let session = sessionsData.get(sessionId); if (!session.seen) { session.seen = true; socket.send("set_seen "+sessionId); } displaySessions(); displayHeader(); displayChatBottom(); displayHistory(); } } 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 16380000) { 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 filesTransfer = []; let fileInfo = ""; for (let i=0; i { if (response.ok) { response.text().then(text => { if (text === "pending") { newPendingMsg(currentSessionId, true, files[i].name); } }); } else { console.log(response); } }); }; } }; document.getElementById("file_cancel").onclick = function() { socket.send("abort "+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 profileDiv = document.querySelector("#me>div"); profileDiv.onclick = function() { let mainDiv = document.createElement("div"); 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.accept = "image/*"; inputAvatar.id = "avatar_input"; inputAvatar.onchange = function(event) { uploadAvatar(event, function() { avatarTimestamps.set("self", Date.now()); refreshSelfAvatar(); }); }; labelAvatar.appendChild(inputAvatar); labelAvatar.appendChild(generateSelfAvatar(avatarTimestamps.get("self"))); let uploadP = document.createElement("p"); uploadP.textContent = "Upload"; labelAvatar.appendChild(uploadP); avatarContainer.appendChild(labelAvatar); let removeAvatar = document.createElement("span"); removeAvatar.id = "removeAvatar"; removeAvatar.textContent = "Remove"; removeAvatar.onclick = function() { socket.send("remove_avatar"); }; avatarContainer.appendChild(removeAvatar); mainDiv.appendChild(avatarContainer); let sectionName = document.createElement("section"); let titleName = document.createElement("h3"); titleName.textContent = "Name:"; sectionName.appendChild(titleName); let inputName = document.createElement("input"); inputName.id = "new_name"; inputName.type = "text"; inputName.value = identityName; sectionName.appendChild(inputName); let saveNameButton = document.createElement("button"); saveNameButton.classList.add("classic_button");; saveNameButton.textContent = "Save"; saveNameButton.onclick = function() { socket.send("change_name "+document.getElementById("new_name").value); }; sectionName.appendChild(saveNameButton); mainDiv.appendChild(sectionName); let sectionFingerprint = document.createElement("section"); let titleFingerprint = document.createElement("h3"); titleFingerprint.textContent = "Your fingerprint:"; sectionFingerprint.appendChild(titleFingerprint); let fingerprint = document.createElement("pre"); fingerprint.textContent = beautifyFingerprint(identityFingerprint); sectionFingerprint.appendChild(fingerprint); mainDiv.appendChild(sectionFingerprint); let sectionPadding = document.createElement("section"); sectionPadding.appendChild(generateSwitchPreference("Use PSEC padding", "PSEC padding obfuscates the length of your messages but uses more network bandwidth.", usePadding, function(checked) { socket.send("set_use_padding "+checked); usePadding = checked; })); mainDiv.appendChild(sectionPadding); let sectionPassword = document.createElement("section"); let titlePassword = document.createElement("h3"); titlePassword.textContent = "Change your password:"; sectionPassword.appendChild(titlePassword); 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.classList.add("classic_button"); changePasswordButton.textContent = "Change password"; changePasswordButton.onclick = function() { let inputs = document.querySelectorAll("input[type=\"password\"]"); let newPassword, newPasswordConfirm, oldPassword; if (isIdentityProtected) { oldPassword = inputs[0].value; newPassword = inputs[1].value; newPasswordConfirm = inputs[2].value; } else { newPassword = inputs[0].value; newPasswordConfirm = inputs[1].value; } 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 (newPasswordSet) { msg += " "+b64EncodeUnicode(newPassword); } socket.send(msg); } else { removePopup(); } } else { newPassword = ""; newPasswordConfirm = ""; errorMsg.textContent = "Passwords don't match."; } }; sectionPassword.appendChild(changePasswordButton); mainDiv.appendChild(sectionPassword); let sectionDelete = document.createElement("section"); let deleteTitle = document.createElement("h3"); deleteTitle.textContent = "Delete identity:"; sectionDelete.appendChild(deleteTitle); 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.classList.add("classic_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.classList.add("classic_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 = showSessionInfoPopup; 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 onMsgsLoad(sessionId, strMsgs) { let msgs = strMsgs.split(' '); 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 (msgLog.scrollHeight - msgLog.scrollTop === msgLog.clientHeight) { displayHistory(); } else { let backupHeight = msgLog.scrollHeight; displayHistory(false); msgLog.scrollTop = msgLog.scrollHeight-backupHeight; } } } } 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(); scrollHistoryToBottom(); } if (currentSessionId == sessionId && !session.isContact) { currentSessionId = -1; chatHeader.classList.add("offline"); } displaySessions(); } 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 refreshAvatar(selector, sessionId) { let avatar = document.querySelector(selector); if (avatar !== null) { if (typeof sessionId === "undefined") { avatar.src = "/avatar/self?"+avatarTimestamps.get("self"); } else { avatar.src = "/avatar/"+sessionId+"/"+sessionsData.get(sessionId).name+"?"+avatarTimestamps.get(sessionId); } } } function refreshSelfAvatar() { refreshAvatar("#avatarContainer .avatar"); displayProfile(); if (currentSessionId != -1) { displayHistory(false); } } function beautifyFingerprint(f) { for (let i=4; idiv").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 scrollHistoryToBottom() { msgLog.scrollTop = msgLog.scrollHeight; } function displayHistory(scrollToBottom = true) { msgLog.innerHTML = ""; let session = sessionsData.get(currentSessionId); 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 { 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 (scrollToBottom) { scrollHistoryToBottom(); } if (msgLog.scrollHeight <= msgLog.clientHeight && session.isContact) { socket.send("load_msgs "+currentSessionId); } } }