// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0 if (localStorage.getItem("DONOTSHARE-secretkey") === null) { window.location.replace("/login") document.body.innerHTML = "Redirecting..." throw new Error(); } if (localStorage.getItem("DONOTSHARE-password") === null) { window.location.replace("/login") document.body.innerHTML = "Redirecting..." throw new Error(); } let remote = localStorage.getItem("homeserverURL") if (remote == null) { localStorage.setItem("homeserverURL", "https://notes.hectabit.org") remote = "https://notes.hectabit.org" } function formatBytes(a, b = 2) { if (!+a) return "0 Bytes"; const c = 0 > b ? 0 : b, d = Math.floor(Math.log(a) / Math.log(1000)); return `${parseFloat((a / Math.pow(1000, d)).toFixed(c))} ${["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"][d]}` } let secretkey = localStorage.getItem("DONOTSHARE-secretkey") let password = localStorage.getItem("DONOTSHARE-password") let currentFontSize = 16 let markdowntoggle = false let backButton = document.getElementById("backButton") let usernameBox = document.getElementById("usernameBox") let optionsCoverDiv = document.getElementById("optionsCoverDiv") let optionsDiv = document.getElementById("optionsDiv") let errorDiv = document.getElementById("errorDiv") let errorMessageThing = document.getElementById("errorMessageThing") let closeErrorButton = document.getElementById("closeErrorButton") let cancelErrorButton = document.getElementById("cancelErrorButton") let errorInput = document.getElementById("errorInput") let exitThing = document.getElementById("exitThing") let exitSessionsThing = document.getElementById("exitSessionsThing") let sessionManagerButton = document.getElementById("sessionManagerButton") let importNotesButton = document.getElementById("importNotesButton") let sessionManagerDiv = document.getElementById("sessionManagerDiv") let importNotesDiv = document.getElementById("importDiv") let sessionDiv = document.getElementById("sessionDiv") let deleteMyAccountButton = document.getElementById("deleteMyAccountButton") let storageThing = document.getElementById("storageThing") let storageProgressThing = document.getElementById("storageProgressThing") let usernameThing = document.getElementById("usernameThing") let logOutButton = document.getElementById("logOutButton") let notesBar = document.getElementById("notesBar") let topBar = document.getElementById("topBar") let notesDiv = document.getElementById("notesDiv") let newNote = document.getElementById("newNote") let noteBox = document.getElementById("noteBox") let noteBoxDiv = document.getElementById("noteBoxDiv") let loadingStuff = document.getElementById("loadingStuff") let exportNotesButton = document.getElementById("exportNotesButton") let markdown = document.getElementById('markdown'); let textSizeBox = document.getElementById('textSizeBox'); let textPlusBox = document.getElementById('textPlusBox'); let textMinusBox = document.getElementById('textMinusBox'); let wordCountBox = document.getElementById('wordCountBox'); let removeBox = document.getElementById("removeBox") let importFile = document.getElementById("importFile") let selectedNote = 0 let timer let waitTime = 400 let indiv = false let mobile = false let selectLatestNote = false if (/Android|iPhone|iPod/i.test(navigator.userAgent)) { mobile = true noteBoxDiv.classList.add("mobile"); noteBoxDiv.style.width = "0px"; notesBar.style.width = "100%" topBar.style.width = "100%" noteBoxDiv.readOnly = true noteBoxDiv.classList.add("hidden") let touchstartX, touchstartY, touchendX, touchendY notesBar.addEventListener("touchstart", function (event) { touchstartX = event.changedTouches[0].screenX; touchstartY = event.changedTouches[0].screenY; }, false); notesBar.addEventListener("touchend", function (event) { touchendX = event.changedTouches[0].screenX; touchendY = event.changedTouches[0].screenY; if (touchendX < touchstartX - 75) { handleGesture(); } }, false); noteBox.addEventListener("touchstart", function (event) { touchstartX = event.changedTouches[0].screenX; touchstartY = event.changedTouches[0].screenY; }, false); noteBox.addEventListener("touchend", function (event) { touchendX = event.changedTouches[0].screenX; touchendY = event.changedTouches[0].screenY; if (touchendX > touchstartX + 75) { handleGesture(); } else if (touchendX < touchstartX - 75) { enableMarkdown(); } }, false); markdown.addEventListener("touchstart", function (event) { touchstartX = event.changedTouches[0].screenX; touchstartY = event.changedTouches[0].screenY; }, false); noteBox.addEventListener("touchend", function (event) { touchendX = event.changedTouches[0].screenX; touchendY = event.changedTouches[0].screenY; if (touchendX > touchstartX + 75) { handleGesture(); } else if (touchendX < touchstartX - 75) { enableMarkdown(); } }, false); markdown.addEventListener("touchstart", function (event) { touchstartX = event.changedTouches[0].screenX; touchstartY = event.changedTouches[0].screenY; }, false); markdown.addEventListener("touchend", function (event) { touchendX = event.changedTouches[0].screenX; touchendY = event.changedTouches[0].screenY; if (touchendX > touchstartX + 75) { disableMarkdown(); } else if (touchendX < touchstartX - 75) { disableMarkdown(); } }, false); } function handleGesture() { if (indiv) { indiv = false notesBar.style.width = "100%"; noteBoxDiv.style.width = "0px" if (selectedNote !== 0) { noteBoxDiv.readOnly = true } notesDiv.classList.remove("hidden") noteBoxDiv.classList.add("hidden") backButton.classList.add("hidden") newNote.classList.remove("hidden") } else { indiv = true noteBoxDiv.style.width = "100%"; notesBar.style.width = "0px" if (selectedNote !== 0) { noteBoxDiv.readOnly = false } notesDiv.classList.add("hidden") noteBoxDiv.classList.remove("hidden") backButton.classList.remove("hidden") newNote.classList.add("hidden") } } noteBox.value = "" noteBox.readOnly = true let noteCount = 0 function displayError(message) { errorDiv.classList.remove("hidden") optionsCoverDiv.classList.remove("hidden") errorMessageThing.innerHTML = message } closeErrorButton.addEventListener("click", () => { errorDiv.classList.add("hidden") optionsCoverDiv.classList.add("hidden") }); closeErrorButton.addEventListener("click", () => { errorDiv.classList.add("hidden") optionsCoverDiv.classList.add("hidden") errorInput.classList.add("hidden") cancelErrorButton.classList.add("hidden") }); function updateFont() { currentFontSize = localStorage.getItem("SETTING-fontsize") noteBox.style.fontSize = currentFontSize + "px" textSizeBox.innerText = currentFontSize + "px" if (markdowntoggle) { markdown.srcdoc = "" + marked.parse(DOMPurify.sanitize(noteBox.value)) + ""; } } async function checknetwork() { fetch(remote + "/api/loggedin", { method: "POST", body: JSON.stringify({ secretKey: localStorage.getItem("DONOTSHARE-secretkey"), }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .catch(() => { noteBox.readOnly = true noteBox.value = "" noteBox.placeholder = "You are currently offline." displayError("Failed to connect to the server.\nPlease check your internet connection.") }) .then((response) => response) .then((response) => { if (response.status == 400) { displayError("Something went wrong! Signing you out...") closeErrorButton.classList.add("hidden") //usernameBox.innerText = "" setTimeout(function () { window.location.replace("/logout") }, 2500); } else if (response.status == 200) { updateUserInfo() } else { noteBox.readOnly = true noteBox.value = "" noteBox.placeholder = "You are currently offline." displayError("Failed to connect to the server.\nPlease check your internet connection.") } }); } if (localStorage.getItem("SETTING-fontsize") === null) { localStorage.setItem("SETTING-fontsize", "16") updateFont() } else { updateFont() } textPlusBox.addEventListener("click", () => { localStorage.setItem("SETTING-fontsize", String(Number(localStorage.getItem("SETTING-fontsize")) + Number(1))) updateFont() }); textMinusBox.addEventListener("click", () => { localStorage.setItem("SETTING-fontsize", String(Number(localStorage.getItem("SETTING-fontsize")) - Number(1))) updateFont() }); function truncateString(str, num) { if (str.length > num) { return str.slice(0, num) + ".."; } else { return str; } } function updateUserInfo() { fetch(remote + "/api/userinfo", { method: "POST", body: JSON.stringify({ secretKey: secretkey }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .then((response) => { async function doStuff() { if (response.status === 500) { displayError("Something went wrong! Signing you out...") closeErrorButton.classList.add("hidden") setTimeout(function () { window.location.replace("/logout") }, 2500); } else { let responseData = await response.json() usernameThing.innerText = "Username: " + responseData["username"] storageThing.innerText = "You've used " + formatBytes(responseData["storageused"]) + " out of " + formatBytes(responseData["storagemax"]) storageProgressThing.value = responseData["storageused"] storageProgressThing.max = responseData["storagemax"] noteCount = responseData["notecount"] } } doStuff() }); } usernameBox.addEventListener("click", () => { optionsCoverDiv.classList.remove("hidden") optionsDiv.classList.remove("hidden") updateUserInfo() }); logOutButton.addEventListener("click", () => { window.location.replace("/logout") }); exitThing.addEventListener("click", () => { optionsDiv.classList.add("hidden") optionsCoverDiv.classList.add("hidden") }); deleteMyAccountButton.addEventListener("click", () => { if (confirm("Are you REALLY sure that you want to delete your account? There's no going back!") === true) { fetch(remote + "/api/deleteaccount", { method: "POST", body: JSON.stringify({ secretKey: secretkey }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .then((response) => { if (response.status === 200) { window.location.href = "/logout" } else { displayError("Failed to delete account (HTTP error code " + response.status + ")") } }) } }); importNotesButton.addEventListener("click", () => { optionsDiv.classList.add("hidden") importNotesDiv.classList.remove("hidden") }); sessionManagerButton.addEventListener("click", () => { optionsDiv.classList.add("hidden") sessionManagerDiv.classList.remove("hidden") fetch(remote + "/api/sessions/list", { method: "POST", body: JSON.stringify({ secretKey: secretkey }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .then((response) => { async function doStuff() { let responseData = await response.json() document.querySelectorAll(".burgerSession").forEach((el) => el.remove()); let ua; for (let i in responseData) { let sessionElement = document.createElement("div") let sessionText = document.createElement("p") let sessionImage = document.createElement("img") let sessionRemoveButton = document.createElement("button") sessionText.classList.add("w300") if (responseData[i]["thisSession"] === true) { sessionText.innerText = "(current) " + responseData[i]["device"] } else { sessionText.innerText = responseData[i]["device"] } sessionText.title = responseData[i]["device"] sessionRemoveButton.innerText = "x" sessionImage.src = "/static/svg/device_other.svg" ua = responseData[i]["device"] if (ua.includes("NT") || ua.includes("Linux")) { sessionImage.src = "/static/svg/device_computer.svg" } if (ua.includes("iPhone" || ua.includes("Android") || ua.includes("iPod"))) { sessionImage.src = "/static/svg/device_smartphone.svg" } sessionRemoveButton.addEventListener("click", () => { fetch(remote + "/api/sessions/remove", { method: "POST", body: JSON.stringify({ secretKey: secretkey, sessionId: responseData[i]["id"] }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .then(() => { if (responseData[i]["thisSession"] === true) { window.location.replace("/logout") } }); sessionElement.remove() }); sessionElement.append(sessionImage) sessionElement.append(sessionText) sessionElement.append(sessionRemoveButton) sessionElement.classList.add("burgerSession") sessionDiv.append(sessionElement) } } doStuff() }); }); exitImportThing.addEventListener("click", () => { optionsDiv.classList.remove("hidden") importNotesDiv.classList.add("hidden") }); exitSessionsThing.addEventListener("click", () => { optionsDiv.classList.remove("hidden") sessionManagerDiv.classList.add("hidden") }); function updateWordCount() { let wordCount = noteBox.value.split(" ").length if (wordCount === 1) { wordCount = 0 } wordCountBox.innerText = wordCount + " words" } function renderMarkDown() { if (markdowntoggle) { markdown.srcdoc = "" + marked.parse(DOMPurify.sanitize(noteBox.value)) + "" } } function selectNote(nameithink) { document.querySelectorAll(".noteButton").forEach((el) => el.classList.remove("selected")); let thingArray = Array.from(document.querySelectorAll(".noteButton")).find(el => String(nameithink) === String(el.id)); thingArray.classList.add("selected") fetch(remote + "/api/readnote", { method: "POST", body: JSON.stringify({ secretKey: secretkey, noteId: nameithink, }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .catch(() => { noteBox.readOnly = true noteBox.value = "" noteBox.placeholder = "" displayError("Something went wrong... Please try again later!") }) .then((response) => { selectedNote = nameithink if (mobile) { handleGesture() } noteBox.readOnly = false noteBox.placeholder = "Type something!" async function doStuff() { let responseData = await response.json() let bytes = CryptoJS.AES.decrypt(responseData["content"], password); noteBox.value = bytes.toString(CryptoJS.enc.Utf8) updateWordCount() renderMarkDown() noteBox.addEventListener("input", () => { updateWordCount() renderMarkDown() clearTimeout(timer); timer = setTimeout(() => { let preEncryptedTitle = noteBox.value if (noteBox.value.substring(0, noteBox.value.indexOf("\n")) !== "") { preEncryptedTitle = noteBox.value.substring(0, noteBox.value.indexOf("\n")); } preEncryptedTitle = truncateString(preEncryptedTitle, 15) document.getElementById(nameithink).innerText = preEncryptedTitle let encryptedText = CryptoJS.AES.encrypt(noteBox.value, password).toString(); let encryptedTitle = CryptoJS.AES.encrypt(preEncryptedTitle, password).toString(); console.log(encryptedTitle) console.log(encryptedText) if (selectedNote === nameithink) { fetch(remote + "/api/editnote", { method: "POST", body: JSON.stringify({ secretKey: secretkey, noteId: nameithink, content: encryptedText, title: encryptedTitle }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .then((response) => { if (response.status === 418) { displayError("You've ran out of storage... Changes will not be saved until you free up storage!") } }) .catch(() => { displayError("Failed to save changes, please try again later...") }) } }, waitTime); }); } doStuff() }); } function updateNotes() { console.log("notes updated") fetch(remote + "/api/listnotes", { method: "POST", body: JSON.stringify({ secretKey: secretkey }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .then((response) => { async function doStuff() { noteBox.readOnly = true selectedNote = 0 if (selectLatestNote == false) { noteBox.placeholder = "" } noteBox.value = "" clearTimeout(timer) updateWordCount() renderMarkDown() let responseData = await response.json() let decryptedResponseData = [] let highestID = 0 // First decrypt note data, then render for (let i in responseData) { noteData = responseData[i] let bytes = CryptoJS.AES.decrypt(noteData["title"], password); let decryptedTitle = bytes.toString(CryptoJS.enc.Utf8); noteData["title"] = decryptedTitle if (noteData["id"] > highestID) { highestID = noteData["id"] } decryptedResponseData.push(noteData) console.log(noteData) } document.querySelectorAll(".noteButton").forEach((el) => el.remove()); for (let i in decryptedResponseData) { let noteData = decryptedResponseData[i] let noteButton = document.createElement("button"); noteButton.classList.add("noteButton") notesDiv.append(noteButton) console.log(noteData["title"]) if (noteData["title"] == "") { console.log(noteData["title"]) console.log("case") noteData["title"] = "New note" } noteButton.id = noteData["id"] noteButton.innerText = truncateString(noteData["title"], 15) noteButton.addEventListener("click", (event) => { if (event.ctrlKey) { fetch(remote + "/api/removenote", { method: "POST", body: JSON.stringify({ secretKey: secretkey, noteId: noteData["id"] }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .then(() => { updateNotes() }) .catch(() => { displayError("Something went wrong! Please try again later...") }) } else { selectNote(noteData["id"]) } }); } document.querySelectorAll(".loadingStuff").forEach((el) => el.remove()); if (selectLatestNote == true) { selectNote(highestID) selectLatestNote = false } } doStuff() }); } updateNotes() newNote.addEventListener("click", () => { let noteName = "New note" selectLatestNote = true console.log(selectLatestNote) // create fake item document.querySelectorAll(".noteButton").forEach((el) => el.classList.remove("selected")); let noteButton = document.createElement("button"); noteButton.classList.add("noteButton") notesDiv.append(noteButton) noteButton.innerText = "New note" noteButton.style.order = -1 noteButton.classList.add("selected") noteBox.placeholder = "Type something!" let encryptedName = CryptoJS.AES.encrypt(noteName, password).toString(CryptoJS.enc.Utf8); fetch(remote + "/api/newnote", { method: "POST", body: JSON.stringify({ secretKey: secretkey, noteName: encryptedName, }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .catch(() => { displayError("Failed to create new note, please try again later...") }) .then((response) => { if (response.status !== 200) { updateNotes() displayError("Failed to create new note (HTTP error code " + response.status + ")") } else { updateNotes() } }); }); function downloadObjectAsJson(exportObj, exportName) { let dataStr = "data:text/json;charset=utf-8," + DOMPurify.sanitize(JSON.stringify(exportObj)); let downloadAnchorNode = document.createElement("a"); downloadAnchorNode.setAttribute("href", dataStr); downloadAnchorNode.setAttribute("download", exportName + ".json"); document.body.appendChild(downloadAnchorNode); downloadAnchorNode.click(); downloadAnchorNode.remove(); } function exportNotes() { fetch(remote + "/api/exportnotes", { method: "POST", body: JSON.stringify({ secretKey: secretkey }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .then((response) => { async function doStuff() { let responseData = await response.json() for (let i in responseData) { exportNotes.innerText = "Decrypting " + i + "/" + noteCount let bytes = CryptoJS.AES.decrypt(responseData[i]["title"], password); responseData[i]["title"] = bytes.toString(CryptoJS.enc.Utf8) let bytesd = CryptoJS.AES.decrypt(responseData[i]["content"], password); responseData[i]["content"] = bytesd.toString(CryptoJS.enc.Utf8) } let jsonString = JSON.parse(JSON.stringify(responseData)) downloadObjectAsJson(jsonString, "data") optionsDiv.classList.add("hidden") displayError("Exported notes!") } doStuff() }) } function importNotes(plaintextNotes) { for (let i in plaintextNotes) { let originalTitle = plaintextNotes[i]["title"]; let encryptedTitle = CryptoJS.AES.encrypt(originalTitle, password).toString(); plaintextNotes[i]["title"] = encryptedTitle; let originalContent = plaintextNotes[i]["content"]; let encryptedContent = CryptoJS.AES.encrypt(originalContent, password).toString(); plaintextNotes[i]["content"] = encryptedContent; } fetch(remote + "/api/importnotes", { method: "POST", body: JSON.stringify({ "secretKey": localStorage.getItem("DONOTSHARE-secretkey"), "notes": JSON.stringify(plaintextNotes) }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .then((response) => { async function doStuff() { if (response.status === 500) { optionsDiv.classList.add("hidden") importNotesDiv.classList.add("hidden") displayError("Something went wrong! Perhaps your note file was invalid?") } else { optionsDiv.classList.add("hidden") importNotesDiv.classList.add("hidden") displayError("Notes uploaded!") updateNotes() } } doStuff() }) } function firstNewVersion() { if (localStorage.getItem("NEWVERSION") === "1.2") { return false; } else { localStorage.setItem("NEWVERSION", "1.2") return true; } } function toggleMarkdown() { if (markdown.style.display === 'none') { enableMarkdown() } else { disableMarkdown() } } function enableMarkdown() { markdown.style.display = 'inherit'; markdowntoggle = true renderMarkDown() } function disableMarkdown() { markdown.style.display = 'none'; markdowntoggle = false markdown.srcdoc = "" } exportNotesButton.addEventListener("click", () => { exportNotes() }); importFile.addEventListener('change', function(e) { let fileread = new FileReader() fileread.addEventListener( "load", () => { let decrypted = JSON.parse(fileread.result) importNotes(decrypted) }, false, ); fileread.readAsText(importFile.files[0]) }) removeBox.addEventListener("click", () => { if (selectedNote === 0) { displayError("You need to select a note first!") } else { selectLatestNote = true fetch(remote + "/api/removenote", { method: "POST", body: JSON.stringify({ secretKey: secretkey, noteId: selectedNote }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .then(() => { updateNotes() }) .catch(() => { displayError("Something went wrong! Please try again later...") }) } }); document.addEventListener("DOMContentLoaded", function() { markdown.srcdoc = "" + marked.parse(DOMPurify.sanitize(noteBox.value)) + "" }); if (firstNewVersion()) { displayError("What's new in Burgernotes 2.0?\nRestyled client\nAdded changing passwords\nMigrated to OAuth2\nAdded importing notes") } checknetwork() // @license-end