Merge development into lint

This commit is contained in:
Mikunj 2019-01-17 12:57:18 +11:00
commit e08a63d078
14 changed files with 505 additions and 159 deletions

View File

@ -80,6 +80,8 @@ module.exports = {
removeSessionsByNumber,
removeAllSessions,
getSwarmNodesByPubkey,
getConversationCount,
saveConversation,
saveConversations,
@ -1039,6 +1041,18 @@ async function removeAllFromTable(table) {
// Conversations
async function getSwarmNodesByPubkey(pubkey) {
const row = await db.get('SELECT * FROM conversations WHERE id = $pubkey;', {
$pubkey: pubkey,
});
if (!row) {
return null;
}
return jsonToObject(row.json).swarmNodes;
}
async function getConversationCount() {
const row = await db.get('SELECT count(*) from conversations;');

View File

@ -1,6 +1,8 @@
{
"serverUrl": "http://localhost:8080",
"cdnUrl": "http://localhost",
"serverUrl": "random.snode",
"cdnUrl": "random.snode",
"messageServerPort": "8080",
"swarmServerPort": "8079",
"disableAutoUpdate": false,
"openDevTools": false,
"buildExpiration": 0,

View File

@ -181,6 +181,7 @@
return conversation;
}
window.LokiSnodeAPI.replenishSwarm(id);
try {
await window.Signal.Data.saveConversation(conversation.attributes, {
Conversation: Whisper.Conversation,

View File

@ -87,6 +87,7 @@
friendRequestStatus: FriendRequestStatusEnum.none,
unlockTimestamp: null, // Timestamp used for expiring friend requests.
sessionResetStatus: SessionResetEnum.none,
swarmNodes: new Set([]),
};
},
@ -1212,7 +1213,7 @@
options.messageType = message.get('type');
// Add the message sending on another queue so that our UI doesn't get blocked
this.queueMessageSend(async () =>
this.queueMessageSend(async () => {
message.send(
this.wrapSend(
sendFunction(
@ -1226,8 +1227,8 @@
options
)
)
)
);
);
});
return true;
});

View File

@ -108,6 +108,8 @@ module.exports = {
removeSessionsByNumber,
removeAllSessions,
getSwarmNodesByPubkey,
getConversationCount,
saveConversation,
saveConversations,
@ -653,12 +655,33 @@ async function removeAllSessions(id) {
// Conversation
function setifyProperty(data, propertyName) {
if (!data) return data;
const returnData = data;
if (returnData[propertyName]) {
returnData[propertyName] = new Set(returnData[propertyName]);
}
return returnData;
}
async function getSwarmNodesByPubkey(pubkey) {
let swarmNodes = await channels.getSwarmNodesByPubkey(pubkey);
if (Array.isArray(swarmNodes)) {
swarmNodes = new Set(swarmNodes);
}
return swarmNodes;
}
async function getConversationCount() {
return channels.getConversationCount();
}
async function saveConversation(data) {
await channels.saveConversation(data);
const storeData = data;
if (storeData.swarmNodes) {
storeData.swarmNodes = Array.from(storeData.swarmNodes);
}
await channels.saveConversation(storeData);
}
async function saveConversations(data) {
@ -666,7 +689,8 @@ async function saveConversations(data) {
}
async function getConversationById(id, { Conversation }) {
const data = await channels.getConversationById(id);
const rawData = await channels.getConversationById(id);
const data = setifyProperty(rawData, 'swarmNodes');
return new Conversation(data);
}
@ -677,6 +701,9 @@ async function updateConversation(id, data, { Conversation }) {
}
const merged = merge({}, existing.attributes, data);
if (merged.swarmNodes instanceof Set) {
merged.swarmNodes = Array.from(merged.swarmNodes);
}
await channels.updateConversation(merged);
}
@ -697,7 +724,9 @@ async function _removeConversations(ids) {
}
async function getAllConversations({ ConversationCollection }) {
const conversations = await channels.getAllConversations();
const conversations = (await channels.getAllConversations()).map(c =>
setifyProperty(c, 'swarmNodes')
);
const collection = new ConversationCollection();
collection.add(conversations);
@ -710,7 +739,9 @@ async function getAllConversationIds() {
}
async function getAllPrivateConversations({ ConversationCollection }) {
const conversations = await channels.getAllPrivateConversations();
const conversations = (await channels.getAllPrivateConversations()).map(c =>
setifyProperty(c, 'swarmNodes')
);
const collection = new ConversationCollection();
collection.add(conversations);
@ -718,7 +749,9 @@ async function getAllPrivateConversations({ ConversationCollection }) {
}
async function getAllGroupsInvolvingId(id, { ConversationCollection }) {
const conversations = await channels.getAllGroupsInvolvingId(id);
const conversations = (await channels.getAllGroupsInvolvingId(id)).map(c =>
setifyProperty(c, 'swarmNodes')
);
const collection = new ConversationCollection();
collection.add(conversations);
@ -726,7 +759,9 @@ async function getAllGroupsInvolvingId(id, { ConversationCollection }) {
}
async function searchConversations(query, { ConversationCollection }) {
const conversations = await channels.searchConversations(query);
const conversations = (await channels.searchConversations(query)).map(c =>
setifyProperty(c, 'swarmNodes')
);
const collection = new ConversationCollection();
collection.add(conversations);

View File

@ -1,24 +1,26 @@
/* eslint-disable no-await-in-loop */
/* global log, dcodeIO, window, callWorker */
const fetch = require('node-fetch');
const is = require('@sindresorhus/is');
class LokiServer {
constructor({ urls }) {
this.nodes = [];
urls.forEach(url => {
if (!is.string(url)) {
throw new Error('WebAPI.initialize: Invalid server url');
}
this.nodes.push({ url });
});
// eslint-disable-next-line
const invert = p => new Promise((res, rej) => p.then(rej, res));
const firstOf = ps => invert(Promise.all(ps.map(invert)));
// Will be raised (to 3?) when we get more nodes
const MINIMUM_SUCCESSFUL_REQUESTS = 2;
class LokiMessageAPI {
constructor({ messageServerPort }) {
this.messageServerPort = messageServerPort ? `:${messageServerPort}` : '';
}
async sendMessage(pubKey, data, messageTimeStamp, ttl) {
const data64 = dcodeIO.ByteBuffer.wrap(data).toString('base64');
// Hardcoded to use a single node/server for now
const currentNode = this.nodes[0];
const swarmNodes = await window.LokiSnodeAPI.getSwarmNodesByPubkey(pubKey);
if (!swarmNodes || swarmNodes.size === 0) {
throw Error('No swarm nodes to query!');
}
const data64 = dcodeIO.ByteBuffer.wrap(data).toString('base64');
const timestamp = Math.floor(Date.now() / 1000);
// Nonce is returned as a base64 string to include in header
let nonce;
@ -38,113 +40,153 @@ class LokiServer {
);
} catch (err) {
// Something went horribly wrong
// TODO: Handle gracefully
throw err;
}
const options = {
url: `${currentNode.url}/store`,
type: 'POST',
responseType: undefined,
timeout: undefined,
};
const requests = Array.from(swarmNodes).map(async node => {
// TODO: Confirm sensible timeout
const options = {
url: `${node}${this.messageServerPort}/store`,
type: 'POST',
responseType: undefined,
timeout: 5000,
};
const fetchOptions = {
method: options.type,
body: data64,
headers: {
'X-Loki-pow-nonce': nonce,
'X-Loki-timestamp': timestamp.toString(),
'X-Loki-ttl': ttl.toString(),
'X-Loki-recipient': pubKey,
},
timeout: options.timeout,
};
const fetchOptions = {
method: options.type,
body: data64,
headers: {
'X-Loki-pow-nonce': nonce,
'X-Loki-timestamp': timestamp.toString(),
'X-Loki-ttl': ttl.toString(),
'X-Loki-recipient': pubKey,
},
timeout: options.timeout,
};
let response;
let response;
try {
response = await fetch(options.url, fetchOptions);
} catch (e) {
log.error(options.type, options.url, 0, 'Error sending message');
window.LokiSnodeAPI.unreachableNode(pubKey, node);
throw HTTPError('fetch error', 0, e.toString());
}
let result;
if (
options.responseType === 'json' &&
response.headers.get('Content-Type') === 'application/json'
) {
result = await response.json();
} else if (options.responseType === 'arraybuffer') {
result = await response.buffer();
} else {
result = await response.text();
}
if (response.status >= 0 && response.status < 400) {
return result;
}
log.error(
options.type,
options.url,
response.status,
'Error sending message'
);
throw HTTPError('sendMessage: error response', response.status, result);
});
try {
response = await fetch(options.url, fetchOptions);
} catch (e) {
log.error(options.type, options.url, 0, 'Error');
throw HTTPError('fetch error', 0, e.toString());
}
let result;
if (
options.responseType === 'json' &&
response.headers.get('Content-Type') === 'application/json'
) {
result = await response.json();
} else if (options.responseType === 'arraybuffer') {
result = await response.buffer();
} else {
result = await response.text();
}
if (response.status >= 0 && response.status < 400) {
// TODO: Possibly change this to require more than a single response?
const result = await firstOf(requests);
return result;
} catch (err) {
throw err;
}
log.error(options.type, options.url, response.status, 'Error');
throw HTTPError('sendMessage: error response', response.status, result);
}
async retrieveMessages(pubKey) {
// Hardcoded to use a single node/server for now
const currentNode = this.nodes[0];
async retrieveMessages(callback) {
const ourKey = window.textsecure.storage.user.getNumber();
let completedRequests = 0;
const options = {
url: `${currentNode.url}/retrieve`,
type: 'GET',
responseType: 'json',
timeout: undefined,
};
const doRequest = async (nodeUrl, nodeData) => {
// TODO: Confirm sensible timeout
const options = {
url: `${nodeUrl}${this.messageServerPort}/retrieve`,
type: 'GET',
responseType: 'json',
timeout: 5000,
};
const headers = {
'X-Loki-recipient': pubKey,
};
const headers = {
'X-Loki-recipient': ourKey,
};
if (currentNode.lastHash) {
headers['X-Loki-last-hash'] = currentNode.lastHash;
}
const fetchOptions = {
method: options.type,
headers,
timeout: options.timeout,
};
let response;
try {
response = await fetch(options.url, fetchOptions);
} catch (e) {
log.error(options.type, options.url, 0, 'Error');
throw HTTPError('fetch error', 0, e.toString());
}
let result;
if (
options.responseType === 'json' &&
response.headers.get('Content-Type') === 'application/json'
) {
result = await response.json();
} else if (options.responseType === 'arraybuffer') {
result = await response.buffer();
} else {
result = await response.text();
}
if (response.status >= 0 && response.status < 400) {
if (result.lastHash) {
currentNode.lastHash = result.lastHash;
if (nodeData.lastHash) {
headers['X-Loki-last-hash'] = nodeData.lastHash;
}
return result;
const fetchOptions = {
method: options.type,
headers,
timeout: options.timeout,
};
let response;
try {
response = await fetch(options.url, fetchOptions);
} catch (e) {
// TODO: Maybe we shouldn't immediately delete?
// And differentiate between different connectivity issues
log.error(
options.type,
options.url,
0,
`Error retrieving messages from ${nodeUrl}`
);
window.LokiSnodeAPI.unreachableNode(ourKey, nodeUrl);
return;
}
let result;
if (
options.responseType === 'json' &&
response.headers.get('Content-Type') === 'application/json'
) {
result = await response.json();
} else if (options.responseType === 'arraybuffer') {
result = await response.buffer();
} else {
result = await response.text();
}
completedRequests += 1;
if (response.status === 200) {
if (result.lastHash) {
window.LokiSnodeAPI.updateLastHash(nodeUrl, result.lastHash);
callback(result.messages);
}
return;
}
// Handle error from snode
log.error(options.type, options.url, response.status, 'Error');
};
while (completedRequests < MINIMUM_SUCCESSFUL_REQUESTS) {
const remainingRequests = MINIMUM_SUCCESSFUL_REQUESTS - completedRequests;
const ourSwarmNodes = await window.LokiSnodeAPI.getOurSwarmNodes();
if (Object.keys(ourSwarmNodes).length < remainingRequests) {
// This means we don't have enough swarm nodes to meet the minimum threshold
if (completedRequests !== 0) {
// TODO: Decide how to handle some completed requests but not enough
}
}
await Promise.all(
Object.entries(ourSwarmNodes)
.splice(0, remainingRequests)
.map(([nodeUrl, lastHash]) => doRequest(nodeUrl, lastHash))
);
}
log.error(options.type, options.url, response.status, 'Error');
throw HTTPError(
'retrieveMessages: error response',
response.status,
result
);
}
}
@ -163,5 +205,5 @@ function HTTPError(message, providedCode, response, stack) {
}
module.exports = {
LokiServer,
LokiMessageAPI,
};

View File

@ -0,0 +1,202 @@
/* global log, window, Whisper */
const fetch = require('node-fetch');
const is = require('@sindresorhus/is');
const dns = require('dns');
// Will be raised (to 3?) when we get more nodes
const MINIMUM_SWARM_NODES = 1;
class LokiSnodeAPI {
constructor({ url, swarmServerPort }) {
if (!is.string(url)) {
throw new Error('WebAPI.initialize: Invalid server url');
}
this.url = url;
this.swarmServerPort = swarmServerPort ? `:${swarmServerPort}` : '';
this.swarmsPendingReplenish = {};
this.ourSwarmNodes = {};
}
getRandomSnodeAddress() {
/* resolve random snode */
return new Promise((resolve, reject) => {
dns.resolveCname(this.url, (err, address) => {
if (err) {
reject(err);
} else {
resolve(address[0]);
}
});
});
}
async unreachableNode(pubKey, nodeUrl) {
if (pubKey === window.textsecure.storage.user.getNumber()) {
delete this.ourSwarmNodes[nodeUrl];
return;
}
const conversation = window.ConversationController.get(pubKey);
const swarmNodes = conversation.get('swarmNodes');
if (swarmNodes.delete(nodeUrl)) {
conversation.set({ swarmNodes });
await window.Signal.Data.updateConversation(
conversation.id,
conversation.attributes,
{
Conversation: Whisper.Conversation,
}
);
}
}
updateLastHash(nodeUrl, hash) {
if (!this.ourSwarmNodes[nodeUrl]) {
this.ourSwarmNodes[nodeUrl] = {
lastHash: hash,
};
} else {
this.ourSwarmNodes[nodeUrl].lastHash = hash;
}
}
async getOurSwarmNodes() {
if (
!this.ourSwarmNodes ||
Object.keys(this.ourSwarmNodes).length < MINIMUM_SWARM_NODES
) {
this.ourSwarmNodes = {};
// Try refresh our swarm list once
const ourKey = window.textsecure.storage.user.getNumber();
const nodeAddresses = await window.LokiSnodeAPI.getSwarmNodes(ourKey);
if (!nodeAddresses || nodeAddresses.length === 0) {
throw Error('Could not load our swarm');
}
nodeAddresses.forEach(url => {
this.ourSwarmNodes[url] = {};
});
}
return this.ourSwarmNodes;
}
async getSwarmNodesByPubkey(pubKey) {
const swarmNodes = await window.Signal.Data.getSwarmNodesByPubkey(pubKey);
// TODO: Check if swarm list is below a threshold rather than empty
if (swarmNodes && swarmNodes.size !== 0) {
return swarmNodes;
}
return this.replenishSwarm(pubKey);
}
async replenishSwarm(pubKey) {
const conversation = window.ConversationController.get(pubKey);
if (!(pubKey in this.swarmsPendingReplenish)) {
this.swarmsPendingReplenish[pubKey] = new Promise(async resolve => {
let newSwarmNodes;
try {
newSwarmNodes = new Set(await this.getSwarmNodes(pubKey));
} catch (e) {
// TODO: Handle these errors sensibly
newSwarmNodes = new Set([]);
}
conversation.set({ swarmNodes: newSwarmNodes });
await window.Signal.Data.updateConversation(
conversation.id,
conversation.attributes,
{
Conversation: Whisper.Conversation,
}
);
resolve(newSwarmNodes);
});
}
const newSwarmNodes = await this.swarmsPendingReplenish[pubKey];
delete this.swarmsPendingReplenish[pubKey];
return newSwarmNodes;
}
async getSwarmNodes(pubKey) {
// TODO: Hit multiple random nodes and merge lists?
const node = await this.getRandomSnodeAddress();
// TODO: Confirm final API URL and sensible timeout
const options = {
url: `http://${node}${this.swarmServerPort}/json_rpc`,
type: 'POST',
responseType: 'json',
timeout: 5000,
};
const body = {
jsonrpc: '2.0',
id: '0',
method: 'get_swarm_list_for_messenger_pubkey',
params: {
pubkey: pubKey,
},
};
const fetchOptions = {
method: options.type,
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
timeout: options.timeout,
};
let response;
try {
response = await fetch(options.url, fetchOptions);
} catch (e) {
log.error(
options.type,
options.url,
0,
`Error getting swarm nodes for ${pubKey}`
);
throw HTTPError('fetch error', 0, e.toString());
}
let result;
if (
options.responseType === 'json' &&
response.headers.get('Content-Type') === 'application/json'
) {
result = await response.json();
} else if (options.responseType === 'arraybuffer') {
result = await response.buffer();
} else {
result = await response.text();
}
if (response.status >= 0 && response.status < 400) {
return result.nodes;
}
log.error(
options.type,
options.url,
response.status,
`Error getting swarm nodes for ${pubKey}`
);
throw HTTPError('sendMessage: error response', response.status, result);
}
}
function HTTPError(message, providedCode, response, stack) {
const code = providedCode > 999 || providedCode < 100 ? -1 : providedCode;
const e = new Error(`${message}; code: ${code}`);
e.name = 'HTTPError';
e.code = code;
if (stack) {
e.stack += `\nOriginal stack:\n${stack}`;
}
if (response) {
e.response = response;
}
return e;
}
module.exports = {
LokiSnodeAPI,
};

View File

@ -4,30 +4,36 @@
(function() {
window.libloki = window.libloki || {};
function consolidateLists(lists, threshold = 1) {
function consolidateLists(lists, threshold, selector = x => x) {
if (typeof threshold !== 'number') {
throw Error('Provided threshold is not a number');
}
if (typeof selector !== 'function') {
throw Error('Provided selector is not a function');
}
// calculate list size manually since `Set`
// does not have a `length` attribute
let numLists = 0;
const occurences = {};
const values = {};
lists.forEach(list => {
numLists += 1;
list.forEach(item => {
if (!(item in occurences)) {
occurences[item] = 1;
const key = selector(item);
if (!(key in occurences)) {
occurences[key] = 1;
values[key] = item;
} else {
occurences[item] += 1;
occurences[key] += 1;
}
});
});
const scaledThreshold = numLists * threshold;
return Object.entries(occurences)
.filter(keyValue => keyValue[1] >= scaledThreshold)
.map(keyValue => keyValue[0]);
return Object.keys(occurences)
.filter(key => occurences[key] >= scaledThreshold)
.map(key => values[key]);
}
window.libloki.serviceNodes = {

View File

@ -23,13 +23,25 @@ describe('ServiceNodes', () => {
);
});
it('should throw when provided a non-function selector', () => {
[1, 'a', 0xffffffff, { really: 'not a function' }].forEach(x => {
assert.throws(
() => libloki.serviceNodes.consolidateLists([], 1, x),
'Provided selector is not a function'
);
});
});
it('should return an empty array when the input is an empty array', () => {
const result = libloki.serviceNodes.consolidateLists([]);
const result = libloki.serviceNodes.consolidateLists([], 1);
assert.deepEqual(result, []);
});
it('should return the input when only 1 list is provided', () => {
const result = libloki.serviceNodes.consolidateLists([['a', 'b', 'c']]);
const result = libloki.serviceNodes.consolidateLists(
[['a', 'b', 'c']],
1
);
assert.deepEqual(result, ['a', 'b', 'c']);
});
@ -41,6 +53,39 @@ describe('ServiceNodes', () => {
assert.deepEqual(result.sort(), ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
});
it('should use the selector to identify the elements', () => {
const result = libloki.serviceNodes.consolidateLists(
[
[
{ id: 1, val: 'a' },
{ id: 2, val: 'b' },
{ id: 3, val: 'c' },
{ id: 8, val: 'h' },
],
[
{ id: 4, val: 'd' },
{ id: 5, val: 'e' },
{ id: 6, val: 'f' },
{ id: 7, val: 'g' },
],
[{ id: 7, val: 'g' }, { id: 8, val: 'h' }],
],
0,
x => x.id
);
const expected = [
{ id: 1, val: 'a' },
{ id: 2, val: 'b' },
{ id: 3, val: 'c' },
{ id: 4, val: 'd' },
{ id: 5, val: 'e' },
{ id: 6, val: 'f' },
{ id: 7, val: 'g' },
{ id: 8, val: 'h' },
];
assert.deepEqual(result.sort((a, b) => a.val > b.val), expected);
});
it('should return the intersection of all lists when threshold is 1', () => {
const result = libloki.serviceNodes.consolidateLists(
[['a', 'b', 'c', 'd'], ['a', 'e', 'f', 'g'], ['a', 'h']],

View File

@ -1,4 +1,4 @@
/* global window, dcodeIO, textsecure, StringView */
/* global window, dcodeIO, textsecure */
// eslint-disable-next-line func-names
(function() {
@ -66,30 +66,8 @@
}
let connected = false;
this.startPolling = async function pollServer(callBack) {
const myKeys = await textsecure.storage.protocol.getIdentityKeyPair();
const pubKey = StringView.arrayBufferToHex(myKeys.pubKey);
let result;
try {
result = await server.retrieveMessages(pubKey);
connected = true;
} catch (err) {
connected = false;
setTimeout(() => {
pollServer(callBack);
}, pollTime);
return;
}
if (typeof callBack === 'function') {
callBack(connected);
}
if (!result.messages) {
setTimeout(() => {
pollServer(callBack);
}, pollTime);
return;
}
const newMessages = await filterIncomingMessages(result.messages);
const processMessages = async messages => {
const newMessages = await filterIncomingMessages(messages);
newMessages.forEach(async message => {
const { data } = message;
const dataPlaintext = stringToArrayBufferBase64(data);
@ -109,8 +87,18 @@
);
}
});
};
this.startPolling = async function pollServer(callback) {
try {
await server.retrieveMessages(processMessages);
connected = true;
} catch (err) {
connected = false;
}
callback(connected);
setTimeout(() => {
pollServer(callBack);
pollServer(callback);
}, pollTime);
};

View File

@ -22,7 +22,7 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
this.signalingKey = signalingKey;
this.username = username;
this.password = password;
this.lokiserver = window.LokiAPI;
this.lokiMessageAPI = window.LokiMessageAPI;
if (!options.serverTrustRoot) {
throw new Error('Server trust root is required!');
@ -67,7 +67,7 @@ MessageReceiver.prototype.extend({
}
this.hasConnected = true;
this.httpPollingResource = new HttpResource(this.lokiserver, {
this.httpPollingResource = new HttpResource(this.lokiMessageAPI, {
handleRequest: this.handleRequest.bind(this),
});
this.httpPollingResource.startPolling(connected => {

View File

@ -34,7 +34,7 @@ function OutgoingMessage(
this.callback = callback;
this.silent = silent;
this.lokiserver = window.LokiAPI;
this.lokiMessageAPI = window.LokiMessageAPI;
this.numbersCompleted = 0;
this.errors = [];
@ -186,7 +186,7 @@ OutgoingMessage.prototype = {
async transmitMessage(number, data, timestamp, ttl = 24 * 60 * 60) {
const pubKey = number;
try {
const result = await this.lokiserver.sendMessage(
const result = await this.lokiMessageAPI.sendMessage(
pubKey,
data,
timestamp,

View File

@ -144,6 +144,8 @@ function prepareURL(pathSegments, moreKeys) {
buildExpiration: config.get('buildExpiration'),
serverUrl: config.get('serverUrl'),
cdnUrl: config.get('cdnUrl'),
messageServerPort: config.get('messageServerPort'),
swarmServerPort: config.get('swarmServerPort'),
certificateAuthority: config.get('certificateAuthority'),
environment: config.environment,
node_version: process.versions.node,

View File

@ -269,10 +269,18 @@ window.WebAPI = initializeWebAPI({
proxyUrl: config.proxyUrl,
});
const { LokiServer } = require('./js/modules/loki_message_api');
const { LokiSnodeAPI } = require('./js/modules/loki_snode_api');
window.LokiAPI = new LokiServer({
urls: [config.serverUrl],
window.LokiSnodeAPI = new LokiSnodeAPI({
url: config.serverUrl,
swarmServerPort: config.swarmServerPort,
});
const { LokiMessageAPI } = require('./js/modules/loki_message_api');
window.LokiMessageAPI = new LokiMessageAPI({
url: config.serverUrl,
messageServerPort: config.messageServerPort,
});
window.mnemonic = require('./libloki/mnemonic');