diff --git a/Gruntfile.js b/Gruntfile.js
index 95e327054..97ee125bd 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -102,6 +102,7 @@ module.exports = grunt => {
libloki: {
src: [
'libloki/api.js',
+ 'libloki/friends.js',
'libloki/crypto.js',
'libloki/service_nodes.js',
'libloki/storage.js',
diff --git a/app/sql.js b/app/sql.js
index 7be7ec038..b64c4feac 100644
--- a/app/sql.js
+++ b/app/sql.js
@@ -90,6 +90,7 @@ module.exports = {
updateConversation,
removeConversation,
getAllConversations,
+ getPubKeysWithFriendStatus,
getAllConversationIds,
getAllPrivateConversations,
getAllGroupsInvolvingId,
@@ -1127,7 +1128,7 @@ async function getSwarmNodesByPubkey(pubkey) {
});
if (!row) {
- return null;
+ return [];
}
return jsonToObject(row.json).swarmNodes;
@@ -1280,6 +1281,18 @@ async function getAllConversations() {
return map(rows, row => jsonToObject(row.json));
}
+async function getPubKeysWithFriendStatus(status) {
+ const rows = await db.all(
+ `SELECT id FROM conversations WHERE
+ friendRequestStatus = $status
+ ORDER BY id ASC;`,
+ {
+ $status: status,
+ }
+ );
+ return map(rows, row => row.id);
+}
+
async function getAllConversationIds() {
const rows = await db.all('SELECT id FROM conversations ORDER BY id ASC;');
return map(rows, row => row.id);
diff --git a/config/default.json b/config/default.json
index 3b605aa6d..09b15a766 100644
--- a/config/default.json
+++ b/config/default.json
@@ -1,6 +1,7 @@
{
"serverUrl": "random.snode",
"cdnUrl": "random.snode",
+ "localServerPort": "8081",
"messageServerPort": "8080",
"swarmServerPort": "8079",
"disableAutoUpdate": false,
diff --git a/config/development-1.json b/config/development-1.json
index 9099ce326..57b4796c5 100644
--- a/config/development-1.json
+++ b/config/development-1.json
@@ -1,5 +1,6 @@
{
"storageProfile": "development1",
+ "localServerPort": "8082",
"disableAutoUpdate": true,
"openDevTools": true
}
diff --git a/js/conversation_controller.js b/js/conversation_controller.js
index c92f07c9c..8decd5151 100644
--- a/js/conversation_controller.js
+++ b/js/conversation_controller.js
@@ -182,11 +182,10 @@
}
try {
- const swarmNodes = await window.LokiSnodeAPI.getFreshSwarmNodes(id);
- conversation.set({ swarmNodes});
await window.Signal.Data.saveConversation(conversation.attributes, {
Conversation: Whisper.Conversation,
});
+ window.LokiSnodeAPI.refreshSwarmNodesForPubKey(id);
} catch (error) {
window.log.error(
'Conversation save failed! ',
diff --git a/js/models/conversations.js b/js/models/conversations.js
index d4274e3af..7f510916e 100644
--- a/js/models/conversations.js
+++ b/js/models/conversations.js
@@ -41,18 +41,7 @@
} = window.Signal.Migrations;
// Possible conversation friend states
- const FriendRequestStatusEnum = Object.freeze({
- // New conversation, no messages sent or received
- none: 0,
- // This state is used to lock the input early while sending
- pendingSend: 1,
- // Friend request sent, awaiting response
- requestSent: 2,
- // Friend request received, awaiting user input
- requestReceived: 3,
- // We did it!
- friends: 4,
- });
+ const FriendRequestStatusEnum = window.friends.friendRequestStatusEnum;
// Possible session reset states
const SessionResetEnum = Object.freeze({
diff --git a/js/modules/data.js b/js/modules/data.js
index e8afe5b14..d4e2fb58e 100644
--- a/js/modules/data.js
+++ b/js/modules/data.js
@@ -110,6 +110,7 @@ module.exports = {
removeAllSessions,
getSwarmNodesByPubkey,
+ saveSwarmNodesForPubKey,
getConversationCount,
saveConversation,
@@ -120,6 +121,7 @@ module.exports = {
_removeConversations,
getAllConversations,
+ getPubKeysWithFriendStatus,
getAllConversationIds,
getAllPrivateConversations,
getAllGroupsInvolvingId,
@@ -673,6 +675,14 @@ async function getSwarmNodesByPubkey(pubkey) {
return channels.getSwarmNodesByPubkey(pubkey);
}
+async function saveSwarmNodesForPubKey(pubKey, swarmNodes, { Conversation }) {
+ const conversation = await getConversationById(pubKey, { Conversation });
+ conversation.set({ swarmNodes });
+ await updateConversation(conversation.id, conversation.attributes, {
+ Conversation,
+ });
+}
+
async function getConversationCount() {
return channels.getConversationCount();
}
@@ -721,6 +731,10 @@ async function _removeConversations(ids) {
await channels.removeConversation(ids);
}
+async function getPubKeysWithFriendStatus(status) {
+ return channels.getPubKeysWithFriendStatus(status);
+}
+
async function getAllConversations({ ConversationCollection }) {
const conversations = await channels.getAllConversations();
diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js
index d479c3731..ddd565548 100644
--- a/js/modules/loki_message_api.js
+++ b/js/modules/loki_message_api.js
@@ -1,6 +1,6 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-loop-func */
-/* global log, dcodeIO, window, callWorker */
+/* global log, dcodeIO, window, callWorker, Whisper */
const fetch = require('node-fetch');
const _ = require('lodash');
@@ -105,20 +105,15 @@ class LokiMessageAPI {
throw HTTPError('sendMessage: error response', response.status, result);
};
- let swarmNodes;
- try {
- swarmNodes = await window.LokiSnodeAPI.getSwarmNodesByPubkey(pubKey);
- } catch (e) {
- throw new window.textsecure.EmptySwarmError(pubKey, e);
- }
+ let swarmNodes = await window.Signal.Data.getSwarmNodesByPubkey(pubKey);
while (successfulRequests < MINIMUM_SUCCESSFUL_REQUESTS) {
if (!canResolve) {
throw new window.textsecure.DNSResolutionError('Sending messages');
}
- if (!swarmNodes || swarmNodes.length === 0) {
+ if (swarmNodes.length === 0) {
swarmNodes = await window.LokiSnodeAPI.getFreshSwarmNodes(pubKey);
swarmNodes = _.difference(swarmNodes, completedNodes);
- if (!swarmNodes || swarmNodes.length === 0) {
+ if (swarmNodes.length === 0) {
if (successfulRequests !== 0) {
// TODO: Decide how to handle some completed requests but not enough
return;
@@ -128,7 +123,9 @@ class LokiMessageAPI {
new Error('Ran out of swarm nodes to query')
);
}
- await window.LokiSnodeAPI.saveSwarmNodes(pubKey, swarmNodes);
+ await window.Signal.Data.saveSwarmNodesForPubKey(pubKey, swarmNodes, {
+ Conversation: Whisper.Conversation,
+ });
}
const remainingRequests =
MINIMUM_SUCCESSFUL_REQUESTS - completedNodes.length;
diff --git a/js/modules/loki_p2p_api.js b/js/modules/loki_p2p_api.js
new file mode 100644
index 000000000..fff8e30d1
--- /dev/null
+++ b/js/modules/loki_p2p_api.js
@@ -0,0 +1,24 @@
+class LokiP2pAPI {
+ constructor() {
+ this.contactP2pDetails = {};
+ }
+
+ addContactP2pDetails(pubKey, address, port) {
+ this.contactP2pDetails[pubKey] = {
+ address,
+ port,
+ };
+ }
+
+ getContactP2pDetails(pubKey) {
+ return this.contactP2pDetails[pubKey] || null;
+ }
+
+ removeContactP2pDetails(pubKey) {
+ delete this.contactP2pDetails[pubKey];
+ }
+}
+
+module.exports = {
+ LokiP2pAPI,
+};
diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js
index d6000c3e8..8d1dc501b 100644
--- a/js/modules/loki_snode_api.js
+++ b/js/modules/loki_snode_api.js
@@ -106,24 +106,11 @@ class LokiSnodeAPI {
return this.ourSwarmNodes;
}
- async getSwarmNodesByPubkey(pubKey) {
- const swarmNodes = await window.Signal.Data.getSwarmNodesByPubkey(pubKey);
- if (swarmNodes) {
- return swarmNodes;
- }
- return [];
- }
-
- async saveSwarmNodes(pubKey, swarmNodes) {
- const conversation = window.ConversationController.get(pubKey);
- conversation.set({ swarmNodes });
- await window.Signal.Data.updateConversation(
- conversation.id,
- conversation.attributes,
- {
- Conversation: Whisper.Conversation,
- }
- );
+ async refreshSwarmNodesForPubKey(pubKey) {
+ const newNodes = await this.getFreshSwarmNodes(pubKey);
+ await window.Signal.Data.saveSwarmNodesForPubKey(pubKey, newNodes, {
+ Conversation: Whisper.Conversation,
+ });
}
async getFreshSwarmNodes(pubKey) {
diff --git a/libloki/api.js b/libloki/api.js
index 6c5c8365f..876cd15e2 100644
--- a/libloki/api.js
+++ b/libloki/api.js
@@ -8,6 +8,53 @@
return sendEmptyMessage(pubKey, true);
}
+ async function broadcastOnlineStatus() {
+ const friendKeys = await window.Signal.Data.getPubKeysWithFriendStatus(
+ window.friends.friendRequestStatusEnum.friends
+ );
+ await Promise.all(
+ friendKeys.map(async pubKey => {
+ try {
+ await sendOnlineBroadcastMessage(pubKey);
+ } catch (e) {
+ log.warn(`Failed to send online broadcast message to ${pubKey}`);
+ }
+ })
+ );
+ }
+
+ async function sendOnlineBroadcastMessage(pubKey) {
+ // TODO: Make this actually get a loki address rather than junk string
+ const lokiAddressMessage = new textsecure.protobuf.LokiAddressMessage({
+ p2pAddress: 'testAddress',
+ p2pPort: parseInt(window.localServerPort, 10),
+ });
+ const content = new textsecure.protobuf.Content({
+ lokiAddressMessage,
+ });
+
+ // will be called once the transmission succeeded or failed
+ const callback = res => {
+ if (res.errors.length > 0) {
+ res.errors.forEach(error => log.error(error));
+ } else {
+ log.info('Online broadcast message sent successfully');
+ }
+ };
+ const options = { messageType: 'onlineBroadcast' };
+ // Send a empty message with information about how to contact us directly
+ const outgoingMessage = new textsecure.OutgoingMessage(
+ null, // server
+ Date.now(), // timestamp,
+ [pubKey], // numbers
+ content, // message
+ true, // silent
+ callback, // callback
+ options
+ );
+ await outgoingMessage.sendToNumber(pubKey);
+ }
+
async function sendEmptyMessage(pubKey, sendContentMessage = false) {
const options = {};
// send an empty message.
@@ -52,5 +99,7 @@
window.libloki.api = {
sendFriendRequestAccepted,
sendEmptyMessage,
+ sendOnlineBroadcastMessage,
+ broadcastOnlineStatus,
};
})();
diff --git a/libloki/friends.js b/libloki/friends.js
new file mode 100644
index 000000000..95b210382
--- /dev/null
+++ b/libloki/friends.js
@@ -0,0 +1,22 @@
+/* global window */
+
+// eslint-disable-next-line func-names
+(function() {
+ // Possible conversation friend states
+ const friendRequestStatusEnum = Object.freeze({
+ // New conversation, no messages sent or received
+ none: 0,
+ // This state is used to lock the input early while sending
+ pendingSend: 1,
+ // Friend request sent, awaiting response
+ requestSent: 2,
+ // Friend request received, awaiting user input
+ requestReceived: 3,
+ // We did it!
+ friends: 4,
+ });
+
+ window.friends = {
+ friendRequestStatusEnum,
+ };
+})();
diff --git a/libloki/local_loki_server.js b/libloki/local_loki_server.js
index 3bfe40887..ed9bf6c03 100644
--- a/libloki/local_loki_server.js
+++ b/libloki/local_loki_server.js
@@ -68,6 +68,7 @@ class LocalLokiServer extends EventEmitter {
// Async wrapper for http server close
close() {
+ this.removeAllListeners();
if (this.server) {
return new Promise(res => {
this.server.close(() => res());
diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js
index e8a2dcb71..5d66d9435 100644
--- a/libtextsecure/message_receiver.js
+++ b/libtextsecure/message_receiver.js
@@ -23,7 +23,7 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
this.username = username;
this.password = password;
this.lokiMessageAPI = window.LokiMessageAPI;
- this.localServer = new window.LocalLokiServer();
+ this.localServer = window.LocalLokiServer;
if (!options.serverTrustRoot) {
throw new Error('Server trust root is required!');
@@ -82,15 +82,11 @@ MessageReceiver.prototype.extend({
}
});
- this.localServer.removeAllListeners();
- this.localServer.on('message', this.httpPollingResource.handleMessage);
-
- // Passing 0 as the port will automatically assign an unused port
- this.localServer
- .start(0)
- .then(port =>
- window.log.info(`Local Server started at localhost:${port}`)
- );
+ this.localServer.start(window.localServerPort).then(port => {
+ window.log.info(`Local Server started at localhost:${port}`);
+ window.libloki.api.broadcastOnlineStatus();
+ this.localServer.on('message', this.httpPollingResource.handleMessage);
+ });
// TODO: Rework this socket stuff to work with online messaging
const useWebSocket = false;
@@ -135,7 +131,10 @@ MessageReceiver.prototype.extend({
}
if (this.localServer) {
- this.localServer.removeAllListeners();
+ this.localServer.removeListener(
+ 'message',
+ this.httpPollingResource.handleMessage
+ );
this.localServer = null;
}
},
@@ -899,6 +898,15 @@ MessageReceiver.prototype.extend({
})
);
},
+ async handleLokiAddressMessage(envelope, lokiAddressMessage) {
+ const { p2pAddress, p2pPort } = lokiAddressMessage;
+ window.LokiP2pAPI.addContactP2pDetails(
+ envelope.source,
+ p2pAddress,
+ p2pPort
+ );
+ return this.removeFromCache(envelope);
+ },
handleDataMessage(envelope, msg) {
window.log.info('data message from', this.getEnvelopeId(envelope));
let p = Promise.resolve();
@@ -1013,6 +1021,11 @@ MessageReceiver.prototype.extend({
envelope.source,
content.preKeyBundleMessage
);
+ if (content.lokiAddressMessage)
+ return this.handleLokiAddressMessage(
+ envelope,
+ content.lokiAddressMessage
+ );
if (content.syncMessage)
return this.handleSyncMessage(envelope, content.syncMessage);
if (content.dataMessage)
diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js
index 668e4067e..885de9250 100644
--- a/libtextsecure/outgoing_message.js
+++ b/libtextsecure/outgoing_message.js
@@ -336,8 +336,19 @@ OutgoingMessage.prototype = {
dcodeIO.ByteBuffer.wrap(ciphertext.body, 'binary').toArrayBuffer()
);
}
+ let ttl;
+ if (this.messageType === 'friend-request') {
+ ttl = 4 * 24 * 60 * 60; // 4 days for friend request message
+ } else if (this.messageType === 'onlineBroadcast') {
+ ttl = 10 * 60; // 10 minutes for online broadcast message
+ } else {
+ const hours = window.getMessageTTL() || 24; // 1 day default for any other message
+ ttl = hours * 60 * 60;
+ }
+
return {
type: ciphertext.type, // FallBackSessionCipher sets this to FRIEND_REQUEST
+ ttl,
ourKey,
sourceDevice: 1,
destinationRegistrationId: ciphertext.registrationId,
@@ -349,17 +360,12 @@ OutgoingMessage.prototype = {
// TODO: handle multiple devices/messages per transmit
const outgoingObject = outgoingObjects[0];
const socketMessage = await this.wrapInWebsocketMessage(outgoingObject);
- let ttl;
- if (
- outgoingObject.type ===
- textsecure.protobuf.Envelope.Type.FRIEND_REQUEST
- ) {
- ttl = 4 * 24 * 60 * 60; // 4 days for friend request message
- } else {
- const hours = window.getMessageTTL() || 24; // 1 day default for any other message
- ttl = hours * 60 * 60;
- }
- await this.transmitMessage(number, socketMessage, this.timestamp, ttl);
+ await this.transmitMessage(
+ number,
+ socketMessage,
+ this.timestamp,
+ outgoingObject.ttl
+ );
this.successfulNumbers[this.successfulNumbers.length] = number;
this.numberCompleted();
})
diff --git a/main.js b/main.js
index 751904140..42445e529 100644
--- a/main.js
+++ b/main.js
@@ -146,6 +146,7 @@ function prepareURL(pathSegments, moreKeys) {
cdnUrl: config.get('cdnUrl'),
messageServerPort: config.get('messageServerPort'),
swarmServerPort: config.get('swarmServerPort'),
+ localServerPort: config.get('localServerPort'),
certificateAuthority: config.get('certificateAuthority'),
environment: config.environment,
node_version: process.versions.node,
diff --git a/preload.js b/preload.js
index 1a7b85b07..67affe39b 100644
--- a/preload.js
+++ b/preload.js
@@ -276,6 +276,10 @@ window.LokiSnodeAPI = new LokiSnodeAPI({
swarmServerPort: config.swarmServerPort,
});
+const { LokiP2pAPI } = require('./js/modules/loki_p2p_api');
+
+window.LokiP2pAPI = new LokiP2pAPI();
+
const { LokiMessageAPI } = require('./js/modules/loki_message_api');
window.LokiMessageAPI = new LokiMessageAPI({
@@ -285,7 +289,8 @@ window.LokiMessageAPI = new LokiMessageAPI({
const { LocalLokiServer } = require('./libloki/local_loki_server');
-window.LocalLokiServer = LocalLokiServer;
+window.localServerPort = config.localServerPort;
+window.LocalLokiServer = new LocalLokiServer();
window.mnemonic = require('./libloki/mnemonic');
const { WorkerInterface } = require('./js/modules/util_worker_interface');
diff --git a/protos/SignalService.proto b/protos/SignalService.proto
index 61d6fa91d..f71b3b1b1 100644
--- a/protos/SignalService.proto
+++ b/protos/SignalService.proto
@@ -35,6 +35,12 @@ message Content {
optional ReceiptMessage receiptMessage = 5;
optional TypingMessage typingMessage = 6;
optional PreKeyBundleMessage preKeyBundleMessage = 101;
+ optional LokiAddressMessage lokiAddressMessage = 102;
+}
+
+message LokiAddressMessage {
+ optional string p2pAddress = 1;
+ optional uint32 p2pPort = 2;
}
message PreKeyBundleMessage {
diff --git a/test/index.html b/test/index.html
index 80e223b1c..c541e717b 100644
--- a/test/index.html
+++ b/test/index.html
@@ -360,6 +360,7 @@
+