1. The encoding scheme was convoluted. We upgraded to TextEncoder and TextDecoder. 2. In case normal decoding fails, it will revert to old decoding.
1136 lines
44 KiB
JavaScript
1136 lines
44 KiB
JavaScript
(function (EXPORTS) { //floCloudAPI v2.4.5a
|
|
/* FLO Cloud operations to send/request application data*/
|
|
'use strict';
|
|
const floCloudAPI = EXPORTS;
|
|
|
|
const DEFAULT = {
|
|
blockchainPrefix: 0x23, //Prefix version for FLO blockchain
|
|
SNStorageID: floGlobals.SNStorageID || "FNaN9McoBAEFUjkRmNQRYLmBF8SpS7Tgfk",
|
|
adminID: floGlobals.adminID,
|
|
application: floGlobals.application,
|
|
SNStorageName: "SuperNodeStorage",
|
|
callback: (d, e) => console.debug(d, e)
|
|
};
|
|
|
|
var user_id, user_public, user_private, aes_key;
|
|
|
|
function user(id, priv) {
|
|
if (!priv || !id)
|
|
return user.clear();
|
|
let pub = floCrypto.getPubKeyHex(priv);
|
|
if (!pub || !floCrypto.verifyPubKey(pub, id))
|
|
return user.clear();
|
|
let n = floCrypto.randInt(12, 20);
|
|
aes_key = floCrypto.randString(n);
|
|
user_private = Crypto.AES.encrypt(priv, aes_key);
|
|
user_public = pub;
|
|
user_id = id;
|
|
return user_id;
|
|
}
|
|
|
|
Object.defineProperties(user, {
|
|
id: {
|
|
get: () => {
|
|
if (!user_id)
|
|
throw "User not set";
|
|
return user_id;
|
|
}
|
|
},
|
|
public: {
|
|
get: () => {
|
|
if (!user_public)
|
|
throw "User not set";
|
|
return user_public;
|
|
}
|
|
},
|
|
sign: {
|
|
value: msg => {
|
|
if (!user_private)
|
|
throw "User not set";
|
|
return floCrypto.signData(msg, Crypto.AES.decrypt(user_private, aes_key));
|
|
}
|
|
},
|
|
clear: {
|
|
value: () => user_id = user_public = user_private = aes_key = undefined
|
|
}
|
|
})
|
|
|
|
Object.defineProperties(floCloudAPI, {
|
|
SNStorageID: {
|
|
get: () => DEFAULT.SNStorageID
|
|
},
|
|
SNStorageName: {
|
|
get: () => DEFAULT.SNStorageName
|
|
},
|
|
adminID: {
|
|
get: () => DEFAULT.adminID
|
|
},
|
|
application: {
|
|
get: () => DEFAULT.application
|
|
},
|
|
user: {
|
|
get: () => user
|
|
}
|
|
});
|
|
|
|
var appObjects, generalData, lastVC;
|
|
Object.defineProperties(floGlobals, {
|
|
appObjects: {
|
|
get: () => appObjects,
|
|
set: obj => appObjects = obj
|
|
},
|
|
generalData: {
|
|
get: () => generalData,
|
|
set: data => generalData = data
|
|
},
|
|
generalDataset: {
|
|
value: (type, options = {}) => generalData[filterKey(type, options)]
|
|
},
|
|
lastVC: {
|
|
get: () => lastVC,
|
|
set: vc => lastVC = vc
|
|
}
|
|
});
|
|
|
|
var supernodes = {}; //each supnernode must be stored as floID : {uri:<uri>,pubKey:<publicKey>}
|
|
Object.defineProperty(floCloudAPI, 'nodes', {
|
|
get: () => JSON.parse(JSON.stringify(supernodes))
|
|
});
|
|
|
|
var kBucket;
|
|
const K_Bucket = floCloudAPI.K_Bucket = function (masterID, nodeList) {
|
|
|
|
const decodeID = floID => {
|
|
let k = bitjs.Base58.decode(floID);
|
|
k.shift();
|
|
k.splice(-4, 4);
|
|
let decodedId = Crypto.util.bytesToHex(k);
|
|
let nodeIdBigInt = new BigInteger(decodedId, 16);
|
|
let nodeIdBytes = nodeIdBigInt.toByteArrayUnsigned();
|
|
let nodeIdNewInt8Array = new Uint8Array(nodeIdBytes);
|
|
return nodeIdNewInt8Array;
|
|
};
|
|
|
|
const _KB = new BuildKBucket({
|
|
localNodeId: decodeID(masterID)
|
|
});
|
|
nodeList.forEach(id => _KB.add({
|
|
id: decodeID(id),
|
|
floID: id
|
|
}));
|
|
|
|
const _CO = nodeList.map(id => [_KB.distance(_KB.localNodeId, decodeID(id)), id])
|
|
.sort((a, b) => a[0] - b[0])
|
|
.map(a => a[1]);
|
|
|
|
const self = this;
|
|
Object.defineProperty(self, 'tree', {
|
|
get: () => _KB
|
|
});
|
|
Object.defineProperty(self, 'list', {
|
|
get: () => Array.from(_CO)
|
|
});
|
|
|
|
self.isNode = floID => _CO.includes(floID);
|
|
self.innerNodes = function (id1, id2) {
|
|
if (!_CO.includes(id1) || !_CO.includes(id2))
|
|
throw Error('Given nodes are not supernode');
|
|
let iNodes = []
|
|
for (let i = _CO.indexOf(id1) + 1; _CO[i] != id2; i++) {
|
|
if (i < _CO.length)
|
|
iNodes.push(_CO[i])
|
|
else i = -1
|
|
}
|
|
return iNodes
|
|
}
|
|
self.outterNodes = function (id1, id2) {
|
|
if (!_CO.includes(id1) || !_CO.includes(id2))
|
|
throw Error('Given nodes are not supernode');
|
|
let oNodes = []
|
|
for (let i = _CO.indexOf(id2) + 1; _CO[i] != id1; i++) {
|
|
if (i < _CO.length)
|
|
oNodes.push(_CO[i])
|
|
else i = -1
|
|
}
|
|
return oNodes
|
|
}
|
|
self.prevNode = function (id, N = 1) {
|
|
let n = N || _CO.length;
|
|
if (!_CO.includes(id))
|
|
throw Error('Given node is not supernode');
|
|
let pNodes = []
|
|
for (let i = 0, j = _CO.indexOf(id) - 1; i < n; j--) {
|
|
if (j == _CO.indexOf(id))
|
|
break;
|
|
else if (j > -1)
|
|
pNodes[i++] = _CO[j]
|
|
else j = _CO.length
|
|
}
|
|
return (N == 1 ? pNodes[0] : pNodes)
|
|
}
|
|
self.nextNode = function (id, N = 1) {
|
|
let n = N || _CO.length;
|
|
if (!_CO.includes(id))
|
|
throw Error('Given node is not supernode');
|
|
if (!n) n = _CO.length;
|
|
let nNodes = []
|
|
for (let i = 0, j = _CO.indexOf(id) + 1; i < n; j++) {
|
|
if (j == _CO.indexOf(id))
|
|
break;
|
|
else if (j < _CO.length)
|
|
nNodes[i++] = _CO[j]
|
|
else j = -1
|
|
}
|
|
return (N == 1 ? nNodes[0] : nNodes)
|
|
}
|
|
self.closestNode = function (id, N = 1) {
|
|
let decodedId = decodeID(id);
|
|
let n = N || _CO.length;
|
|
let cNodes = _KB.closest(decodedId, n)
|
|
.map(k => k.floID)
|
|
return (N == 1 ? cNodes[0] : cNodes)
|
|
}
|
|
}
|
|
|
|
floCloudAPI.init = function startCloudProcess(nodes) {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
supernodes = nodes;
|
|
kBucket = new K_Bucket(DEFAULT.SNStorageID, Object.keys(supernodes));
|
|
resolve('Cloud init successful');
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
})
|
|
}
|
|
|
|
Object.defineProperty(floCloudAPI, 'kBucket', {
|
|
get: () => kBucket
|
|
});
|
|
|
|
const _inactive = new Set();
|
|
|
|
function ws_connect(snID) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!(snID in supernodes))
|
|
return reject(`${snID} is not a supernode`)
|
|
if (_inactive.has(snID))
|
|
return reject(`${snID} is not active`)
|
|
var wsConn = new WebSocket("wss://" + supernodes[snID].uri + "/");
|
|
wsConn.onopen = evt => resolve(wsConn);
|
|
wsConn.onerror = evt => {
|
|
_inactive.add(snID)
|
|
reject(`${snID} is unavailable`)
|
|
}
|
|
})
|
|
}
|
|
|
|
function ws_activeConnect(snID, reverse = false) {
|
|
return new Promise((resolve, reject) => {
|
|
if (_inactive.size === kBucket.list.length)
|
|
return reject('Cloud offline');
|
|
if (!(snID in supernodes))
|
|
snID = kBucket.closestNode(proxyID(snID));
|
|
ws_connect(snID)
|
|
.then(node => resolve(node))
|
|
.catch(error => {
|
|
if (reverse)
|
|
var nxtNode = kBucket.prevNode(snID);
|
|
else
|
|
var nxtNode = kBucket.nextNode(snID);
|
|
ws_activeConnect(nxtNode, reverse)
|
|
.then(node => resolve(node))
|
|
.catch(error => reject(error))
|
|
})
|
|
})
|
|
}
|
|
|
|
function fetch_API(snID, data) {
|
|
return new Promise((resolve, reject) => {
|
|
if (_inactive.has(snID))
|
|
return reject(`${snID} is not active`);
|
|
let fetcher, sn_url = "https://" + supernodes[snID].uri;
|
|
if (typeof data === "string")
|
|
fetcher = fetch(sn_url + "?" + data);
|
|
else if (typeof data === "object" && data.method === "POST")
|
|
fetcher = fetch(sn_url, data);
|
|
fetcher.then(response => {
|
|
if (response.ok || response.status === 400 || response.status === 500)
|
|
resolve(response);
|
|
else
|
|
reject(response);
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
function fetch_ActiveAPI(snID, data, reverse = false) {
|
|
return new Promise((resolve, reject) => {
|
|
if (_inactive.size === kBucket.list.length)
|
|
return reject('Cloud offline');
|
|
if (!(snID in supernodes))
|
|
snID = kBucket.closestNode(proxyID(snID));
|
|
fetch_API(snID, data)
|
|
.then(result => resolve(result))
|
|
.catch(error => {
|
|
_inactive.add(snID)
|
|
if (reverse)
|
|
var nxtNode = kBucket.prevNode(snID);
|
|
else
|
|
var nxtNode = kBucket.nextNode(snID);
|
|
fetch_ActiveAPI(nxtNode, data, reverse)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error));
|
|
})
|
|
})
|
|
}
|
|
|
|
function singleRequest(floID, data_obj, method = "POST") {
|
|
return new Promise((resolve, reject) => {
|
|
let data;
|
|
if (method === "POST")
|
|
data = {
|
|
method: "POST",
|
|
body: JSON.stringify(data_obj)
|
|
};
|
|
else
|
|
data = new URLSearchParams(JSON.parse(JSON.stringify(data_obj))).toString();
|
|
fetch_ActiveAPI(floID, data).then(response => {
|
|
if (response.ok)
|
|
response.json()
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
else response.text()
|
|
.then(result => reject(response.status + ": " + result)) //Error Message from Node
|
|
.catch(error => reject(error))
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
const _liveRequest = {};
|
|
|
|
function liveRequest(floID, request, callback) {
|
|
const filterData = typeof request.status !== 'undefined' ?
|
|
data => {
|
|
if (request.status)
|
|
return data;
|
|
else {
|
|
let filtered = {};
|
|
for (let i in data)
|
|
if (request.trackList.includes(i))
|
|
filtered[i] = data[i];
|
|
return filtered;
|
|
}
|
|
} :
|
|
data => {
|
|
data = objectifier(data);
|
|
let filtered = {},
|
|
proxy = proxyID(request.receiverID),
|
|
r = request;
|
|
for (let v in data) {
|
|
let d = data[v];
|
|
if ((!r.atVectorClock || r.atVectorClock == v) &&
|
|
(r.atVectorClock || !r.lowerVectorClock || r.lowerVectorClock <= v) &&
|
|
(r.atVectorClock || !r.upperVectorClock || r.upperVectorClock >= v) &&
|
|
(!r.afterTime || r.afterTime < d.log_time) &&
|
|
r.application == d.application &&
|
|
(proxy == d.receiverID || proxy == d.proxyID) &&
|
|
(!r.comment || r.comment == d.comment) &&
|
|
(!r.type || r.type == d.type) &&
|
|
(!r.senderID || r.senderID.includes(d.senderID)))
|
|
filtered[v] = data[v];
|
|
}
|
|
return filtered;
|
|
};
|
|
|
|
return new Promise((resolve, reject) => {
|
|
ws_activeConnect(floID).then(node => {
|
|
let randID = floCrypto.randString(5);
|
|
node.send(JSON.stringify(request));
|
|
node.onmessage = (evt) => {
|
|
let d = null,
|
|
e = null;
|
|
try {
|
|
d = filterData(JSON.parse(evt.data));
|
|
} catch (error) {
|
|
e = evt.data
|
|
} finally {
|
|
callback(d, e)
|
|
}
|
|
}
|
|
_liveRequest[randID] = node;
|
|
_liveRequest[randID].request = request;
|
|
resolve(randID);
|
|
}).catch(error => reject(error));
|
|
});
|
|
}
|
|
|
|
Object.defineProperty(floCloudAPI, 'liveRequest', {
|
|
get: () => _liveRequest
|
|
});
|
|
|
|
Object.defineProperty(floCloudAPI, 'inactive', {
|
|
get: () => _inactive
|
|
});
|
|
|
|
const util = floCloudAPI.util = {};
|
|
|
|
//Updating encoding/Decoding to modern standards
|
|
const encodeMessage = util.encodeMessage = function (message) {
|
|
const bytes = new TextEncoder().encode(JSON.stringify(message));
|
|
let bin = "";
|
|
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
return btoa(bin);
|
|
};
|
|
|
|
const decodeMessage = util.decodeMessage = function (message) {
|
|
try {
|
|
// try modern decode first
|
|
const bin = atob(message);
|
|
const bytes = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
return JSON.parse(new TextDecoder().decode(bytes));
|
|
} catch (e1) {
|
|
try {
|
|
// fallback to legacy decode
|
|
return JSON.parse(decodeURIComponent(escape(atob(message))));
|
|
} catch (e2) {
|
|
// final fallback: return raw string to avoid hard crash
|
|
return message;
|
|
}
|
|
}
|
|
};
|
|
|
|
const filterKey = util.filterKey = function (type, options = {}) {
|
|
return type + (options.comment ? ':' + options.comment : '') +
|
|
'|' + (options.group || options.receiverID || DEFAULT.adminID) +
|
|
'|' + (options.application || DEFAULT.application);
|
|
}
|
|
|
|
const proxyID = util.proxyID = function (address) {
|
|
if (!address)
|
|
return;
|
|
var bytes;
|
|
if (address.length == 33 || address.length == 34) { //legacy encoding
|
|
let decode = bitjs.Base58.decode(address);
|
|
bytes = decode.slice(0, decode.length - 4);
|
|
let checksum = decode.slice(decode.length - 4),
|
|
hash = Crypto.SHA256(Crypto.SHA256(bytes, {
|
|
asBytes: true
|
|
}), {
|
|
asBytes: true
|
|
});
|
|
hash[0] != checksum[0] || hash[1] != checksum[1] || hash[2] != checksum[2] || hash[3] != checksum[3] ?
|
|
bytes = undefined : bytes.shift();
|
|
} else if (address.length == 42 || address.length == 62) { //bech encoding
|
|
if (typeof coinjs !== 'function')
|
|
throw "library missing (lib_btc.js)";
|
|
let decode = coinjs.bech32_decode(address);
|
|
if (decode) {
|
|
bytes = decode.data;
|
|
bytes.shift();
|
|
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
|
|
if (address.length == 62) //for long bech, aggregate once more to get 160 bit
|
|
bytes = coinjs.bech32_convert(bytes, 5, 8, false);
|
|
}
|
|
} else if (address.length == 66) { //public key hex
|
|
bytes = ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(address), {
|
|
asBytes: true
|
|
}));
|
|
}
|
|
if (!bytes)
|
|
throw "Invalid address: " + address;
|
|
else {
|
|
bytes.unshift(DEFAULT.blockchainPrefix);
|
|
let hash = Crypto.SHA256(Crypto.SHA256(bytes, {
|
|
asBytes: true
|
|
}), {
|
|
asBytes: true
|
|
});
|
|
return bitjs.Base58.encode(bytes.concat(hash.slice(0, 4)));
|
|
}
|
|
}
|
|
|
|
const lastCommit = {};
|
|
Object.defineProperty(lastCommit, 'get', {
|
|
value: objName => JSON.parse(lastCommit[objName])
|
|
});
|
|
Object.defineProperty(lastCommit, 'set', {
|
|
value: objName => lastCommit[objName] = JSON.stringify(appObjects[objName])
|
|
});
|
|
|
|
function updateObject(objectName, dataSet) {
|
|
try {
|
|
console.log(dataSet)
|
|
let vcList = Object.keys(dataSet).sort();
|
|
for (let vc of vcList) {
|
|
if (vc < lastVC[objectName] || dataSet[vc].type !== objectName)
|
|
continue;
|
|
switch (dataSet[vc].comment) {
|
|
case "RESET":
|
|
if (dataSet[vc].message.reset)
|
|
appObjects[objectName] = dataSet[vc].message.reset;
|
|
break;
|
|
case "UPDATE":
|
|
if (dataSet[vc].message.diff)
|
|
appObjects[objectName] = diff.merge(appObjects[objectName], dataSet[vc].message.diff);
|
|
}
|
|
lastVC[objectName] = vc;
|
|
}
|
|
lastCommit.set(objectName);
|
|
compactIDB.writeData("appObjects", appObjects[objectName], objectName);
|
|
compactIDB.writeData("lastVC", lastVC[objectName], objectName);
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
}
|
|
|
|
function storeGeneral(fk, dataSet) {
|
|
try {
|
|
console.log(dataSet)
|
|
if (typeof generalData[fk] !== "object")
|
|
generalData[fk] = {}
|
|
for (let vc in dataSet) {
|
|
generalData[fk][vc] = dataSet[vc];
|
|
if (dataSet[vc].log_time > lastVC[fk])
|
|
lastVC[fk] = dataSet[vc].log_time;
|
|
}
|
|
compactIDB.writeData("lastVC", lastVC[fk], fk)
|
|
compactIDB.writeData("generalData", generalData[fk], fk)
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
}
|
|
|
|
function objectifier(data) {
|
|
if (!Array.isArray(data))
|
|
data = [data];
|
|
return Object.fromEntries(data.map(d => {
|
|
d.message = decodeMessage(d.message);
|
|
return [d.vectorClock, d];
|
|
}));
|
|
}
|
|
|
|
//set status as online for user_id
|
|
floCloudAPI.setStatus = function (options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
let callback = options.callback instanceof Function ? options.callback : DEFAULT.callback;
|
|
var request = {
|
|
floID: user.id,
|
|
application: options.application || DEFAULT.application,
|
|
time: Date.now(),
|
|
status: true,
|
|
pubKey: user.public
|
|
}
|
|
let hashcontent = ["time", "application", "floID"].map(d => request[d]).join("|");
|
|
request.sign = user.sign(hashcontent);
|
|
liveRequest(options.refID || DEFAULT.adminID, request, callback)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
//request status of floID(s) in trackList
|
|
floCloudAPI.requestStatus = function (trackList, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!Array.isArray(trackList))
|
|
trackList = [trackList];
|
|
let callback = options.callback instanceof Function ? options.callback : DEFAULT.callback;
|
|
let request = {
|
|
status: false,
|
|
application: options.application || DEFAULT.application,
|
|
trackList: trackList
|
|
}
|
|
liveRequest(options.refID || DEFAULT.adminID, request, callback)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
//send any message to supernode cloud storage
|
|
const sendApplicationData = floCloudAPI.sendApplicationData = function (message, type, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
var data = {
|
|
senderID: user.id,
|
|
receiverID: options.receiverID || DEFAULT.adminID,
|
|
pubKey: user.public,
|
|
message: encodeMessage(message),
|
|
time: Date.now(),
|
|
application: options.application || DEFAULT.application,
|
|
type: type,
|
|
comment: options.comment || ""
|
|
}
|
|
let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"]
|
|
.map(d => data[d]).join("|")
|
|
data.sign = user.sign(hashcontent);
|
|
singleRequest(data.receiverID, data)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
//request any data from supernode cloud
|
|
const _requestApplicationData = function (type, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
var request = {
|
|
receiverID: options.receiverID || DEFAULT.adminID,
|
|
senderID: options.senderID || undefined,
|
|
application: options.application || DEFAULT.application,
|
|
type: type,
|
|
comment: options.comment || undefined,
|
|
lowerVectorClock: options.lowerVectorClock || undefined,
|
|
upperVectorClock: options.upperVectorClock || undefined,
|
|
atVectorClock: options.atVectorClock || undefined,
|
|
afterTime: options.afterTime || undefined,
|
|
mostRecent: options.mostRecent || undefined,
|
|
}
|
|
|
|
if (options.callback instanceof Function) {
|
|
liveRequest(request.receiverID, request, options.callback)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
} else {
|
|
if (options.method === "POST")
|
|
request = {
|
|
time: Date.now(),
|
|
request
|
|
};
|
|
singleRequest(request.receiverID, request, options.method || "GET")
|
|
.then(data => resolve(data)).catch(error => reject(error))
|
|
}
|
|
})
|
|
}
|
|
|
|
floCloudAPI.requestApplicationData = function (type, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
let single_request_mode = !(options.callback instanceof Function);
|
|
_requestApplicationData(type, options).then(data => {
|
|
if (single_request_mode)
|
|
resolve(objectifier(data))
|
|
else resolve(data);
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
/*(NEEDS UPDATE)
|
|
//delete data from supernode cloud (received only)
|
|
floCloudAPI.deleteApplicationData = function(vectorClocks, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
var delreq = {
|
|
requestorID: user.id,
|
|
pubKey: user.public,
|
|
time: Date.now(),
|
|
delete: (Array.isArray(vectorClocks) ? vectorClocks : [vectorClocks]),
|
|
application: options.application || DEFAULT.application
|
|
}
|
|
let hashcontent = ["time", "application", "delete"]
|
|
.map(d => delreq[d]).join("|")
|
|
delreq.sign = user.sign(hashcontent)
|
|
singleRequest(delreq.requestorID, delreq).then(result => {
|
|
let success = [],
|
|
failed = [];
|
|
result.forEach(r => r.status === 'fulfilled' ?
|
|
success.push(r.value) : failed.push(r.reason));
|
|
resolve({
|
|
success,
|
|
failed
|
|
})
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
*/
|
|
//edit comment of data in supernode cloud (sender only)
|
|
floCloudAPI.editApplicationData = function (vectorClock, comment_edit, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
//request the data from cloud for resigning
|
|
let req_options = Object.assign({}, options);
|
|
req_options.atVectorClock = vectorClock;
|
|
_requestApplicationData(undefined, req_options).then(result => {
|
|
if (!result.length)
|
|
return reject("Data not found");
|
|
let data = result[0];
|
|
if (data.senderID !== user.id)
|
|
return reject("Only sender can edit comment");
|
|
data.comment = comment_edit;
|
|
let hashcontent = ["receiverID", "time", "application", "type", "message", "comment"]
|
|
.map(d => data[d]).join("|");
|
|
let re_sign = user.sign(hashcontent);
|
|
var request = {
|
|
receiverID: options.receiverID || DEFAULT.adminID,
|
|
requestorID: user.id,
|
|
pubKey: user.public,
|
|
time: Date.now(),
|
|
vectorClock: vectorClock,
|
|
edit: comment_edit,
|
|
re_sign: re_sign
|
|
}
|
|
let request_hash = ["time", "vectorClock", "edit", "re_sign"].map(d => request[d]).join("|");
|
|
request.sign = user.sign(request_hash);
|
|
singleRequest(request.receiverID, request)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
//tag data in supernode cloud (subAdmin access only)
|
|
floCloudAPI.tagApplicationData = function (vectorClock, tag, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!floGlobals.subAdmins.includes(user.id))
|
|
return reject("Only subAdmins can tag data")
|
|
var request = {
|
|
receiverID: options.receiverID || DEFAULT.adminID,
|
|
requestorID: user.id,
|
|
pubKey: user.public,
|
|
time: Date.now(),
|
|
vectorClock: vectorClock,
|
|
tag: tag,
|
|
}
|
|
let hashcontent = ["time", "vectorClock", 'tag'].map(d => request[d]).join("|");
|
|
request.sign = user.sign(hashcontent);
|
|
singleRequest(request.receiverID, request)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
//note data in supernode cloud (receiver only or subAdmin allowed if receiver is adminID)
|
|
floCloudAPI.noteApplicationData = function (vectorClock, note, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
var request = {
|
|
receiverID: options.receiverID || DEFAULT.adminID,
|
|
requestorID: user.id,
|
|
pubKey: user.public,
|
|
time: Date.now(),
|
|
vectorClock: vectorClock,
|
|
note: note,
|
|
}
|
|
let hashcontent = ["time", "vectorClock", 'note'].map(d => request[d]).join("|");
|
|
request.sign = user.sign(hashcontent);
|
|
singleRequest(request.receiverID, request)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
//send general data
|
|
floCloudAPI.sendGeneralData = function (message, type, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
if (options.encrypt) {
|
|
let encryptionKey = options.encrypt === true ?
|
|
floGlobals.settings.encryptionKey : options.encrypt
|
|
message = floCrypto.encryptData(JSON.stringify(message), encryptionKey)
|
|
}
|
|
sendApplicationData(message, type, options)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
//request general data
|
|
floCloudAPI.requestGeneralData = function (type, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
var fk = filterKey(type, options)
|
|
lastVC[fk] = parseInt(lastVC[fk]) || 0;
|
|
options.afterTime = options.afterTime || lastVC[fk];
|
|
if (options.callback instanceof Function) {
|
|
let new_options = Object.create(options)
|
|
new_options.callback = (d, e) => {
|
|
storeGeneral(fk, d);
|
|
options.callback(d, e)
|
|
}
|
|
_requestApplicationData(type, new_options)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
} else {
|
|
_requestApplicationData(type, options).then(dataSet => {
|
|
storeGeneral(fk, objectifier(dataSet))
|
|
resolve(dataSet)
|
|
}).catch(error => reject(error))
|
|
}
|
|
})
|
|
}
|
|
|
|
//request an object data from supernode cloud
|
|
floCloudAPI.requestObjectData = function (objectName, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
options.lowerVectorClock = options.lowerVectorClock || lastVC[objectName] + 1;
|
|
options.senderID = [false, null].includes(options.senderID) ? null :
|
|
options.senderID || floGlobals.subAdmins;
|
|
options.mostRecent = true;
|
|
options.comment = 'RESET';
|
|
let callback = null;
|
|
if (options.callback instanceof Function) {
|
|
let old_callback = options.callback;
|
|
callback = (d, e) => {
|
|
updateObject(objectName, d);
|
|
old_callback(d, e);
|
|
}
|
|
delete options.callback;
|
|
}
|
|
_requestApplicationData(objectName, options).then(dataSet => {
|
|
updateObject(objectName, objectifier(dataSet));
|
|
delete options.comment;
|
|
options.lowerVectorClock = lastVC[objectName] + 1;
|
|
delete options.mostRecent;
|
|
if (callback) {
|
|
let new_options = Object.create(options);
|
|
new_options.callback = callback;
|
|
_requestApplicationData(objectName, new_options)
|
|
.then(result => resolve(result))
|
|
.catch(error => reject(error))
|
|
} else {
|
|
_requestApplicationData(objectName, options).then(dataSet => {
|
|
updateObject(objectName, objectifier(dataSet))
|
|
resolve(appObjects[objectName])
|
|
}).catch(error => reject(error))
|
|
}
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
floCloudAPI.closeRequest = function (requestID) {
|
|
return new Promise((resolve, reject) => {
|
|
let conn = _liveRequest[requestID]
|
|
if (!conn)
|
|
return reject('Request not found')
|
|
conn.onclose = evt => {
|
|
delete _liveRequest[requestID];
|
|
resolve('Request connection closed')
|
|
}
|
|
conn.close()
|
|
})
|
|
}
|
|
|
|
//reset or initialize an object and send it to cloud
|
|
floCloudAPI.resetObjectData = function (objectName, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
let message = {
|
|
reset: appObjects[objectName]
|
|
}
|
|
options.comment = 'RESET';
|
|
sendApplicationData(message, objectName, options).then(result => {
|
|
lastCommit.set(objectName);
|
|
resolve(result)
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
//update the diff and send it to cloud
|
|
floCloudAPI.updateObjectData = function (objectName, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
let message = {
|
|
diff: diff.find(lastCommit.get(objectName), appObjects[
|
|
objectName])
|
|
}
|
|
options.comment = 'UPDATE';
|
|
sendApplicationData(message, objectName, options).then(result => {
|
|
lastCommit.set(objectName);
|
|
resolve(result)
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
//upload file
|
|
floCloudAPI.uploadFile = function (fileBlob, type, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!(fileBlob instanceof File) && !(fileBlob instanceof Blob))
|
|
return reject("file must be instance of File/Blob");
|
|
fileBlob.arrayBuffer().then(arraybuf => {
|
|
let file_data = { type: fileBlob.type, name: fileBlob.name };
|
|
file_data.content = Crypto.util.bytesToBase64(new Uint8Array(arraybuf));
|
|
if (options.encrypt) {
|
|
let encryptionKey = options.encrypt === true ?
|
|
floGlobals.settings.encryptionKey : options.encrypt
|
|
file_data = floCrypto.encryptData(JSON.stringify(file_data), encryptionKey)
|
|
}
|
|
sendApplicationData(file_data, type, options)
|
|
.then(({ vectorClock, receiverID, type, application }) => resolve({ vectorClock, receiverID, type, application }))
|
|
.catch(error => reject(error))
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
//download file
|
|
floCloudAPI.downloadFile = function (vectorClock, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
options.atVectorClock = vectorClock;
|
|
_requestApplicationData(options.type, options).then(result => {
|
|
if (!result.length)
|
|
return reject("File not found");
|
|
result = result[0];
|
|
try {
|
|
let file_data = decodeMessage(result.message);
|
|
//file is encrypted: decryption required
|
|
if (file_data instanceof Object && "secret" in file_data) {
|
|
if (!options.decrypt)
|
|
return reject("Data is encrypted");
|
|
let decryptionKey = (options.decrypt === true) ? Crypto.AES.decrypt(user_private, aes_key) : options.decrypt;
|
|
if (!Array.isArray(decryptionKey))
|
|
decryptionKey = [decryptionKey];
|
|
let flag = false;
|
|
for (let key of decryptionKey) {
|
|
try {
|
|
let tmp = floCrypto.decryptData(file_data, key);
|
|
file_data = JSON.parse(tmp);
|
|
flag = true;
|
|
break;
|
|
} catch (error) { }
|
|
}
|
|
if (!flag)
|
|
return reject("Unable to decrypt file: Invalid private key");
|
|
}
|
|
//reconstruct the file
|
|
let arraybuf = new Uint8Array(Crypto.util.base64ToBytes(file_data.content))
|
|
result.file = new File([arraybuf], file_data.name, { type: file_data.type });
|
|
resolve(result)
|
|
} catch (error) {
|
|
console.error(error);
|
|
reject("Data is not a file");
|
|
}
|
|
}).catch(error => reject(error))
|
|
})
|
|
}
|
|
|
|
/*
|
|
Functions:
|
|
findDiff(original, updatedObj) returns an object with the added, deleted and updated differences
|
|
mergeDiff(original, allDiff) returns a new object from original object merged with all differences (allDiff is returned object of findDiff)
|
|
*/
|
|
var diff = (function () {
|
|
const isDate = d => d instanceof Date;
|
|
const isEmpty = o => Object.keys(o).length === 0;
|
|
const isObject = o => o != null && typeof o === 'object';
|
|
const properObject = o => isObject(o) && !o.hasOwnProperty ? {
|
|
...o
|
|
} : o;
|
|
const getLargerArray = (l, r) => l.length > r.length ? l : r;
|
|
|
|
const preserve = (diff, left, right) => {
|
|
if (!isObject(diff)) return diff;
|
|
return Object.keys(diff).reduce((acc, key) => {
|
|
const leftArray = left[key];
|
|
const rightArray = right[key];
|
|
if (Array.isArray(leftArray) && Array.isArray(rightArray)) {
|
|
const array = [...getLargerArray(leftArray, rightArray)];
|
|
return {
|
|
...acc,
|
|
[key]: array.reduce((acc2, item, index) => {
|
|
if (diff[key].hasOwnProperty(index)) {
|
|
acc2[index] = preserve(diff[key][index], leftArray[index], rightArray[index]); // diff recurse and check for nested arrays
|
|
return acc2;
|
|
}
|
|
delete acc2[index]; // no diff aka empty
|
|
return acc2;
|
|
}, array)
|
|
};
|
|
}
|
|
return {
|
|
...acc,
|
|
[key]: diff[key]
|
|
};
|
|
}, {});
|
|
};
|
|
|
|
const updatedDiff = (lhs, rhs) => {
|
|
if (lhs === rhs) return {};
|
|
if (!isObject(lhs) || !isObject(rhs)) return rhs;
|
|
const l = properObject(lhs);
|
|
const r = properObject(rhs);
|
|
if (isDate(l) || isDate(r)) {
|
|
if (l.valueOf() == r.valueOf()) return {};
|
|
return r;
|
|
}
|
|
return Object.keys(r).reduce((acc, key) => {
|
|
if (l.hasOwnProperty(key)) {
|
|
const difference = updatedDiff(l[key], r[key]);
|
|
if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc;
|
|
return {
|
|
...acc,
|
|
[key]: difference
|
|
};
|
|
}
|
|
return acc;
|
|
}, {});
|
|
};
|
|
|
|
|
|
const diff = (lhs, rhs) => {
|
|
if (lhs === rhs) return {}; // equal return no diff
|
|
if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs
|
|
const l = properObject(lhs);
|
|
const r = properObject(rhs);
|
|
const deletedValues = Object.keys(l).reduce((acc, key) => {
|
|
return r.hasOwnProperty(key) ? acc : {
|
|
...acc,
|
|
[key]: null
|
|
};
|
|
}, {});
|
|
if (isDate(l) || isDate(r)) {
|
|
if (l.valueOf() == r.valueOf()) return {};
|
|
return r;
|
|
}
|
|
return Object.keys(r).reduce((acc, key) => {
|
|
if (!l.hasOwnProperty(key)) return {
|
|
...acc,
|
|
[key]: r[key]
|
|
}; // return added r key
|
|
const difference = diff(l[key], r[key]);
|
|
if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff
|
|
return {
|
|
...acc,
|
|
[key]: difference
|
|
}; // return updated key
|
|
}, deletedValues);
|
|
};
|
|
|
|
const addedDiff = (lhs, rhs) => {
|
|
if (lhs === rhs || !isObject(lhs) || !isObject(rhs)) return {};
|
|
const l = properObject(lhs);
|
|
const r = properObject(rhs);
|
|
return Object.keys(r).reduce((acc, key) => {
|
|
if (l.hasOwnProperty(key)) {
|
|
const difference = addedDiff(l[key], r[key]);
|
|
if (isObject(difference) && isEmpty(difference)) return acc;
|
|
return {
|
|
...acc,
|
|
[key]: difference
|
|
};
|
|
}
|
|
return {
|
|
...acc,
|
|
[key]: r[key]
|
|
};
|
|
}, {});
|
|
};
|
|
|
|
const arrayDiff = (lhs, rhs) => {
|
|
if (lhs === rhs) return {}; // equal return no diff
|
|
if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs
|
|
const l = properObject(lhs);
|
|
const r = properObject(rhs);
|
|
const deletedValues = Object.keys(l).reduce((acc, key) => {
|
|
return r.hasOwnProperty(key) ? acc : {
|
|
...acc,
|
|
[key]: null
|
|
};
|
|
}, {});
|
|
if (isDate(l) || isDate(r)) {
|
|
if (l.valueOf() == r.valueOf()) return {};
|
|
return r;
|
|
}
|
|
if (Array.isArray(r) && Array.isArray(l)) {
|
|
const deletedValues = l.reduce((acc, item, index) => {
|
|
return r.hasOwnProperty(index) ? acc.concat(item) : acc.concat(null);
|
|
}, []);
|
|
return r.reduce((acc, rightItem, index) => {
|
|
if (!deletedValues.hasOwnProperty(index)) {
|
|
return acc.concat(rightItem);
|
|
}
|
|
const leftItem = l[index];
|
|
const difference = diff(rightItem, leftItem);
|
|
if (isObject(difference) && isEmpty(difference) && !isDate(difference)) {
|
|
delete acc[index];
|
|
return acc; // return no diff
|
|
}
|
|
return acc.slice(0, index).concat(rightItem).concat(acc.slice(index + 1)); // return updated key
|
|
}, deletedValues);
|
|
}
|
|
|
|
return Object.keys(r).reduce((acc, key) => {
|
|
if (!l.hasOwnProperty(key)) return {
|
|
...acc,
|
|
[key]: r[key]
|
|
}; // return added r key
|
|
const difference = diff(l[key], r[key]);
|
|
if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff
|
|
return {
|
|
...acc,
|
|
[key]: difference
|
|
}; // return updated key
|
|
}, deletedValues);
|
|
};
|
|
|
|
const deletedDiff = (lhs, rhs) => {
|
|
if (lhs === rhs || !isObject(lhs) || !isObject(rhs)) return {};
|
|
const l = properObject(lhs);
|
|
const r = properObject(rhs);
|
|
return Object.keys(l).reduce((acc, key) => {
|
|
if (r.hasOwnProperty(key)) {
|
|
const difference = deletedDiff(l[key], r[key]);
|
|
if (isObject(difference) && isEmpty(difference)) return acc;
|
|
return {
|
|
...acc,
|
|
[key]: difference
|
|
};
|
|
}
|
|
return {
|
|
...acc,
|
|
[key]: null
|
|
};
|
|
}, {});
|
|
};
|
|
|
|
const mergeRecursive = (obj1, obj2, deleteMode = false) => {
|
|
for (var p in obj2) {
|
|
try {
|
|
if (obj2[p].constructor == Object)
|
|
obj1[p] = mergeRecursive(obj1[p], obj2[p], deleteMode);
|
|
// Property in destination object set; update its value.
|
|
else if (Array.isArray(obj2[p])) {
|
|
// obj1[p] = [];
|
|
if (obj2[p].length < 1)
|
|
obj1[p] = obj2[p];
|
|
else
|
|
obj1[p] = mergeRecursive(obj1[p], obj2[p], deleteMode);
|
|
} else
|
|
obj1[p] = deleteMode && obj2[p] === null ? undefined : obj2[p];
|
|
} catch (e) {
|
|
// Property in destination object not set; create it and set its value.
|
|
obj1[p] = deleteMode && obj2[p] === null ? undefined : obj2[p];
|
|
}
|
|
}
|
|
return obj1;
|
|
}
|
|
|
|
const cleanse = (obj) => {
|
|
Object.keys(obj).forEach(key => {
|
|
var value = obj[key];
|
|
if (typeof value === "object" && value !== null)
|
|
obj[key] = cleanse(value);
|
|
else if (typeof value === 'undefined')
|
|
delete obj[key]; // undefined, remove it
|
|
});
|
|
if (Array.isArray(obj))
|
|
obj = obj.filter(v => typeof v !== 'undefined');
|
|
return obj;
|
|
}
|
|
|
|
|
|
const findDiff = (lhs, rhs) => ({
|
|
added: addedDiff(lhs, rhs),
|
|
deleted: deletedDiff(lhs, rhs),
|
|
updated: updatedDiff(lhs, rhs),
|
|
});
|
|
|
|
/*obj is original object or array, diff is the output of findDiff */
|
|
const mergeDiff = (obj, diff) => {
|
|
if (Object.keys(diff.updated).length !== 0)
|
|
obj = mergeRecursive(obj, diff.updated)
|
|
if (Object.keys(diff.deleted).length !== 0) {
|
|
obj = mergeRecursive(obj, diff.deleted, true)
|
|
obj = cleanse(obj)
|
|
}
|
|
if (Object.keys(diff.added).length !== 0)
|
|
obj = mergeRecursive(obj, diff.added)
|
|
return obj
|
|
}
|
|
|
|
return {
|
|
find: findDiff,
|
|
merge: mergeDiff
|
|
}
|
|
})();
|
|
|
|
|
|
})('object' === typeof module ? module.exports : window.floCloudAPI = {});
|