Flo-Whatsapp/app/app.js
sairajzero ebd532b608 Added UI for group options
Added custom checkbox list for selecting users in group options
Added buttons to invoke group options
Hover on the 'more-options' (3-dot) button in the top-right corner to view group options
Note : group options will only be displayed when user is either creator or admin of respective group
2019-09-13 01:15:49 +05:30

1564 lines
50 KiB
JavaScript

window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
if (!window.indexedDB) {
window.alert("Your browser doesn't support a stable version of IndexedDB.")
}
var contacts, groups;
var searchIndex = new FlexSearch();
var receiverID, selfID, recStat, modSuperNode, msgType;
var selfwebsocket, receiverWebSocket;
var privKey;
var floOpt = {
p: BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16),
exponent1: function () {
return floOpt.p.add(BigInteger.ONE).divide(BigInteger("4"))
},
calculateY: function (x) {
let p = this.p;
let exp = this.exponent1();
// x is x value of public key in BigInteger format without 02 or 03 or 04 prefix
return x.modPow(BigInteger("3"), p).add(BigInteger("7")).mod(p).modPow(exp, p)
},
// Insert a compressed public key
getUncompressedPublicKey: function (compressedPublicKey) {
const p = this.p;
// Fetch x from compressedPublicKey
let pubKeyBytes = Crypto.util.hexToBytes(compressedPublicKey);
const prefix = pubKeyBytes.shift() // remove prefix
let prefix_modulus = prefix % 2;
pubKeyBytes.unshift(0) // add prefix 0
let x = new BigInteger(pubKeyBytes)
let xDecimalValue = x.toString()
// Fetch y
let y = this.calculateY(x);
let yDecimalValue = y.toString();
// verify y value
let resultBigInt = y.mod(BigInteger("2"));
let check = resultBigInt.toString() % 2;
if (prefix_modulus !== check) {
yDecimalValue = y.negate().mod(p).toString();
}
return {
x: xDecimalValue,
y: yDecimalValue
};
},
getSenderPublicKeyString: function () {
privateKey = ellipticCurveEncryption.senderRandom();
senderPublicKeyString = ellipticCurveEncryption.senderPublicString(privateKey);
return {
privateKey: privateKey,
senderPublicKeyString: senderPublicKeyString
}
},
deriveSharedKeySender: function (receiverCompressedPublicKey, senderPrivateKey) {
try {
let receiverPublicKeyString = this.getUncompressedPublicKey(
receiverCompressedPublicKey);
var senderDerivedKey = {
XValue: "",
YValue: ""
};
senderDerivedKey = ellipticCurveEncryption.senderSharedKeyDerivation(
receiverPublicKeyString.x,
receiverPublicKeyString.y, senderPrivateKey);
return senderDerivedKey;
} catch (error) {
return new Error(error);
}
},
deriveReceiverSharedKey: function (senderPublicKeyString, receiverPrivateKey) {
return ellipticCurveEncryption.receiverSharedKeyDerivation(
senderPublicKeyString.XValuePublicString, senderPublicKeyString.YValuePublicString,
receiverPrivateKey);
},
getReceiverPublicKeyString: function (privateKey) {
return ellipticCurveEncryption.receiverPublicString(privateKey);
},
deriveSharedKeyReceiver: function (senderPublicKeyString, receiverPrivateKey) {
try {
return ellipticCurveEncryption.receiverSharedKeyDerivation(senderPublicKeyString.XValuePublicString,
senderPublicKeyString.YValuePublicString, receiverPrivateKey);
} catch (error) {
return new Error(error);
}
},
encryptData: function (data, receiverCompressedPublicKey) {
var senderECKeyData = this.getSenderPublicKeyString();
var senderDerivedKey = {
XValue: "",
YValue: ""
};
var senderPublicKeyString = {};
senderDerivedKey = this.deriveSharedKeySender(
receiverCompressedPublicKey, senderECKeyData.privateKey);
console.log("senderDerivedKey", senderDerivedKey);
let senderKey = senderDerivedKey.XValue + senderDerivedKey.YValue;
let secret = Crypto.AES.encrypt(data, senderKey);
return {
secret: secret,
pubVal: senderECKeyData.senderPublicKeyString
};
},
decryptData: function (secret, senderPublicKeyString, myPrivateKey) {
var receiverDerivedKey = {
XValue: "",
YValue: ""
};
var receiverECKeyData = {};
if (typeof myPrivateKey !== "string") throw new Error("No private key found.");
let privateKey = this.wifToDecimal(myPrivateKey, true);
if (typeof privateKey.privateKeyDecimal !== "string") throw new Error(
"Failed to detremine your private key.");
receiverECKeyData.privateKey = privateKey.privateKeyDecimal;
receiverDerivedKey = this.deriveReceiverSharedKey(senderPublicKeyString,
receiverECKeyData.privateKey);
console.log("receiverDerivedKey", receiverDerivedKey);
let receiverKey = receiverDerivedKey.XValue + receiverDerivedKey.YValue;
let decryptMsg = Crypto.AES.decrypt(secret, receiverKey);
return decryptMsg;
},
ecparams: EllipticCurve.getSECCurveByName("secp256k1"),
getPubKeyHex: function (privateKeyHex) {
var key = new Bitcoin.ECKey(privateKeyHex);
if (key.priv == null) {
alert("Invalid Private key");
return;
}
key.setCompressed(true);
var pubkeyHex = key.getPubKeyHex();
return pubkeyHex;
},
getFLOIDfromPubkeyHex: function (pubkeyHex) {
var key = new Bitcoin.ECKey().setPub(pubkeyHex);
var floID = key.getBitcoinAddress();
return floID;
},
signData: function (msg, privateKeyHex) {
var key = new Bitcoin.ECKey(privateKeyHex);
key.setCompressed(true);
var privateKeyArr = key.getBitcoinPrivateKeyByteArray();
privateKey = BigInteger.fromByteArrayUnsigned(privateKeyArr);
var messageHash = Crypto.SHA256(msg);
var messageHashBigInteger = new BigInteger(messageHash);
var messageSign = Bitcoin.ECDSA.sign(messageHashBigInteger, key.priv);
var sighex = Crypto.util.bytesToHex(messageSign);
return sighex;
},
verifyData: function (msg, signatureHex, publicKeyHex) {
var msgHash = Crypto.SHA256(msg);
var messageHashBigInteger = new BigInteger(msgHash);
var sigBytes = Crypto.util.hexToBytes(signatureHex);
var signature = Bitcoin.ECDSA.parseSig(sigBytes);
var publicKeyPoint = this.ecparams.getCurve().decodePointHex(publicKeyHex);
var verify = Bitcoin.ECDSA.verifyRaw(messageHashBigInteger, signature.r, signature.s,
publicKeyPoint);
return verify;
},
wifToDecimal: function (pk_wif, isPubKeyCompressed = false) {
let pk = Bitcoin.Base58.decode(pk_wif)
pk.shift()
pk.splice(-4, 4)
//If the private key corresponded to a compressed public key, also drop the last byte (it should be 0x01).
if (isPubKeyCompressed == true) pk.pop()
pk.unshift(0)
privateKeyDecimal = BigInteger(pk).toString()
privateKeyHex = Crypto.util.bytesToHex(pk)
return {
privateKeyDecimal: privateKeyDecimal,
privateKeyHex: privateKeyHex
}
},
genNewIDpair: function () {
try {
var key = new Bitcoin.ECKey(false);
key.setCompressed(true);
return {
floID: key.getBitcoinAddress(),
pubKey: key.getPubKeyHex(),
privKey: key.getBitcoinWalletImportFormat()
}
} catch (e) {
console.log(e);
}
}
}
//Script for AJAX, and register functions
function ajax(method, uri) {
var request = new XMLHttpRequest();
var url = `${server}/${uri}`
console.log(url)
var result;
request.open(method, url, false);
request.onload = function () {
if (request.readyState == 4 && request.status == 200)
result = this.response;
else {
console.log('error');
result = false;
}
};
request.send();
console.log(result);
return result;
}
function registerID(sender, onionAddr, wif, pubkey, username) {
var receiver = adminID;
var trx = bitjs.transaction();
var utxoAmt = 0.0;
var x = sendAmt + fee;
var response = ajax("GET", `api/addr/${sender}/utxo`);
var utxos = JSON.parse(response);
for (var x = utxos.length - 1; x >= 0; x--) {
if (utxoAmt < sendAmt + fee) {
trx.addinput(utxos[x].txid, utxos[x].vout, utxos[x].scriptPubKey);
utxoAmt += utxos[x].amount;
} else
break;
}
console.log(utxoAmt + ":" + (sendAmt + fee));
if (utxoAmt < sendAmt + fee) {
alert("Insufficient balance!");
return;
}
trx.addoutput(receiver, sendAmt);
console.log(receiver + ":" + sendAmt);
var change = utxoAmt - sendAmt - fee;
if (change > 0)
trx.addoutput(sender, change);
console.log(sender + ":" + change);
var key = new Bitcoin.ECKey(wif);
var sendFloData = JSON.stringify({
FLO_chat: {
onionAddr: onionAddr,
name: username,
pubKey: pubkey
}
});;
trx.addflodata(sendFloData);
console.log(sendFloData);
var signedTxHash = trx.sign(wif, 1);
console.log(signedTxHash);
return broadcastTx(signedTxHash);
}
function broadcastTx(signedTxHash) {
var http = new XMLHttpRequest();
var url = `${server}/api/tx/send`;
if (signedTxHash.length < 1) {
alert("Empty Signature");
return false;
}
var params = `{"rawtx":"${signedTxHash}"}`;
var result;
http.open('POST', url, false);
//Send the proper header information along with the request
http.setRequestHeader('Content-type', 'application/json');
http.onreadystatechange = () => { //Call a function when the state changes.
if (http.readyState == 4 && http.status == 200) {
console.log(http.response);
var txid = JSON.parse(http.response).txid.result;
alert("Transaction successful! txid : " + txid);
result = true;
} else {
console.log(http.responseText);
result = false;
}
}
http.send(params);
return result;
}
function userDataStartUp() {
console.log("StartUp");
document.getElementById("sendMsgInput").addEventListener("keyup", (event) => {
if (event.keyCode === 13) {
event.preventDefault();
sendMsg();
}
});
document.getElementById("searchContact").addEventListener("input", searchContact, true);
document.getElementById("searchList").addEventListener("input", searchChecklist, true);
getDatafromAPI().then(result => {
console.log(result);
getContactsfromIDB().then(result => {
contacts = result;
getSuperNodeListfromIDB().then(result => {
console.log(result)
superNodeList = result;
kBucketObj.launchKBucket().then(result => {
console.log(result)
getuserID().then(result => {
console.log(result);
selfID = result;
if (superNodeList.includes(selfID))
modSuperNode = true;
alert(`${selfID}\nWelcome ${contacts[selfID].name}`)
getGroupsfromIDB().then(result => {
groups = result;
readMsgfromIDB().then(result => {
console.log(result);
readGroupMsgfromIDB().then(result => {
console.log(result);
initselfWebSocket();
pingSuperNodeForAwayMessages();
displayContacts();
const createClock = setInterval(checkStatusInterval, 30000);
}).catch(error => {
console.log(error);
});
}).catch(error => {
console.log(error);
});
}).catch(error => {
console.log(error.message);
});
}).catch(error => {
console.log(error.message);
});
}).catch(error => {
console.log(error.message);
});
}).catch(error => {
console.log(error.message);
});
}).catch(error => {
console.log(error.message);
});
}).catch(error => {
console.log(error.message);
});
}
function storeContact(data) {
return new Promise((resolve, reject) => {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction('contacts', "readwrite").objectStore('contacts');
objectRequest = obs.put(data);
objectRequest.onerror = (event) => {
reject(Error('Error occured: Unable to store data'));
};
objectRequest.onsuccess = (event) => {
resolve('Data saved OK');
db.close();
};
};
});
}
function storeSuperNodeData(data) {
return new Promise((resolve, reject) => {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction('superNodes', "readwrite").objectStore('superNodes');
if (data.addNodes)
for (var i = 0; i < data.addNodes.length; i++)
obs.add(true, data.addNodes[i])
if (data.removeNodes)
for (var i = 0; i < data.removeNodes.length; i++)
obs.delete(data.removeNodes[i])
db.close();
resolve('Updated superNodes list in IDB');
};
});
}
function getDatafromAPI() {
return new Promise((resolve, reject) => {
var addr = adminID;
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onupgradeneeded = (event) => {
var db = event.target.result;
var objectStore0 = db.createObjectStore("superNodes");
var objectStore1 = db.createObjectStore("contacts", {
keyPath: 'floID'
});
objectStore1.createIndex('onionAddr', 'onionAddr', {
unique: false
});
objectStore1.createIndex('name', 'name', {
unique: false
});
objectStore1.createIndex('pubKey', 'pubKey', {
unique: false
});
var objectStore2 = db.createObjectStore("lastTx");
var objectStore3 = db.createObjectStore("messages", {
keyPath: 'time'
});
objectStore3.createIndex('text', 'text', {
unique: false
});
objectStore3.createIndex('floID', 'floID', {
unique: false
});
objectStore3.createIndex('type', 'type', {
unique: false
});
var objectStore4 = db.createObjectStore("groups", {
keyPath: 'groupID'
});
objectStore4.createIndex('groupInfo', 'groupInfo', {
unique: false
});
var objectStore5 = db.createObjectStore("groupMsg", {
keyPath: 'time'
});
objectStore5.createIndex('text', 'text', {
unique: false
});
objectStore5.createIndex('groupID', 'groupID', {
unique: false
});
objectStore5.createIndex('type', 'type', {
unique: false
});
objectStore5.createIndex('sender', 'sender', {
unique: false
});
};
idb.onsuccess = (event) => {
var db = event.target.result;
var lastTx = db.transaction('lastTx', "readwrite").objectStore('lastTx');
console.log(addr);
new Promise((res, rej) => {
var lastTxReq = lastTx.get(addr);
lastTxReq.onsuccess = (event) => {
var lasttx = event.target.result;
if (lasttx === undefined) {
lasttx = 0;
}
res(lasttx);
}
}).then(lasttx => {
var response = ajax("GET", `api/addrs/${addr}/txs`);
var nRequired = JSON.parse(response).totalItems - lasttx;
console.log(nRequired);
while (true && nRequired) {
var response = ajax("GET", `api/addrs/${addr}/txs?from=0&to=${nRequired}`);
response = JSON.parse(response);
if (nRequired + lasttx != response.totalItems) {
nRequired = response.totalItems - lasttx;
continue;
}
response.items.reverse().forEach(tx => {
try {
if (tx.vin[0].addr == addr) {
var data = JSON.parse(tx.floData).FLO_chat_SuperNode;
if (data !== undefined) {
storeSuperNodeData(data).then(response => {}).catch(error => {
console.log(error.message);
});
}
} else {
var data = JSON.parse(tx.floData).FLO_chat;
if (data !== undefined) {
if (floOpt.getFLOIDfromPubkeyHex(data.pubKey) != tx.vin[0].addr)
throw ("PublicKey doesnot match with floID")
data = {
floID: tx.vin[0].addr,
onionAddr: data.onionAddr,
name: data.name,
pubKey: data.pubKey
};
storeContact(data).then(response => {}).catch(error => {
console.log(error.message);
});
}
}
} catch (e) {
console.log(e)
}
});
var obs = db.transaction('lastTx', "readwrite").objectStore('lastTx');
obs.put(response.totalItems, addr);
break;
}
db.close();
resolve('retrived data from API');
});
};
});
}
function getuserID() {
return new Promise((resolve, reject) => {
privKey = prompt("Enter FLO Private Key : ")
var key = new Bitcoin.ECKey(privKey);
while (key.priv == null) {
privKey = prompt("Invalid FLO Private Key! Retry : ")
key = Bitcoin.ECKey(privKey);
}
key.setCompressed(true);
var userID = key.getBitcoinAddress();
if (contacts[userID] === undefined)
var reg = confirm(`${userID} is not registers to FLO chat!\nRegister FLO ID to this onion?`);
else if (contacts[userID].onionAddr == window.location.host)
resolve(userID)
else
var reg = confirm(`${userID} is registered to another onion!\nChange to this onion?`);
if (reg) {
var name = prompt("Enter your name :");
var pubKey = key.getPubKeyHex();
if (registerID(userID, window.location.host, privKey, pubKey, name)) {
contacts[userID] = {
onionAddr: window.location.host,
name: name,
pubKey: pubKey
};
resolve(userID);
}
}
reject(`Unable to bind ${userID} to this onionAddress!\nTry again later!`);
});
}
function getContactsfromIDB() {
return new Promise((resolve, reject) => {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("contacts", "readwrite").objectStore("contacts");
var getReq = obs.getAll();
getReq.onsuccess = (event) => {
var result = {}
event.target.result.forEach(c => {
result[c.floID] = c;
searchIndex.add(c.floID, c.name + ' @' + c.floID);
});
resolve(result);
}
getReq.onerror = (event) => {
reject('Unable to read contacts!')
}
db.close();
};
});
}
function getGroupsfromIDB() {
return new Promise((resolve, reject) => {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("groups", "readwrite").objectStore("groups");
var getReq = obs.getAll();
getReq.onsuccess = (event) => {
var result = {}
event.target.result.forEach(g => {
var gInfo = JSON.parse(g.groupInfo);
result[g.groupID] = gInfo;
searchIndex.add(g.groupID, gInfo.name + ' #' + gInfo.floID);
});
resolve(result);
}
getReq.onerror = (event) => {
reject('Unable to read groups!')
}
db.close();
};
});
}
function getSuperNodeListfromIDB() {
return new Promise((resolve, reject) => {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("superNodes", "readwrite").objectStore("superNodes");
var getReq = obs.getAllKeys();
getReq.onsuccess = (event) => {
resolve(event.target.result);
}
getReq.onerror = (event) => {
reject('Unable to read superNode list!')
}
db.close();
};
});
}
function readMsgfromIDB() {
return new Promise((resolve, reject) => {
var disp = document.getElementById("conversation");
for (floID in contacts) {
var createLi = document.createElement('div');
createLi.setAttribute("id", floID);
createLi.setAttribute("class", "message-inner");
createLi.style.display = 'none';
disp.appendChild(createLi);
}
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("messages", "readwrite").objectStore("messages");
obs.openCursor().onsuccess = (event) => {
var cursor = event.target.result;
if (cursor) {
createMsgElement(cursor.value);
cursor.continue();
} else {
resolve("Read Msg from IDB");
}
};
db.close();
};
});
}
function readGroupMsgfromIDB() {
return new Promise((resolve, reject) => {
var disp = document.getElementById("conversation");
for (floID in groups) {
var createLi = document.createElement('div');
createLi.setAttribute("id", floID);
createLi.setAttribute("class", "message-inner");
createLi.style.display = 'none';
disp.appendChild(createLi);
}
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
reject("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("groupMsg", "readwrite").objectStore("groupMsg");
obs.openCursor().onsuccess = (event) => {
var cursor = event.target.result;
if (cursor) {
createMsgElement(cursor.value);
cursor.continue();
} else {
resolve("Read Group Msgs from IDB");
}
};
db.close();
};
});
}
function storeMsg(data) {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("messages", "readwrite").objectStore("messages");
obs.add(data);
db.close();
};
}
function storeGroupMsg(data) {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("groupMsg", "readwrite").objectStore("groupMsg");
obs.add(data);
db.close();
};
}
function storeSuperNodeMsg(data) {
var idb = indexedDB.open("FLO_Chat", 2);
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onupgradeneeded = (event) => {
var objectStore = event.target.result.createObjectStore("superNodeMsg", {
keyPath: 'id'
});
objectStore.createIndex('from', 'from', {
unique: false
});
objectStore.createIndex('to', 'to', {
unique: false
});
objectStore.createIndex('data', 'data', {
unique: false
});
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("superNodeMsg", "readwrite").objectStore("superNodeMsg");
var parsedData = JSON.parse(data);
var id = '' + parsedData.from + '_' + parsedData.to + '_' + parsedData.time;
obs.add({
id: id,
from: parsedData.from,
to: parsedData.to,
data: data
});
db.close();
};
}
function displayContacts() {
console.log('displayContacts');
var listElement = document.getElementById('contact-display');
for (floID in contacts) {
var createLi = document.createElement('div');
createLi.setAttribute("name", floID);
createLi.setAttribute("onClick", 'changeReceiver(this)');
createLi.setAttribute("class", "row sideBar-body");
createLi.innerHTML = `<div class="col-sm-11 col-xs-11 sideBar-main">
<div class="row">
<div class="col-sm-12 col-xs-12 sideBar-name">
<span class="name-meta">${contacts[floID].name}</span><br/>
<span class="time-meta">@${floID}</span>
</div>
</div>
</div>`
listElement.appendChild(createLi);
}
for (floID in groups) {
var createLi = document.createElement('div');
createLi.setAttribute("name", floID);
createLi.setAttribute("onClick", 'changeReceiver(this)');
createLi.setAttribute("class", "row sideBar-body");
createLi.innerHTML = `<div class="col-sm-11 col-xs-11 sideBar-main">
<div class="row">
<div class="col-sm-12 col-xs-12 sideBar-name">
<span class="name-meta">${groups[floID].name}</span><br/>
<span class="time-meta">#${floID}</span>
</div>
</div>
</div>`
listElement.appendChild(createLi);
}
}
function initselfWebSocket() {
var selfwebsocket = new WebSocket("ws://" + location.host + "/ws");
selfwebsocket.onopen = (evt) => {
console.log("CONNECTED");
var pass = prompt("Enter server password :")
selfwebsocket.send("$" + pass);
};
selfwebsocket.onclose = (evt) => {
console.log("DISCONNECTED");
};
selfwebsocket.onmessage = (evt) => {
console.log(evt.data);
try {
var data = JSON.parse(evt.data);
if (data.to == selfID) {
console.log('Incoming data')
processIncomingData(data);
} else if (modSuperNode) {
if (data.pingAway !== undefined)
sendStoredSuperNodeMsgs(data.pingAway)
else {
kBucketObj.determineClosestSupernode(data.to).then(result => {
console.log(result)
if (result[0].floID == selfID)
storeSuperNodeMsg(evt.data);
}).catch(e => {
console.log(e.message);
});
}
}
} catch (err) {
if (evt.data[0] == '$')
alert(evt.data);
else
console.log(err);
}
};
selfwebsocket.onerror = (evt) => {
console.log(evt);
};
}
function processIncomingData(data) {
if (data.directMsg !== undefined) {
var msg = floOpt.decryptData(data.directMsg.msgCipher.secret, data.directMsg.msgCipher.pubVal, privKey)
if (!floOpt.verifyData(msg, data.directMsg.sign, contacts[data.from].pubKey))
return
var msgInfo = {
time: Date.now(),
floID: data.from,
text: msg,
type: "R"
}
createMsgElement(msgInfo);
storeGroupMsg(msgInfo);
} else if (data.groupMsg !== undefined && data.groupMsg.group in groups) {
if (!(groups[data.groupMsg.group].members.includes(data.from)))
return
var msg = floOpt.decryptData(data.groupMsg.msgCipher.secret, data.groupMsg.msgCipher.pubVal, groups[data.groupMsg.group].privKey);
if (!floOpt.verifyData(msg, data.groupMsg.sign, contacts[data.from].pubKey))
return
var msgInfo = {
time: Date.now(),
groupID: data.groupMsg.group,
sender: data.from,
text: msg,
type: "R"
}
createMsgElement(msgInfo);
storeGroupMsg(msgInfo);
} else if (data.newGroup !== undefined) {
var groupInfoStr = floOpt.decryptData(data.newGroup.groupInfo.secret, data.newGroup.groupInfo.pubVal, privKey)
var groupInfo = JSON.parse(groupInfoStr);
if (floOpt.verifyData(groupInfoStr, data.newGroup.sign, contacts[groupInfo.creator].pubKey)) {
groups[groupInfo.floID] = groupInfo;
searchIndex.add(groupInfo.floID, groupInfo.name + ' #' + groupInfo.floID);
storeGroup(groupInfoStr, groupInfo.floID);
createGroupDisplay(groupInfo);
}
} else if (data.deleteGroup !== undefined && data.deleteGroup.group in groups) {
if (data.from != groups[data.deleteGroup.group].creator && !groups[data.deleteGroup.group].admins.includes(data.from))
return
if (floOpt.verifyData('deleteGroup:' + data.deleteGroup.group, data.deleteGroup.sign, contacts[data.from].pubKey)) {
delete groups[data.deleteGroup.group];
searchIndex.remove(data.deleteGroup.group);
deleteGroupFromIDB(data.deleteGroup.group);
}
} else if (data.addGroupMembers !== undefined && data.addGroupMembers.group in groups) {
if (data.from != groups[data.addGroupMembers.group].creator && !groups[data.addGroupMembers.group].admins.includes(data.from))
return
if (floOpt.verifyData('addGroupMembers:' + data.addGroupMembers.group + data.addGroupMembers.members.join('|'), data.addGroupMembers.sign, contacts[data.from].pubKey)) {
groups[data.addGroupMembers.group].members = groups[data.addGroupMembers.group].members.concat(data.addGroupMembers.members);
var groupInfoStr = JSON.stringify(groups[data.addGroupMembers.group]);
storeGroup(groupInfoStr, data.addGroupMembers.group);
}
} else if (data.rmGroupMembers !== undefined && data.rmGroupMembers.group in groups) {
if (data.from != groups[data.rmGroupMembers.group].creator && !groups[data.rmGroupMembers.group].admins.includes(data.from))
return
if (floOpt.verifyData('rmGroupMembers:' + data.rmGroupMembers.group + data.rmGroupMembers.members.join('|'), data.rmGroupMembers.sign, contacts[data.from].pubKey)) {
groups[data.rmGroupMembers.group].members = groups[data.rmGroupMembers.group].members.filter(x => !data.rmGroupMembers.members.includes(x)); //remove member from group
var groupInfoStr = JSON.stringify(groups[data.rmGroupMembers.group]);
storeGroup(groupInfoStr, data.rmGroupMembers.group);
}
} else if (data.addGroupAdmins !== undefined && data.addGroupAdmins.group in groups) {
if (data.from != groups[data.addGroupAdmins.group].creator)
return
if (floOpt.verifyData('addGroupAdmins:' + data.addGroupAdmins.group + data.addGroupAdmins.admins.join('|'), data.addGroupAdmins.sign, contacts[data.from].pubKey)) {
groups[data.addGroupAdmins.group].admins = groups[data.addGroupAdmins.group].admins.concat(data.addGroupAdmins.admins);
var groupInfoStr = JSON.stringify(groups[data.addGroupAdmins.group]);
storeGroup(groupInfoStr, data.addGroupAdmins.group);
}
} else if (data.rmGroupAdmins !== undefined && data.rmGroupAdmins.group in groups) {
if (data.from != groups[data.rmGroupAdmins.group].creator)
return
if (floOpt.verifyData('rmGroupAdmins:' + data.rmGroupAdmins.group + data.rmGroupAdmins.admins.join('|'), data.rmGroupAdmins.sign, contacts[data.from].pubKey)) {
groups[data.rmGroupAdmins.group].admins = groups[data.rmGroupAdmins.group].admins.filter(x => !data.rmGroupAdmins.admins.includes(x)); //remove member from group
var groupInfoStr = JSON.stringify(groups[data.rmGroupAdmins.group]);
storeGroup(groupInfoStr, data.rmGroupAdmins.group);
}
}
}
function createMsgElement(msgInfo) {
try {
const type = {
S: 'sender',
R: 'receiver'
};
if (!msgInfo.groupID) {
var msgEl = document.getElementById(msgInfo.floID);
var msghd = '';
} else {
var msgEl = document.getElementById(msgInfo.groupID);
var msghd = `<b>${msgInfo.sender}</b>`;
}
if (!msgEl)
return;
var msgdiv = document.createElement('div');
msgdiv.setAttribute("class", "row message-body");
msgdiv.innerHTML = `<div class="col-sm-12 message-main-${type[msgInfo.type]}">
<div class="${type[msgInfo.type]}">
<span class="message-text">
${msghd}<br/>${msgInfo.text}
</span>
<span class="message-time pull-right">
${getTime(msgInfo.time)}
</span>
</div>
</div>`;
msgEl.appendChild(msgdiv);
} catch (e) {
console.log(e);
}
}
function pingSuperNodeForAwayMessages() {
kBucketObj.determineClosestSupernode(selfID).then(result => {
var selfSuperNodeWS = new WebSocket("ws://" + contacts[result[0].floID].onionAddr + "/ws");
selfSuperNodeWS.onopen = (evt) => {
var data = JSON.stringify({
pingAway: selfID
});
selfSuperNodeWS.send(data)
console.log('Pinged selfSupernode for new messages')
};
selfSuperNodeWS.onerror = (ev) => {
console.log('Unable to ping superNode for new messages');
};
selfSuperNodeWS.onclose = (ev) => {
console.log('Connection with selfSupernode is closed')
};
}).catch(err => {
console.log(err.message);
});
}
function checkStatusInterval() {
try {
if (receiverWebSocket !== undefined && receiverWebSocket.readyState !== WebSocket.OPEN) {
receiverWebSocket.close()
receiverWebSocket = new WebSocket("ws://" + contacts[receiverID].onionAddr + "/ws");
receiverWebSocket.onopen = (evt) => {
receiverWebSocket.send('#')
};
receiverWebSocket.onerror = (ev) => {
receiverStatus(false);
};
receiverWebSocket.onclose = (ev) => {
receiverStatus(false);
};
receiverWebSocket.onmessage = (evt) => {
console.log(evt.data);
if (evt.data[0] == '#') {
if (evt.data[1] == '+')
receiverStatus(true);
else if (evt.data[1] == '-')
receiverStatus(false);
}
}
}
} catch (e) {
console.log(e);
}
}
function changeReceiver(param) {
if (receiverID !== undefined)
document.getElementById(receiverID).style.display = 'none';
//console.log(param.getAttribute("name"));
receiverID = param.getAttribute("name");
document.getElementById('recipient-floID').innerHTML = receiverID;
receiverStatus(false)
document.getElementById(receiverID).style.display = 'block';
document.getElementById("groupOptions").style.display = 'none';
if (receiverID in contacts) {
msgType = 'direct';
try {
if (receiverWebSocket !== undefined && receiverWebSocket.readyState === WebSocket.OPEN)
receiverWebSocket.close()
receiverWebSocket = new WebSocket("ws://" + contacts[receiverID].onionAddr + "/ws");
receiverWebSocket.onopen = (ev) => {
receiverWebSocket.send('#');
};
receiverWebSocket.onerror = (ev) => {
receiverStatus(false);
};
receiverWebSocket.onclose = (ev) => {
receiverStatus(false);
};
receiverWebSocket.onmessage = (evt) => {
console.log(evt.data);
if (evt.data[0] == '#') {
if (evt.data[1] == '+')
receiverStatus(true);
else if (evt.data[1] == '-')
receiverStatus(false);
}
}
} catch (e) {
console.log(e);
}
} else if (receiverID in groups) {
msgType = 'group';
if (receiverWebSocket !== undefined && receiverWebSocket.readyState === WebSocket.OPEN)
receiverWebSocket.close()
receiverWebSocket = undefined;
if (selfID == groups[receiverID].creator) {
var grpOpt = document.getElementById("groupOptions");
grpOpt.style.display = 'block';
var optList = grpOpt.querySelectorAll('li');
for (var i = 0; i < optList.length; i++)
optList[i].style.display = 'block';
} else if (groups[receiverID].admins.includes(selfID)) {
var grpOpt = document.getElementById("groupOptions");
grpOpt.style.display = 'block';
var optList = grpOpt.querySelectorAll('li');
for (var i = 0; i < 2; i++)
optList[i].style.display = 'block';
for (var i = 2; i < optList.length; i++)
optList[i].style.display = 'none';
}
}
}
function receiverStatus(status) {
if (status)
document.getElementById('recipient-status').style.color = "#4CC94C";
else
document.getElementById('recipient-status').style.color = "#CD5C5C";
recStat = status;
}
function getTime(time) {
var t = new Date(time);
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
var fn = (n) => {
if (n < 10)
return '0' + n;
else
return n;
};
var tmp = `${months[t.getMonth()]} ${fn(t.getDate())} ${t.getFullYear()} ${fn(t.getHours())}:${fn(t.getMinutes())}`;
return tmp;
}
function sendMsg() {
if (receiverID === undefined) {
alert("Select a contact and send message");
return;
}
var inp = document.getElementById('sendMsgInput')
var msg = inp.value;
inp.value = "";
console.log(msg);
var time = Date.now();
var sign = floOpt.signData(msg, privKey);
if (msgType === 'direct')
sendDirectMsg(msg, time, sign);
else if (msgType === 'group')
sendGroupMsg(msg, time, sign);
}
function sendDirectMsg(msg, time, sign) {
var data = JSON.stringify({
from: selfID,
to: receiverID,
directMsg: {
time: time,
msgCipher: floOpt.encryptData(msg, contacts[receiverID].pubKey),
sign: sign
}
});
if (recStat)
receiverWebSocket.send(data);
else
sendDataToSuperNode(receiverID, data);
var msgInfo = {
time: time,
floID: receiverID,
text: msg,
type: "S"
}
createMsgElement(msgInfo);
storeMsg(msgInfo);
}
function sendGroupMsg(msg, time, sign) {
var data = {
from: selfID,
groupMsg: {
group: receiverID,
time: time,
msgCipher: floOpt.encryptData(msg, groups[receiverID].pubKey),
sign: sign
}
};
console.log(data);
groups[receiverID].members.forEach(floID => {
if (floID == selfID) //dont send to self
return;
data.to = floID;
sendData(floID, JSON.stringify(data));
});
var msgInfo = {
time: time,
sender: selfID,
groupID: receiverID,
text: msg,
type: "S"
}
createMsgElement(msgInfo);
storeGroupMsg(msgInfo);
}
function sendStoredSuperNodeMsgs(floID) {
var receiverWS = new WebSocket("ws://" + contacts[floID].onionAddr + "/ws");
receiverWS.onopen = (ev) => {
var idb = indexedDB.open("FLO_Chat", 2);
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onupgradeneeded = (event) => {
var objectStore = event.target.result.createObjectStore("superNodeMsg", {
keyPath: 'id'
});
objectStore.createIndex('from', 'from', {
unique: false
});
objectStore.createIndex('to', 'to', {
unique: false
});
objectStore.createIndex('data', 'data', {
unique: false
});
};
idb.onsuccess = (event) => {
var db = event.target.result;
var obs = db.transaction("superNodeMsg", "readwrite").objectStore("superNodeMsg");
obs.openCursor().onsuccess = (event) => {
var cursor = event.target.result;
if (cursor) {
if (cursor.value.to == floID) {
receiverWS.send(cursor.value.data);
cursor.delete();
}
cursor.continue();
} else {
console.log('Sent All messages to ' + floID)
}
}
db.close();
};
};
receiverWS.onerror = (ev) => {
console.log('Connection Error to ' + floID)
};
receiverWS.onclose = (ev) => {
console.log('Disconnected from ' + floID)
};
}
async function sendData(floID, data) {
try {
var recipientWS = new WebSocket("ws://" + contacts[floID].onionAddr + "/ws");
recipientWS.onopen = (ev) => {
recipientWS.send('#');
};
recipientWS.onerror = (ev) => {
sendDataToSuperNode(floID, data);
};
recipientWS.onclose = (ev) => {
console.log("Closed")
};
recipientWS.onmessage = (evt) => {
console.log(evt.data);
if (evt.data[0] == '#') {
if (evt.data[1] == '+')
recipientWS.send(data);
else if (evt.data[1] == '-')
sendDataToSuperNode(floID, data);
}
}
} catch (e) {
console.log(e);
}
}
function sendDataToSuperNode(floID, data) {
kBucketObj.determineClosestSupernode(floID).then(result => {
var superNodeWS = new WebSocket("ws://" + contacts[result[0].floID].onionAddr + "/ws");
superNodeWS.onopen = (ev) => {
console.log(`Connected to ${floID}'s SuperNode!`);
superNodeWS.send(data);
};
superNodeWS.onerror = (ev) => {
console.log(`${floID}'s SuperNode is offline!`);
};
superNodeWS.onclose = (ev) => {
console.log(`Disconnected from ${floID}'s SuperNode!`);
};
}).catch(e => {
console.log(e.message);
});
}
function createGroupDisplay(groupInfo) {
var createLi = document.createElement('div');
createLi.setAttribute("name", groupInfo.floID);
createLi.setAttribute("onClick", 'changeReceiver(this)');
createLi.setAttribute("class", "row sideBar-body");
createLi.innerHTML = `<div class="col-sm-11 col-xs-11 sideBar-main">
<div class="row">
<div class="col-sm-12 col-xs-12 sideBar-name">
<span class="name-meta">${groupInfo.name}</span><br/>
<span class="time-meta">#${groupInfo.floID}</span>
</div>
</div>
</div>`;
document.getElementById('contact-display').appendChild(createLi);
var createEl = document.createElement('div');
createEl.setAttribute("id", groupInfo.floID);
createEl.setAttribute("class", "message-inner");
createEl.style.display = 'none';
document.getElementById("conversation").appendChild(createEl);
}
function storeGroup(groupInfoStr, groupID) {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
console.log(groupID, groupInfoStr);
var obs = db.transaction('groups', "readwrite").objectStore('groups');
obs.put({
groupID: groupID,
groupInfo: groupInfoStr
});
db.close();
};
}
function deleteGroupFromIDB(groupID) {
var idb = indexedDB.open("FLO_Chat");
idb.onerror = (event) => {
console.log("Error in opening IndexedDB!");
};
idb.onsuccess = (event) => {
var db = event.target.result;
console.log('Delete Group:', groupID);
var obs = db.transaction('groups', "readwrite").objectStore('groups');
obs.delete(groupID);
db.close();
};
}
function createGroup() {
customCheckList(Object.keys(contacts), [selfID], 'Create Group', 'success').then(result => {
var grpInfo = floOpt.genNewIDpair();
grpInfo.name = result.grpName;
grpInfo.members = result.members;
grpInfo.members.push(selfID)
grpInfo.creator = selfID;
grpInfo.admins = [];
var grpInfoStr = JSON.stringify(grpInfo);
console.log(grpInfoStr);
var data = {
from: selfID,
newGroup: {
sign: floOpt.signData(grpInfoStr, privKey)
}
}
grpInfo.members.forEach(floID => {
data.to = floID;
data.newGroup.groupInfo = floOpt.encryptData(grpInfoStr, contacts[floID].pubKey),
sendData(floID, JSON.stringify(data));
});
}).catch(error => {
console.log(error);
})
}
function deleteGroup() {
var flag = confirm("Are you sure you want to delete this group?");
if (flag) {
var data = {
from: selfID,
deleteGroup: {
group: receiverID,
sign: floOpt.signData('deleteGroup:' + receiverID, privKey)
}
};
groups[receiverID].members.forEach(floID => {
data.to = floID;
sendData(floID, JSON.stringify(data));
});
}
}
function addGroupMembers() {
customCheckList(Object.keys(contacts), groups[receiverID].members, 'Add Members', 'success').then(result => {
var newMembers = result.members;
var data1 = {
from: selfID,
addGroupMembers: {
group: receiverID,
members: newMembers,
sign: floOpt.signData('addGroupMembers:' + receiverID + newMembers.join('|'), privKey)
}
}
groups[receiverID].members.forEach(floID => {
if (floID == selfID) //dont send to self
return;
data1.to = floID;
sendData(floID, JSON.stringify(data1));
});
groups[receiverID].members = groups[receiverID].members.concat(newMembers);
var grpInfoStr = JSON.stringify(groups[receiverID]);
console.log(grpInfoStr);
var data2 = {
from: selfID,
newGroup: {
sign: floOpt.signData(grpInfoStr, privKey)
}
}
newMembers.forEach(floID => {
data2.to = floID;
data2.newGroup.groupInfo = floOpt.encryptData(grpInfoStr, contacts[floID].pubKey),
sendData(floID, JSON.stringify(data2));
});
storeGroup(grpInfoStr, receiverID);
}).catch(error => {
console.log(error);
})
}
function rmGroupMembers() {
customCheckList(groups[receiverID].members, [], 'Remove Members', 'danger').then(result => {
var rmMembers = result.members;
var data1 = {
from: selfID,
rmGroupMembers: {
group: receiverID,
members: rmMembers,
sign: floOpt.signData('rmGroupMembers:' + receiverID + rmMembers.join('|'), privKey)
}
}
groups[receiverID].members = groups[receiverID].members.filter(x => !rmMembers.includes(x)); //remove member from group
storeGroup(JSON.stringify(groups[receiverID]), receiverID);
groups[receiverID].members.forEach(floID => {
if (floID == selfID)
return;
data1.to = floID;
sendData(floID, JSON.stringify(data1));
});
var data2 = {
from: selfID,
deleteGroup: {
group: receiverID,
sign: floOpt.signData('deleteGroup:' + receiverID, privKey)
}
};
rmMembers.forEach(floID => {
data2.to = floID;
sendData(floID, JSON.stringify(data2));
});
}).catch(error => {
console.log(error);
})
}
function addGroupAdmins() {
customCheckList(groups[receiverID].members, groups[receiverID].admins, 'Add Admins', 'success').then(result => {
var newAdmins = result.members;
var data = {
from: selfID,
addGroupAdmins: {
group: receiverID,
admins: newAdmins,
sign: floOpt.signData('addGroupAdmins:' + receiverID + newAdmins.join('|'), privKey)
}
}
groups[receiverID].members.forEach(floID => {
if (floID == selfID) //dont send to self
return;
data.to = floID;
sendData(floID, JSON.stringify(data));
});
groups[receiverID].admins = groups[receiverID].admins.concat(newAdmins);
var grpInfoStr = JSON.stringify(groups[receiverID]);
storeGroup(grpInfoStr, receiverID);
}).catch(error => {
console.log(error);
})
}
function rmGroupAdmins() {
customCheckList(groups[receiverID].admins, [], 'Remove Admins', 'danger').then(result => {
var rmAdmins = result.members;
var data = {
from: selfID,
rmGroupAdmins: {
group: receiverID,
admins: rmAdmins,
sign: floOpt.signData('rmGroupAdmins:' + receiverID + rmAdmins.join('|'), privKey)
}
}
groups[receiverID].members.forEach(floID => {
if (floID == selfID) //dont send to self
return;
data.to = floID;
sendData(floID, JSON.stringify(data));
});
groups[receiverID].admins = groups[receiverID].admins.filter(x => !rmAdmins.includes(x)); //remove admins
var grpInfoStr = JSON.stringify(groups[receiverID]);
storeGroup(grpInfoStr, receiverID);
}).catch(error => {
console.log(error);
})
}
function searchContact() {
try {
var searchKey = this.value;
if (!searchKey)
var searchResults = Object.keys(contacts).concat(Object.keys(groups));
else
var searchResults = searchIndex.search(searchKey);
var contactList = document.getElementById('contact-display').children;
for (var i = 0; i < contactList.length; i++) {
if (searchResults.includes(contactList[i].getAttribute("name")))
contactList[i].style.display = 'block';
else
contactList[i].style.display = 'none';
};
} catch (e) {
console.log(e);
}
}
function customCheckList(userList, ignoreList, okBtnVal = "Ok", okBtnType = "success") {
var dialog = document.getElementById('overlay');
dialog.style.display = "block";
var okButton = dialog.querySelector('button.ok');
var cancelButton = dialog.querySelector('button.cancel');
okButton.setAttribute("class", `ok btn btn-${okBtnType}`);
okButton.innerHTML = okBtnVal;
var grpNameInput = dialog.querySelector('input.grpName')
grpNameInput.style.display = (okBtnVal === "Create Group" ? "block" : "none");
grpNameInput.value = '';
var userChecklist = document.getElementById('userChecklist');
for (var i = 0; i < userList.length; i++) {
if (ignoreList.includes(userList[i]))
continue;
var listEl = document.createElement('label');
listEl.setAttribute('class', "btn btn-default listLabel");
listEl.setAttribute('name', userList[i]);
listEl.innerHTML = `
<span>${contacts[userList[i]].name}<br/>
<sub>@${userList[i]}</sub>
</span>
<input type="checkbox" class="badgebox" value="${userList[i]}">
<span class="badge">&check;</span>`;
userChecklist.appendChild(listEl);
}
return new Promise((resolve, reject) => {
dialog.addEventListener('click', function handleButtonClicks(e) {
if (e.target.tagName !== 'BUTTON') {
return;
}
dialog.removeEventListener('click', handleButtonClicks);
dialog.style.display = 'none';
if (e.target === okButton) {
var selectedList = [];
var checklist = dialog.querySelectorAll('input.badgebox');
for (var i = 0; i < checklist.length; i++)
if (checklist[i].checked)
selectedList.push(checklist[i].value);
if (selectedList.length == 0)
reject('User Didnt select Any Users!');
else
resolve({
grpName: grpNameInput.value,
members: selectedList
});
} else if (e.target === cancelButton) {
reject('User cancelled!');
} else {
reject('Some other button was clicked!');
}
});
});
}
function searchChecklist() {
try {
var searchKey = this.value;
if (!searchKey)
var searchResults = Object.keys(contacts);
else
var searchResults = searchIndex.search(searchKey);
var checklist = document.getElementById('userChecklist').children;
for (var i = 0; i < checklist.length; i++) {
if (searchResults.includes(checklist[i].getAttribute("name")))
checklist[i].style.display = 'block';
else
checklist[i].style.display = 'none';
};
} catch (e) {
console.log(e);
}
}