const path = require("path"); const { ipcRenderer } = require("electron"); const { v4: uuidv4 } = require("uuid"); // https://stackoverflow.com/a/26227660 var appDataFolder = process.env.APPDATA || (process.platform == "darwin" ? process.env.HOME + "/Library/Application Support" : process.env.HOME + "/.config"); var configFolder = path.join(appDataFolder, "FreeDATA"); var configPath = path.join(configFolder, "config.json"); const config = require(configPath); // set date format const dateFormat = new Intl.DateTimeFormat("en-GB", { timeStyle: "long", dateStyle: "full", }); // set date format information const dateFormatShort = new Intl.DateTimeFormat("en-GB", { year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric", hour12: false, }); const dateFormatHours = new Intl.DateTimeFormat("en-GB", { hour: "numeric", minute: "numeric", hour12: false, }); // split character const split_char = "\0;\1;"; // global for our selected file we want to transmit // ----------------- some chat globals var filetype = ""; var file = ""; var filename = ""; var callsign_counter = 0; var selected_callsign = ""; // ----------------------------------- var chatDB = path.join(configFolder, "chatDB"); // ---- MessageDB try { var PouchDB = require("pouchdb"); } catch (err) { console.log(err); /* This is a fix for raspberryPi where we get an error when loading pouchdb because of leveldown package isnt running on ARM devices. pouchdb-browser does not depend on leveldb and seems to be working. */ console.log("using pouchdb-browser fallback"); var PouchDB = require("pouchdb-browser"); } PouchDB.plugin(require("pouchdb-find")); //PouchDB.plugin(require('pouchdb-replication')); var db = new PouchDB(chatDB); /* // REMOTE SYNC ATTEMPTS var remoteDB = new PouchDB('http://172.20.10.4:5984/chatDB') // we need express packages for running pouchdb sync "express-pouchdb" var express = require('express'); var app = express(); //app.use('/chatDB', require('express-pouchdb')(PouchDB)); //app.listen(5984); app.use('/chatDB', require('pouchdb-express-router')(PouchDB)); app.listen(5984); db.sync('http://172.20.10.4:5984/jojo', { //var sync = PouchDB.sync('chatDB', 'http://172.20.10.4:5984/chatDB', { live: true, retry: false }).on('change', function (change) { // yo, something changed! console.log(change) }).on('paused', function (err) { // replication was paused, usually because of a lost connection console.log(err) }).on('active', function (info) { // replication was resumed console.log(info) }).on('error', function (err) { // totally unhandled error (shouldn't happen) console.log(err) }).on('denied', function (err) { // a document failed to replicate (e.g. due to permissions) console.log(err) }).on('complete', function (info) { // handle complete; console.log(info) }); */ var dxcallsigns = new Set(); db.createIndex({ index: { fields: [ "timestamp", "uuid", "dxcallsign", "dxgrid", "msg", "checksum", "type", "command", "status", "percent", "bytesperminute", "_attachments", ], }, }) .then(function (result) { // handle result console.log(result); }) .catch(function (err) { console.log(err); }); db.find({ selector: { timestamp: { $exists: true, }, }, sort: [ { timestamp: "asc", }, ], }) .then(function (result) { // handle result if (typeof result !== "undefined") { result.docs.forEach(function (item) { //console.log(item) // another query with attachments db.get(item._id, { attachments: true, }).then(function (item_with_attachments) { update_chat(item_with_attachments); }); }); } }) .catch(function (err) { console.log(err); }); // WINDOW LISTENER window.addEventListener("DOMContentLoaded", () => { // theme selector if (config.theme != "default") { var theme_path = "../node_modules/bootswatch/dist/" + config.theme + "/bootstrap.min.css"; document.getElementById("bootstrap_theme").href = theme_path; } else { var theme_path = "../node_modules/bootstrap/dist/css/bootstrap.min.css"; document.getElementById("bootstrap_theme").href = theme_path; } document .querySelector("emoji-picker") .addEventListener("emoji-click", (event) => { var msg = document.getElementById("chatModuleMessage"); //Convert to utf-8--so we can just use utf-8 everywhere msg.setRangeText(event.detail.emoji.unicode.toString("utf-8")); //console.log(event.detail); //msg.focus(); }); document.getElementById("emojipickerbutton").addEventListener("click", () => { var element = document.getElementById("emojipickercontainer"); console.log(element.style.display); if (element.style.display === "none") { element.style.display = "block"; } else { element.style.display = "none"; } }); document .getElementById("delete_selected_chat") .addEventListener("click", () => { db.find({ selector: { dxcallsign: selected_callsign, }, }) .then(function (result) { // handle result if (typeof result !== "undefined") { result.docs.forEach(function (item) { console.log(item); db.get(item._id) .then(function (doc) { db.remove(doc) .then(function (doc) { return location.reload(); }) .catch(function (err) { console.log(err); }); }) .catch(function (err) { console.log(err); }); }); } }) .catch(function (err) { console.log(err); }); }); document.getElementById("selectFilesButton").addEventListener("click", () => { //document.getElementById('selectFiles').click(); ipcRenderer.send("select-file", { title: "Title", }); }); document.getElementById("ping").addEventListener("click", () => { ipcRenderer.send("run-tnc-command", { command: "ping", dxcallsign: selected_callsign, }); }); document.addEventListener("keyup", function (event) { // Number 13 == Enter if (event.keyCode === 13 && !event.shiftKey) { // Cancel the default action, if needed event.preventDefault(); // Trigger the button element with a click document.getElementById("sendMessage").click(); } }); // ADJUST TEXTAREA SIZE document.getElementById("chatModuleMessage").addEventListener("input", () => { var textarea = document.getElementById("chatModuleMessage"); var text = textarea.value; if (document.getElementById("expand_textarea").checked) { var lines = 6; } else { var lines = text.split("\n").length; if (lines >= 6) { lines = 6; } } var message_container_height_offset = 130 + 20 * lines; var message_container_height = `calc(100% - ${message_container_height_offset}px)`; document.getElementById("message-container").style.height = message_container_height; textarea.rows = lines; console.log(textarea.value); }); document.getElementById("expand_textarea").addEventListener("click", () => { var textarea = document.getElementById("chatModuleMessage"); if (document.getElementById("expand_textarea").checked) { var lines = 6; document.getElementById("expand_textarea_button").className = "bi bi-chevron-compact-down"; } else { var lines = 1; document.getElementById("expand_textarea_button").className = "bi bi-chevron-compact-up"; } var message_container_height_offset = 130 + 20 * lines; //var message_container_height_offset = 90 + (23*lines); var message_container_height = `calc(100% - ${message_container_height_offset}px)`; document.getElementById("message-container").style.height = message_container_height; textarea.rows = lines; console.log(textarea.rows); }); // NEW CHAT document .getElementById("createNewChatButton") .addEventListener("click", () => { var dxcallsign = document.getElementById("chatModuleNewDxCall").value; var uuid = uuidv4(); db.post({ _id: uuid, timestamp: Math.floor(Date.now() / 1000), dxcallsign: dxcallsign.toUpperCase(), dxgrid: "---", msg: "null", checksum: "null", type: "newchat", status: "null", uuid: uuid, }) .then(function (response) { // handle response console.log("new database entry"); console.log(response); }) .catch(function (err) { console.log(err); }); update_chat_obj_by_uuid(uuid); }); // SEND MSG document.getElementById("sendMessage").addEventListener("click", () => { document.getElementById("emojipickercontainer").style.display = "none"; var dxcallsign = selected_callsign.toUpperCase(); var textarea = document.getElementById("chatModuleMessage"); var chatmessage = textarea.value; //Remove non-printable chars from begining and end of string--should save us a byte here and there chatmessage = chatmessage.toString().trim(); // reset textarea size var message_container_height_offset = 150; var message_container_height = `calc(100% - ${message_container_height_offset}px)`; document.getElementById("message-container").style.height = message_container_height; textarea.rows = 1; document.getElementById("expand_textarea_button").className = "bi bi-chevron-compact-up"; document.getElementById("expand_textarea").checked = false; console.log(file); console.log(filename); console.log(filetype); if (filetype == "") { filetype = "plain/text"; } var timestamp = Math.floor(Date.now() / 1000); var file_checksum = crc32(file).toString(16).toUpperCase(); console.log(file_checksum); var data_with_attachment = timestamp + split_char + chatmessage + split_char + filename + split_char + filetype + split_char + file; document.getElementById("selectFilesButton").innerHTML = ``; var uuid = uuidv4(); let uuidlast = uuid.lastIndexOf("-"); uuidlast += 1; if (uuidlast > 0) { uuid = uuid.substring(uuidlast); } console.log(data_with_attachment); let Data = { command: "send_message", dxcallsign: dxcallsign, mode: 255, frames: 1, data: data_with_attachment, checksum: file_checksum, uuid: uuid, }; ipcRenderer.send("run-tnc-command", Data); db.post({ _id: uuid, timestamp: timestamp, dxcallsign: dxcallsign, dxgrid: "null", msg: chatmessage, checksum: file_checksum, type: "transmit", status: "transmit", uuid: uuid, _attachments: { [filename]: { content_type: filetype, //data: btoa(file) data: btoa_FD(file), }, }, }) .then(function (response) { // handle response console.log("new database entry"); console.log(response); }) .catch(function (err) { console.log(err); }); update_chat_obj_by_uuid(uuid); // clear input document.getElementById("chatModuleMessage").value = ""; // after adding file data to our attachment variable, delete it from global filetype = ""; file = ""; filename = ""; }); // cleanup after transmission filetype = ""; file = ""; filename = ""; }); ipcRenderer.on("return-selected-files", (event, arg) => { filetype = arg.mime; console.log(filetype); file = arg.data; filename = arg.filename; document.getElementById("selectFilesButton").innerHTML = ` New file selected `; }); ipcRenderer.on("action-update-transmission-status", (event, arg) => { var data = arg["data"][0]; console.log(data.status); db.get(data.uuid, { attachments: true, }) .then(function (doc) { return db.put({ _id: data.uuid, _rev: doc._rev, timestamp: doc.timestamp, dxcallsign: doc.dxcallsign, dxgrid: doc.dxgrid, msg: doc.msg, checksum: doc.checksum, type: "transmit", status: data.status, percent: data.percent, bytesperminute: data.bytesperminute, uuid: doc.uuid, _attachments: doc._attachments, }); }) .then(function (response) { update_chat_obj_by_uuid(data.uuid); }) .catch(function (err) { console.log(err); console.log(data); }); }); ipcRenderer.on("action-new-msg-received", (event, arg) => { console.log(arg.data); var new_msg = arg.data; new_msg.forEach(function (item) { console.log(item.status); let obj = new Object(); //handle ping if (item.ping == "received") { obj.timestamp = parseInt(item.timestamp); obj.dxcallsign = item.dxcallsign; obj.dxgrid = item.dxgrid; obj.uuid = item.uuid; obj.command = "ping"; obj.checksum = "null"; obj.msg = "null"; obj.status = item.status; obj.snr = item.snr; obj.type = "ping"; obj.filename = "null"; obj.filetype = "null"; obj.file = "null"; add_obj_to_database(obj); update_chat_obj_by_uuid(obj.uuid); // handle beacon } else if (item.beacon == "received") { obj.timestamp = parseInt(item.timestamp); obj.dxcallsign = item.dxcallsign; obj.dxgrid = item.dxgrid; obj.uuid = item.uuid; obj.command = "beacon"; obj.checksum = "null"; obj.msg = "null"; obj.status = item.status; obj.snr = item.snr; obj.type = "beacon"; obj.filename = "null"; obj.filetype = "null"; obj.file = "null"; add_obj_to_database(obj); update_chat_obj_by_uuid(obj.uuid); // handle ARQ transmission } else if (item.arq == "transmission" && item.status == "received") { //var encoded_data = atob(item.data); //var encoded_data = Buffer.from(item.data,'base64').toString('utf-8'); var encoded_data = atob_FD(item.data); var splitted_data = encoded_data.split(split_char); console.log(splitted_data); obj.timestamp = parseInt(splitted_data[4]); obj.dxcallsign = item.dxcallsign; obj.dxgrid = item.dxgrid; obj.command = splitted_data[1]; obj.checksum = splitted_data[2]; // convert message to unicode from utf8 because of emojis //No, don't convert; we're already UTF-8!!!!! obj.uuid = splitted_data[3]; obj.msg = splitted_data[5]; obj.status = "null"; obj.snr = "null"; obj.type = "received"; obj.filename = splitted_data[6]; obj.filetype = splitted_data[7]; //obj.file = btoa(splitted_data[8]); obj.file = btoa_FD(splitted_data[8]); add_obj_to_database(obj); update_chat_obj_by_uuid(obj.uuid); } }); //window.location = window.location; }); // Update chat list update_chat = function (obj) { var dxcallsign = obj.dxcallsign; var timestamp = dateFormat.format(obj.timestamp * 1000); var timestampShort = dateFormatShort.format(obj.timestamp * 1000); var timestampHours = dateFormatHours.format(obj.timestamp * 1000); var dxgrid = obj.dxgrid; // define shortmessage if (obj.msg == "null" || obj.msg == "NULL") { var shortmsg = obj.type; } else { var shortmsg = obj.msg; var maxlength = 30; var shortmsg = shortmsg.length > maxlength ? shortmsg.substring(0, maxlength - 3) + "..." : shortmsg; } try { //console.log(Object.keys(obj._attachments)[0].length) if ( typeof obj._attachments !== "undefined" && Object.keys(obj._attachments)[0].length > 0 ) { //var filename = obj._attachments; var filename = Object.keys(obj._attachments)[0]; var filetype = filename.split(".")[1]; var filesize = obj._attachments[filename]["length"] + " Bytes"; if (filesize == "undefined Bytes") { // get filesize of new submitted data // not that nice.... // we really should avoid converting back from base64 for performance reasons... //var filesize = Math.ceil(atob(obj._attachments[filename]["data"]).length) + "Bytes"; var filesize = Math.ceil(atob_FD(obj._attachments[filename]["data"]).length) + " Bytes"; } // check if image, then display it if (filetype == "image/png" || filetype == "png") { var fileheader = `

${filename} ${filesize}


`; } else { var fileheader = `

${filename} ${filesize}


`; } var controlarea_transmit = `
`; var controlarea_receive = `
`; } else { var filename = ""; var fileheader = ""; var filetype = "text/plain"; var controlarea_transmit = `
`; var controlarea_receive = ""; } } catch (err) { console.log("error with database parsing..."); console.log(err); } // CALLSIGN LIST if (!document.getElementById("chat-" + dxcallsign + "-list")) { // increment callsign counter callsign_counter++; if (callsign_counter == 1) { var callsign_selected = "active show"; //document.getElementById('chatModuleDxCall').value = dxcallsign; selected_callsign = dxcallsign; } var new_callsign = `
${dxcallsign}
${dxgrid} ${timestampHours} ${shortmsg}
`; document .getElementById("list-tab") .insertAdjacentHTML("beforeend", new_callsign); var message_area = `
`; document .getElementById("nav-tabContent") .insertAdjacentHTML("beforeend", message_area); // create eventlistener for listening on clicking on a callsign document .getElementById("chat-" + dxcallsign + "-list") .addEventListener("click", function () { //document.getElementById('chatModuleDxCall').value = dxcallsign; selected_callsign = dxcallsign; setTimeout(scrollMessagesToBottom, 200); }); // if callsign entry already exists - update } else { // gridsquare - update only on receive if (obj.type !== "transmit") { document.getElementById("chat-" + dxcallsign + "-list-dxgrid").innerHTML = dxgrid; } // time document.getElementById("chat-" + dxcallsign + "-list-time").innerHTML = timestampHours; // short message document.getElementById("chat-" + dxcallsign + "-list-shortmsg").innerHTML = shortmsg; } // APPEND MESSAGES TO CALLSIGN if (!document.getElementById("msg-" + obj._id)) { if (obj.type == "ping") { var new_message = `

snr: ${obj.snr} - ${timestamp}

`; } if (obj.type == "beacon") { var new_message = `

snr: ${obj.snr} - ${timestamp}

`; } if (obj.type == "newchat") { var new_message = `

new chat opened - ${timestamp}

`; } // CHECK FOR NEW LINE AND REPLACE WITH
var message_html = obj.msg.replaceAll(/\n/g, "
"); if (obj.type == "received") { var new_message = `
${fileheader}

${message_html}

${timestamp}

${controlarea_receive}
`; } if (obj.type == "transmit") { //console.log('msg-' + obj._id + '-status') if (obj.status == "failed") { var progressbar_bg = "bg-danger"; } else { var progressbar_bg = "bg-primary"; } //Sneak in low graphics mode if so enabled for progress bars if (config.high_graphics.toString().toUpperCase() != "TRUE") { progressbar_bg += " disable-effects"; //console.log("Low graphics enabled for chat module"); } var new_message = `
${controlarea_transmit}
${fileheader}

${message_html}

${timestamp} - ${get_icon_for_state( obj.status )}

${obj.percent} % - ${obj.bytesperminute} Bpm

`; } // CHECK CHECK CHECK --> This could be done better var id = "chat-" + obj.dxcallsign; document.getElementById(id).insertAdjacentHTML("beforeend", new_message); /* UPDATE EXISTING ELEMENTS */ } else if (document.getElementById("msg-" + obj._id)) { console.log("element already exists......"); console.log(obj); console.log( document .getElementById("msg-" + obj._id + "-progress") .getAttribute("aria-valuenow") ); document.getElementById("msg-" + obj._id + "-status").innerHTML = get_icon_for_state(obj.status); document .getElementById("msg-" + obj._id + "-progress") .setAttribute("aria-valuenow", obj.percent); document .getElementById("msg-" + obj._id + "-progress") .setAttribute("style", "width:" + obj.percent + "%;"); document.getElementById( "msg-" + obj._id + "-progress-information" ).innerHTML = obj.percent + "% - " + obj.bytesperminute + " Bpm"; if (obj.percent >= 100) { //document.getElementById('msg-' + obj._id + '-progress').classList.remove("progress-bar-striped"); document .getElementById("msg-" + obj._id + "-progress") .classList.remove("progress-bar-animated"); document .getElementById("msg-" + obj._id + "-progress") .classList.remove("bg-danger"); document .getElementById("msg-" + obj._id + "-progress") .classList.add("bg-primary"); document.getElementById("msg-" + obj._id + "-progress").innerHTML = ""; } else { document .getElementById("msg-" + obj._id + "-progress") .classList.add("progress-bar-striped"); document .getElementById("msg-" + obj._id + "-progress") .classList.add("progress-bar-animated"); } if (obj.status == "failed") { //document.getElementById('msg-' + obj._id + '-progress').classList.remove("progress-bar-striped"); document .getElementById("msg-" + obj._id + "-progress") .classList.remove("progress-bar-animated"); document .getElementById("msg-" + obj._id + "-progress") .classList.remove("bg-primary"); document .getElementById("msg-" + obj._id + "-progress") .classList.add("bg-danger"); } //document.getElementById(id).className = message_class; } // CREATE SAVE TO FOLDER EVENT LISTENER if ( document.getElementById("save-file-msg-" + obj._id) && !document .getElementById("save-file-msg-" + obj._id) .hasAttribute("listenerOnClick") ) { // set Attribute to determine if we already created an EventListener for this element document .getElementById("save-file-msg-" + obj._id) .setAttribute("listenerOnClick", "true"); document .getElementById("save-file-msg-" + obj._id) .addEventListener("click", () => { saveFileToFolder(obj._id); }); } // CREATE RESEND MSG EVENT LISTENER // check if element exists and if we already created NOT created an event listener if ( document.getElementById("retransmit-msg-" + obj._id) && !document .getElementById("retransmit-msg-" + obj._id) .hasAttribute("listenerOnClick") ) { // set Attribute to determine if we already created an EventListener for this element document .getElementById("retransmit-msg-" + obj._id) .setAttribute("listenerOnClick", "true"); document .getElementById("retransmit-msg-" + obj._id) .addEventListener("click", () => { db.get(obj._id, { attachments: true, }) .then(function (doc) { // handle doc console.log(doc); var filename = Object.keys(obj._attachments)[0]; var filetype = filename.content_type; console.log(filename); console.log(filetype); var file = obj._attachments[filename].data; console.log(file); console.log(Object.keys(obj._attachments)[0].data); //var file = atob(obj._attachments[filename]["data"]) db.getAttachment(obj._id, filename).then(function (data) { console.log(data); //Rewrote this part to use buffers to ensure encoding is corect -- n1qm var binaryString = atob_FD(data); console.log(binaryString); var data_with_attachment = doc.timestamp + split_char + doc.msg + split_char + filename + split_char + filetype + split_char + binaryString; let Data = { command: "send_message", dxcallsign: doc.dxcallsign, mode: 255, frames: 1, data: data_with_attachment, checksum: doc.checksum, uuid: doc.uuid, }; console.log(Data); ipcRenderer.send("run-tnc-command", Data); }); /* // convert blob data to binary string blobUtil.blobToBinaryString(data).then(function (binaryString) { console.log(binaryString) }).catch(function (err) { // error console.log(err); binaryString = blobUtil.arrayBufferToBinaryString(data); }).then(function(){ console.log(binaryString) console.log(binaryString.length) var data_with_attachment = doc.timestamp + split_char + utf8.encode(doc.msg) + split_char + filename + split_char + filetype + split_char + binaryString; let Data = { command: "send_message", dxcallsign: doc.dxcallsign, mode: 255, frames: 1, data: data_with_attachment, checksum: doc.checksum, uuid: doc.uuid }; console.log(Data) ipcRenderer.send('run-tnc-command', Data); }); }); */ }) .catch(function (err) { console.log(err); }); }); } //window.location = window.location // scroll to bottom on new message scrollMessagesToBottom(); }; function saveFileToFolder(id) { db.get(id, { attachments: true, }) .then(function (obj) { console.log(obj); console.log(Object.keys(obj._attachments)[0].content_type); var filename = Object.keys(obj._attachments)[0]; var filetype = filename.content_type; var file = filename.data; console.log(file); console.log(filename.data); db.getAttachment(id, filename) .then(function (data) { // handle result console.log(data.length); //data = new Blob([data.buffer], { type: 'image/png' } /* (1) */) console.log(data); // we need to encode data because of error "an object could not be cloned" let Data = { file: data, filename: filename, filetype: filetype, }; console.log(Data); ipcRenderer.send("save-file-to-folder", Data); }) .catch(function (err) { console.log(err); return false; }); }) .catch(function (err) { console.log(err); }); } // function for setting an ICON to the corresponding state function get_icon_for_state(state) { if (state == "transmit") { var status_icon = ''; } else if (state == "transmitting") { //var status_icon = ''; var status_icon = ` `; } else if (state == "failed") { var status_icon = ''; } else if (state == "transmitted") { var status_icon = ''; } else { var status_icon = ''; } return status_icon; } update_chat_obj_by_uuid = function (uuid) { db.get(uuid, { attachments: true, }) .then(function (doc) { update_chat(doc); //return doc }) .catch(function (err) { console.log(err); }); }; add_obj_to_database = function (obj) { db.put({ _id: obj.uuid, timestamp: parseInt(obj.timestamp), uuid: obj.uuid, dxcallsign: obj.dxcallsign, dxgrid: obj.dxgrid, msg: obj.msg, checksum: obj.checksum, type: obj.type, command: obj.command, status: obj.status, snr: obj.snr, _attachments: { [obj.filename]: { content_type: obj.filetype, data: obj.file, }, }, }) .then(function (response) { console.log("new database entry"); console.log(response); }) .catch(function (err) { console.log(err); }); }; // Scroll to bottom of message-container function scrollMessagesToBottom() { var messageBody = document.getElementById("message-container"); messageBody.scrollTop = messageBody.scrollHeight - messageBody.clientHeight; } // CRC CHECKSUMS // https://stackoverflow.com/a/50579690 // crc32 calculation //console.log(crc32('abc')); //var crc32=function(r){for(var a,o=[],c=0;c<256;c++){a=c;for(var f=0;f<8;f++)a=1&a?3988292384^a>>>1:a>>>1;o[c]=a}for(var n=-1,t=0;t>>8^o[255&(n^r.charCodeAt(t))];return(-1^n)>>>0}; //console.log(crc32('abc').toString(16).toUpperCase()); // hex var makeCRCTable = function () { var c; var crcTable = []; for (var n = 0; n < 256; n++) { c = n; for (var k = 0; k < 8; k++) { c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; } crcTable[n] = c; } return crcTable; }; var crc32 = function (str) { var crcTable = window.crcTable || (window.crcTable = makeCRCTable()); var crc = 0 ^ -1; for (var i = 0; i < str.length; i++) { crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xff]; } return (crc ^ -1) >>> 0; }; /** * Binary to ASCII replacement * @param {string} data in normal/usual utf-8 format * @returns base64 encoded string */ function btoa_FD(data) { return Buffer.from(data, "utf-8").toString("base64"); } /** * ASCII to Binary replacement * @param {string} data in base64 encoding * @returns utf-8 normal/usual string */ function atob_FD(data) { return Buffer.from(data, "base64").toString("utf-8"); }