Merge commit 'ef44a123ed4ada70489d78fde16a86c60ced34a8' into feature/blocking

pull/47/head
Mikunj 6 years ago
commit a3a7f4a621

@ -89,6 +89,9 @@ module.exports = {
getMessageCount, getMessageCount,
saveMessage, saveMessage,
cleanSeenMessages,
saveSeenMessageHashes,
saveSeenMessageHash,
saveMessages, saveMessages,
removeMessage, removeMessage,
getUnreadByConversation, getUnreadByConversation,
@ -98,6 +101,7 @@ module.exports = {
getAllMessageIds, getAllMessageIds,
getAllUnsentMessages, getAllUnsentMessages,
getMessagesBySentAt, getMessagesBySentAt,
getSeenMessagesByHashList,
getExpiredMessages, getExpiredMessages,
getOutgoingWithoutExpiresAt, getOutgoingWithoutExpiresAt,
getNextExpiringMessage, getNextExpiringMessage,
@ -390,6 +394,13 @@ async function updateToSchemaVersion6(currentVersion, instance) {
console.log('updateToSchemaVersion6: starting...'); console.log('updateToSchemaVersion6: starting...');
await instance.run('BEGIN TRANSACTION;'); await instance.run('BEGIN TRANSACTION;');
await instance.run(
`CREATE TABLE seenMessages(
hash STRING PRIMARY KEY,
expiresAt INTEGER
);`
);
// key-value, ids are strings, one extra column // key-value, ids are strings, one extra column
await instance.run( await instance.run(
`CREATE TABLE sessions( `CREATE TABLE sessions(
@ -447,6 +458,11 @@ async function updateToSchemaVersion6(currentVersion, instance) {
);` );`
); );
await instance.run(`CREATE UNIQUE INDEX contact_prekey_identity_key_string_keyid ON contactPreKeys (
identityKeyString,
keyId
);`);
await instance.run( await instance.run(
`CREATE TABLE contactSignedPreKeys( `CREATE TABLE contactSignedPreKeys(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
@ -456,6 +472,11 @@ async function updateToSchemaVersion6(currentVersion, instance) {
);` );`
); );
await instance.run(`CREATE UNIQUE INDEX contact_signed_prekey_identity_key_string_keyid ON contactSignedPreKeys (
identityKeyString,
keyId
);`);
await instance.run('PRAGMA schema_version = 6;'); await instance.run('PRAGMA schema_version = 6;');
await instance.run('COMMIT TRANSACTION;'); await instance.run('COMMIT TRANSACTION;');
console.log('updateToSchemaVersion6: success!'); console.log('updateToSchemaVersion6: success!');
@ -1116,8 +1137,8 @@ async function saveMessage(data, { forceSave } = {}) {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
received_at, received_at,
schemaVersion, schemaVersion,
// eslint-disable-next-line camelcase
sent, sent,
// eslint-disable-next-line camelcase
sent_at, sent_at,
source, source,
sourceDevice, sourceDevice,
@ -1230,6 +1251,45 @@ async function saveMessage(data, { forceSave } = {}) {
return toCreate.id; return toCreate.id;
} }
async function saveSeenMessageHashes(arrayOfHashes) {
let promise;
db.serialize(() => {
promise = Promise.all([
db.run('BEGIN TRANSACTION;'),
...map(arrayOfHashes, hashData => saveSeenMessageHash(hashData)),
db.run('COMMIT TRANSACTION;'),
]);
});
await promise;
}
async function saveSeenMessageHash(data) {
const {
expiresAt,
hash,
} = data;
await db.run(
`INSERT INTO seenMessages (
expiresAt,
hash
) values (
$expiresAt,
$hash
);`, {
$expiresAt: expiresAt,
$hash: hash,
}
);
}
async function cleanSeenMessages() {
await db.run('DELETE FROM seenMessages WHERE expiresAt <= $now;', {
$now: Date.now(),
});
}
async function saveMessages(arrayOfMessages, { forceSave } = {}) { async function saveMessages(arrayOfMessages, { forceSave } = {}) {
let promise; let promise;
@ -1360,6 +1420,15 @@ async function getMessagesBySentAt(sentAt) {
return map(rows, row => jsonToObject(row.json)); return map(rows, row => jsonToObject(row.json));
} }
async function getSeenMessagesByHashList(hashes) {
const rows = await db.all(
`SELECT * FROM seenMessages WHERE hash IN ( ${hashes.map(() => '?').join(', ')} );`,
hashes
);
return map(rows, row => row.hash);
}
async function getExpiredMessages() { async function getExpiredMessages() {
const now = Date.now(); const now = Date.now();

@ -7,13 +7,12 @@
Signal, Signal,
storage, storage,
textsecure, textsecure,
WebAPI
Whisper, Whisper,
BlockedNumberController BlockedNumberController
*/ */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(async function() { (async function () {
'use strict'; 'use strict';
// Globally disable drag and drop // Globally disable drag and drop
@ -326,7 +325,7 @@
// Combine the models // Combine the models
const messagesForCleanup = results.reduce((array, current) => array.concat(current.toArray()), []); const messagesForCleanup = results.reduce((array, current) => array.concat(current.toArray()), []);
window.log.info( window.log.info(
`Cleanup: Found ${messagesForCleanup.length} messages for cleanup` `Cleanup: Found ${messagesForCleanup.length} messages for cleanup`
); );
@ -377,7 +376,7 @@
let isMigrationWithIndexComplete = false; let isMigrationWithIndexComplete = false;
window.log.info( window.log.info(
`Starting background data migration. Target version: ${ `Starting background data migration. Target version: ${
Message.CURRENT_SCHEMA_VERSION Message.CURRENT_SCHEMA_VERSION
}` }`
); );
idleDetector.on('idle', async () => { idleDetector.on('idle', async () => {
@ -466,7 +465,13 @@
} }
}); });
function manageSeenMessages() {
window.Signal.Data.cleanSeenMessages();
setTimeout(manageSeenMessages, 1000 * 60 * 60);
}
async function start() { async function start() {
manageSeenMessages();
window.dispatchEvent(new Event('storage_ready')); window.dispatchEvent(new Event('storage_ready'));
window.log.info('listening for registration events'); window.log.info('listening for registration events');
@ -561,7 +566,7 @@
// Gets called when a user accepts or declines a friend request // Gets called when a user accepts or declines a friend request
Whisper.events.on('friendRequestUpdated', friendRequest => { Whisper.events.on('friendRequestUpdated', friendRequest => {
const { pubKey, ...message } = friendRequest; const { pubKey, ...message } = friendRequest;
if (messageReceiver) { if (messageReceiver) {
messageReceiver.onFriendRequestUpdate(pubKey, message); messageReceiver.onFriendRequestUpdate(pubKey, message);
} }
@ -573,11 +578,13 @@
} }
}); });
Whisper.events.on('calculatingPoW', ({ pubKey, timestamp}) => { Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => {
try { try {
const conversation = ConversationController.get(pubKey); const conversation = ConversationController.get(pubKey);
conversation.onCalculatingPoW(pubKey, timestamp); conversation.onCalculatingPoW(pubKey, timestamp);
} catch (e) {} } catch (e) {
window.log.error('Error showing PoW cog');
}
}); });
} }
@ -1285,7 +1292,7 @@
} catch (error) { } catch (error) {
window.log.error( window.log.error(
`Failed to send delivery receipt to ${data.source} for message ${ `Failed to send delivery receipt to ${data.source} for message ${
data.timestamp data.timestamp
}:`, }:`,
error && error.stack ? error.stack : error error && error.stack ? error.stack : error
); );

@ -1,8 +1,7 @@
/* global _: false */ /* global _: false */
/* global Backbone: false */ /* global Backbone: false */
/* global BlockedNumberController: false */
/* global ConversationController: false */ /* global ConversationController: false */
/* global i18n: false */
/* global libsignal: false */ /* global libsignal: false */
/* global storage: false */ /* global storage: false */
/* global textsecure: false */ /* global textsecure: false */
@ -11,7 +10,7 @@
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -251,7 +250,7 @@
} }
}, },
// This goes through all our message history and finds a friend request // This goes through all our message history and finds a friend request
// But this is not a concurrent operation and thus `updatePendingFriendRequests` is used // But this is not a concurrent operation and thus updatePendingFriendRequests is used
async hasPendingFriendRequests() { async hasPendingFriendRequests() {
// Go through the messages and check for any pending friend requests // Go through the messages and check for any pending friend requests
const messages = await window.Signal.Data.getMessagesByConversation( const messages = await window.Signal.Data.getMessagesByConversation(
@ -261,27 +260,29 @@
MessageCollection: Whisper.MessageCollection, MessageCollection: Whisper.MessageCollection,
} }
); );
const pendingFriendRequest =
for (const message of messages.models) { messages.models.find(message =>
if (message.isFriendRequest() && message.attributes.friendStatus === 'pending') return true; message.isFriendRequest() &&
} message.attributes.friendStatus === 'pending'
);
return false; return pendingFriendRequest !== undefined;
}, },
async getPendingFriendRequests(direction) { async getPendingFriendRequests(direction) {
// Theoretically all ouur messages could be friend requests, thus we have to unfortunately go through each one :( // Theoretically all our messages could be friend requests,
// thus we have to unfortunately go through each one :(
const messages = await window.Signal.Data.getMessagesByConversation( const messages = await window.Signal.Data.getMessagesByConversation(
this.id, this.id,
{ {
type: 'friend-request', type: 'friend-request',
MessageCollection: Whisper.MessageCollection, MessageCollection: Whisper.MessageCollection,
} }
); );
// Get the messages that are matching the direction and the friendStatus // Get the messages that are matching the direction and the friendStatus
return messages.models.filter(m => { return messages.models.filter(m =>
return (m.attributes.direction === direction && m.attributes.friendStatus === 'pending') m.attributes.direction === direction &&
}); m.attributes.friendStatus === 'pending'
);
}, },
getPropsForListItem() { getPropsForListItem() {
const result = { const result = {
@ -364,7 +365,7 @@
if (!this.isPrivate()) { if (!this.isPrivate()) {
throw new Error( throw new Error(
'You cannot verify a group conversation. ' + 'You cannot verify a group conversation. ' +
'You must verify individual contacts.' 'You must verify individual contacts.'
); );
} }
@ -482,7 +483,9 @@
}, },
async onFriendRequestAccepted({ updateUnread }) { async onFriendRequestAccepted({ updateUnread }) {
// Make sure we don't keep incrementing the unread count // Make sure we don't keep incrementing the unread count
const unreadCount = this.isKeyExchangeCompleted() || !updateUnread ? {} : { unreadCount: this.get('unreadCount') + 1 }; const unreadCount = !updateUnread || this.isKeyExchangeCompleted()
? {}
: { unreadCount: this.get('unreadCount') + 1 };
this.set({ this.set({
friendRequestStatus: null, friendRequestStatus: null,
keyExchangeCompleted: true, keyExchangeCompleted: true,
@ -541,7 +544,7 @@
friendRequestStatus.allowSending = false; friendRequestStatus.allowSending = false;
const delayMs = 60 * 60 * 1000 * friendRequestLockDuration; const delayMs = 60 * 60 * 1000 * friendRequestLockDuration;
friendRequestStatus.unlockTimestamp = Date.now() + delayMs; friendRequestStatus.unlockTimestamp = Date.now() + delayMs;
// Update the text input state // Update the text input state
this.updateTextInputState(); this.updateTextInputState();
@ -593,7 +596,7 @@
if (!this.isPrivate()) { if (!this.isPrivate()) {
throw new Error( throw new Error(
'You cannot set a group conversation as trusted. ' + 'You cannot set a group conversation as trusted. ' +
'You must set individual contacts as trusted.' 'You must set individual contacts as trusted.'
); );
} }
@ -751,17 +754,16 @@
// This is to ensure that one user cannot spam us with multiple friend requests // This is to ensure that one user cannot spam us with multiple friend requests
if (_options.direction === 'incoming') { if (_options.direction === 'incoming') {
const requests = await this.getPendingFriendRequests('incoming'); const requests = await this.getPendingFriendRequests('incoming');
for (const request of requests) { // Delete the old message if it's pending
// Delete the old message if it's pending await Promise.all(requests.map(request => this._removeMessage(request.id)));
await this._removeMessage(request.id);
}
// Trigger an update if we removed messages // Trigger an update if we removed messages
if (requests.length > 0) if (requests.length > 0)
this.trigger('change'); this.trigger('change');
} }
// Add the new message // Add the new message
// eslint-disable-next-line camelcase
const received_at = _options.received_at || Date.now(); const received_at = _options.received_at || Date.now();
const message = { const message = {
conversationId: this.id, conversationId: this.id,
@ -783,11 +785,11 @@
Message: Whisper.Message, Message: Whisper.Message,
}); });
const whisperMessage = new Whisper.Message({ const whisperMessage = new Whisper.Message({
...message, ...message,
id, id,
}); });
this.trigger('newmessage', whisperMessage); this.trigger('newmessage', whisperMessage);
this.notify(whisperMessage); this.notify(whisperMessage);
}, },
@ -991,9 +993,9 @@
fileName: fileName || null, fileName: fileName || null,
thumbnail: thumbnail thumbnail: thumbnail
? { ? {
...(await loadAttachmentData(thumbnail)), ...(await loadAttachmentData(thumbnail)),
objectUrl: getAbsoluteAttachmentPath(thumbnail.path), objectUrl: getAbsoluteAttachmentPath(thumbnail.path),
} }
: null, : null,
}; };
}) })
@ -1020,7 +1022,7 @@
'with timestamp', 'with timestamp',
now now
); );
let messageWithSchema = null; let messageWithSchema = null;
// If we have exchanged keys then let the user send the message normally // If we have exchanged keys then let the user send the message normally
@ -1037,26 +1039,31 @@
recipients, recipients,
}); });
} else { } else {
// We also need to make sure we don't send a new friend request if we already have an existing one // We also need to make sure we don't send a new friend request
const incomingRequests = await this.getPendingFriendRequests('incoming'); // if we already have an existing one
if (incomingRequests.length > 0) return; const incomingRequests = await this.getPendingFriendRequests('incoming');
if (incomingRequests.length > 0) return null;
// Otherwise check if we have sent a friend request // Otherwise check if we have sent a friend request
const outgoingRequests = await this.getPendingFriendRequests('outgoing'); const outgoingRequests = await this.getPendingFriendRequests('outgoing');
if (outgoingRequests.length > 0) { if (outgoingRequests.length > 0) {
// Check if the requests have errored, if so then remove them and send the new request if possible // Check if the requests have errored, if so then remove them
const friendRequestSent = false; // and send the new request if possible
for (const outgoing of outgoingRequests) { let friendRequestSent = false;
const promises = [];
outgoingRequests.forEach(outgoing => {
if (outgoing.hasErrors()) { if (outgoing.hasErrors()) {
await this._removeMessage(outgoing.id); promises.push(this._removeMessage(outgoing.id));
} else { } else {
// No errors = we have sent over the friend request // No errors = we have sent over the friend request
friendRequestSent = true; friendRequestSent = true;
} }
} });
await Promise.all(promises);
// If the requests didn't error then don't add a new friend request because one of them was sent successfully // If the requests didn't error then don't add a new friend request
if (friendRequestSent) return; // because one of them was sent successfully
if (friendRequestSent) return null;
} }
// Send the friend request! // Send the friend request!
@ -1127,8 +1134,8 @@
const options = this.getSendOptions(); const options = this.getSendOptions();
// Add the message sending on another queue so that our UI doesn't get blocked // Add the message sending on another queue so that our UI doesn't get blocked
this.queueMessageSend(async () => { this.queueMessageSend(async () =>
return message.send( message.send(
this.wrapSend( this.wrapSend(
sendFunction( sendFunction(
destination, destination,
@ -1141,8 +1148,8 @@
options options
) )
) )
); )
}); );
return true; return true;
}); });
@ -1161,12 +1168,11 @@
this.trigger('disable:input', true); this.trigger('disable:input', true);
this.trigger('change:placeholder', 'disabled'); this.trigger('change:placeholder', 'disabled');
return; return;
} else {
// Tell the user to introduce themselves
this.trigger('disable:input', false);
this.trigger('change:placeholder', 'friend-request');
return;
} }
// Tell the user to introduce themselves
this.trigger('disable:input', false);
this.trigger('change:placeholder', 'friend-request');
return;
} }
this.trigger('disable:input', false); this.trigger('disable:input', false);
this.trigger('change:placeholder', 'chat'); this.trigger('change:placeholder', 'chat');
@ -1316,8 +1322,8 @@
accessKey && sealedSender === SEALED_SENDER.ENABLED accessKey && sealedSender === SEALED_SENDER.ENABLED
? accessKey ? accessKey
: window.Signal.Crypto.arrayBufferToBase64( : window.Signal.Crypto.arrayBufferToBase64(
window.Signal.Crypto.getRandomBytes(16) window.Signal.Crypto.getRandomBytes(16)
), ),
}, },
}; };
}, },
@ -1596,7 +1602,7 @@
} else { } else {
window.log.warn( window.log.warn(
'Marked a message as read in the database, but ' + 'Marked a message as read in the database, but ' +
'it was not in messageCollection.' 'it was not in messageCollection.'
); );
} }
@ -2117,8 +2123,10 @@
// Notification for friend request received // Notification for friend request received
async notifyFriendRequest(source, type) { async notifyFriendRequest(source, type) {
// Data validation // Data validation
if (!source) return Promise.reject('Invalid source'); if (!source)
if (!['accepted', 'requested'].includes(type)) return Promise.reject('Type must be accepted or requested.'); throw new Error('Invalid source');
if (!['accepted', 'requested'].includes(type))
throw new Error('Type must be accepted or requested.');
// Call the notification on the right conversation // Call the notification on the right conversation
let conversation = this; let conversation = this;
@ -2128,29 +2136,33 @@
source, source,
'private' 'private'
); );
window.log.info(`Notify called on a different conversation. expected: ${this.id}. actual: ${conversation.id}`); window.log.info(`Notify called on a different conversation.
Expected: ${this.id}. Actual: ${conversation.id}`);
} catch (e) { } catch (e) {
return Promise.reject('Failed to fetch conversation'); throw new Error('Failed to fetch conversation.');
} }
} }
const isTypeAccepted = type === 'accepted'; const isTypeAccepted = type === 'accepted';
const title = isTypeAccepted ? 'friendRequestAcceptedNotificationTitle' : 'friendRequestNotificationTitle'; const title = isTypeAccepted
const message = isTypeAccepted ? 'friendRequestAcceptedNotificationMessage' : 'friendRequestNotificationMessage'; ? 'friendRequestAcceptedNotificationTitle'
: 'friendRequestNotificationTitle';
conversation.getNotificationIcon().then(iconUrl => { const message = isTypeAccepted
window.log.info('Add notification for friend request updated', { ? 'friendRequestAcceptedNotificationMessage'
conversationId: conversation.idForLogging(), : 'friendRequestNotificationMessage';
});
Whisper.Notifications.add({ const iconUrl = await conversation.getNotificationIcon();
conversationId: conversation.id, window.log.info('Add notification for friend request updated', {
iconUrl, conversationId: conversation.idForLogging(),
isExpiringMessage: false, });
message: i18n(message, conversation.getTitle()), Whisper.Notifications.add({
messageSentAt: Date.now(), conversationId: conversation.id,
title: i18n(title), iconUrl,
}); isExpiringMessage: false,
}); message: i18n(message, conversation.getTitle()),
messageSentAt: Date.now(),
title: i18n(title),
});
}, },
}); });

@ -12,7 +12,7 @@
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function () {
'use strict'; 'use strict';
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
@ -548,8 +548,8 @@
contact.number && contact.number[0] && contact.number[0].value; contact.number && contact.number[0] && contact.number[0].value;
const onSendMessage = firstNumber const onSendMessage = firstNumber
? () => { ? () => {
this.trigger('open-conversation', firstNumber); this.trigger('open-conversation', firstNumber);
} }
: null; : null;
const onClick = async () => { const onClick = async () => {
// First let's be sure that the signal account check is complete. // First let's be sure that the signal account check is complete.
@ -588,8 +588,8 @@
!path && !objectUrl !path && !objectUrl
? null ? null
: Object.assign({}, attachment.thumbnail || {}, { : Object.assign({}, attachment.thumbnail || {}, {
objectUrl: path || objectUrl, objectUrl: path || objectUrl,
}); });
return Object.assign({}, attachment, { return Object.assign({}, attachment, {
isVoiceMessage: Signal.Types.Attachment.isVoiceMessage(attachment), isVoiceMessage: Signal.Types.Attachment.isVoiceMessage(attachment),
@ -656,15 +656,15 @@
url: getAbsoluteAttachmentPath(path), url: getAbsoluteAttachmentPath(path),
screenshot: screenshot screenshot: screenshot
? { ? {
...screenshot, ...screenshot,
url: getAbsoluteAttachmentPath(screenshot.path), url: getAbsoluteAttachmentPath(screenshot.path),
} }
: null, : null,
thumbnail: thumbnail thumbnail: thumbnail
? { ? {
...thumbnail, ...thumbnail,
url: getAbsoluteAttachmentPath(thumbnail.path), url: getAbsoluteAttachmentPath(thumbnail.path),
} }
: null, : null,
}; };
}, },
@ -1405,7 +1405,7 @@
if (previousUnread !== message.get('unread')) { if (previousUnread !== message.get('unread')) {
window.log.warn( window.log.warn(
'Caught race condition on new message read state! ' + 'Caught race condition on new message read state! ' +
'Manually starting timers.' 'Manually starting timers.'
); );
// We call markRead() even though the message is already // We call markRead() even though the message is already
// marked read because we need to start expiration // marked read because we need to start expiration

@ -122,6 +122,9 @@ module.exports = {
getMessageCount, getMessageCount,
saveMessage, saveMessage,
cleanSeenMessages,
saveSeenMessageHash,
saveSeenMessageHashes,
saveLegacyMessage, saveLegacyMessage,
saveMessages, saveMessages,
removeMessage, removeMessage,
@ -140,6 +143,7 @@ module.exports = {
getOutgoingWithoutExpiresAt, getOutgoingWithoutExpiresAt,
getNextExpiringMessage, getNextExpiringMessage,
getMessagesByConversation, getMessagesByConversation,
getSeenMessagesByHashList,
getUnprocessedCount, getUnprocessedCount,
getAllUnprocessed, getAllUnprocessed,
@ -728,6 +732,18 @@ async function getMessageCount() {
return channels.getMessageCount(); return channels.getMessageCount();
} }
async function cleanSeenMessages() {
await channels.cleanSeenMessages();
}
async function saveSeenMessageHashes(data) {
await channels.saveSeenMessageHashes(_cleanData(data));
}
async function saveSeenMessageHash(data) {
await channels.saveSeenMessageHash(_cleanData(data));
}
async function saveMessage(data, { forceSave, Message } = {}) { async function saveMessage(data, { forceSave, Message } = {}) {
const updated = keysFromArrayBuffer(MESSAGE_PRE_KEYS, data); const updated = keysFromArrayBuffer(MESSAGE_PRE_KEYS, data);
const id = await channels.saveMessage(_cleanData(updated), { forceSave }); const id = await channels.saveMessage(_cleanData(updated), { forceSave });
@ -861,6 +877,13 @@ async function getMessagesByConversation(
return new MessageCollection(encoded); return new MessageCollection(encoded);
} }
async function getSeenMessagesByHashList(
hashes
) {
const seenMessages = await channels.getSeenMessagesByHashList(hashes);
return seenMessages;
}
async function removeAllMessagesInConversation( async function removeAllMessagesInConversation(
conversationId, conversationId,
{ MessageCollection } { MessageCollection }

@ -1,4 +1,4 @@
/* global log, dcodeIO */ /* global log, dcodeIO, window */
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const is = require('@sindresorhus/is'); const is = require('@sindresorhus/is');

@ -1,7 +1,7 @@
/* global window, dcodeIO, textsecure, StringView */ /* global window, dcodeIO, textsecure, StringView */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function () {
let server; let server;
function stringToArrayBufferBase64(string) { function stringToArrayBufferBase64(string) {
@ -40,6 +40,17 @@
}; };
}; };
const filterIncomingMessages = async function filterIncomingMessages(messages) {
const incomingHashes = messages.map(m => m.hash);
const dupHashes = await window.Signal.Data.getSeenMessagesByHashList(incomingHashes);
const newMessages = messages.filter(m => !dupHashes.includes(m.hash));
const newHashes = newMessages.map(m => ({
expiresAt: m.expiration,
hash: m.hash,
}));
await window.Signal.Data.saveSeenMessageHashes(newHashes);
return newMessages;
};
window.HttpResource = function HttpResource(_server, opts = {}) { window.HttpResource = function HttpResource(_server, opts = {}) {
server = _server; server = _server;
@ -56,7 +67,7 @@
try { try {
result = await server.retrieveMessages(pubKey); result = await server.retrieveMessages(pubKey);
connected = true; connected = true;
} catch(err) { } catch (err) {
connected = false; connected = false;
setTimeout(() => { pollServer(callBack); }, 5000); setTimeout(() => { pollServer(callBack); }, 5000);
return; return;
@ -68,7 +79,8 @@
setTimeout(() => { pollServer(callBack); }, 5000); setTimeout(() => { pollServer(callBack); }, 5000);
return; return;
} }
result.messages.forEach(async message => { const newMessages = await filterIncomingMessages(result.messages);
newMessages.forEach(async message => {
const { data } = message; const { data } = message;
const dataPlaintext = stringToArrayBufferBase64(data); const dataPlaintext = stringToArrayBufferBase64(data);
const messageBuf = textsecure.protobuf.WebSocketMessage.decode(dataPlaintext); const messageBuf = textsecure.protobuf.WebSocketMessage.decode(dataPlaintext);

@ -1,6 +1,7 @@
/* global window: false */ /* global window: false */
/* global textsecure: false */ /* global textsecure: false */
/* global StringView: false */ /* global StringView: false */
/* global libloki: false */
/* global libsignal: false */ /* global libsignal: false */
/* global WebSocket: false */ /* global WebSocket: false */
/* global Event: false */ /* global Event: false */
@ -10,8 +11,10 @@
/* global ContactBuffer: false */ /* global ContactBuffer: false */
/* global GroupBuffer: false */ /* global GroupBuffer: false */
/* global Worker: false */ /* global Worker: false */
/* global WebSocketResource: false */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
/* eslint-disable no-unreachable */
const WORKER_TIMEOUT = 60 * 1000; // one minute const WORKER_TIMEOUT = 60 * 1000; // one minute
@ -251,27 +254,26 @@ MessageReceiver.prototype.extend({
this.calledClose this.calledClose
); );
// TODO: handle properly // TODO: handle properly
return; // this.shutdown();
this.shutdown();
// if (this.calledClose) {
if (this.calledClose) { // return Promise.resolve();
return Promise.resolve(); // }
} // if (ev.code === 3000) {
if (ev.code === 3000) { // return Promise.resolve();
return Promise.resolve(); // }
} // if (ev.code === 3001) {
if (ev.code === 3001) { // this.onEmpty();
this.onEmpty(); // }
} // // possible 403 or network issue. Make an request to confirm
// possible 403 or network issue. Make an request to confirm // return this.server
return this.server // .getDevices(this.number)
.getDevices(this.number) // .then(this.connect.bind(this)) // No HTTP error? Reconnect
.then(this.connect.bind(this)) // No HTTP error? Reconnect // .catch(e => {
.catch(e => { // const event = new Event('error');
const event = new Event('error'); // event.error = e;
event.error = e; // return this.dispatchAndWait(event);
return this.dispatchAndWait(event); // });
});
}, },
handleRequest(request) { handleRequest(request) {
this.incoming = this.incoming || []; this.incoming = this.incoming || [];
@ -708,7 +710,7 @@ MessageReceiver.prototype.extend({
promise = sessionCipher.decryptWhisperMessage(ciphertext) promise = sessionCipher.decryptWhisperMessage(ciphertext)
.then(this.unpad); .then(this.unpad);
break; break;
case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: {
window.log.info('friend-request message from ', envelope.source); window.log.info('friend-request message from ', envelope.source);
const fallBackSessionCipher = new libloki.FallBackSessionCipher( const fallBackSessionCipher = new libloki.FallBackSessionCipher(
address address
@ -716,6 +718,7 @@ MessageReceiver.prototype.extend({
promise = fallBackSessionCipher.decrypt(ciphertext.toArrayBuffer()) promise = fallBackSessionCipher.decrypt(ciphertext.toArrayBuffer())
.then(this.unpad); .then(this.unpad);
break; break;
}
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
window.log.info('prekey message from', this.getEnvelopeId(envelope)); window.log.info('prekey message from', this.getEnvelopeId(envelope));
promise = this.decryptPreKeyWhisperMessage( promise = this.decryptPreKeyWhisperMessage(
@ -971,13 +974,13 @@ MessageReceiver.prototype.extend({
if (!message || !message.direction || !message.friendStatus) return; if (!message || !message.direction || !message.friendStatus) return;
// Update the conversation // Update the conversation
const conversation = ConversationController.get(pubKey); const conversation = window.ConversationController.get(pubKey);
if (conversation) { if (conversation) {
// Update the conversation friend request indicator // Update the conversation friend request indicator
conversation.updatePendingFriendRequests(); conversation.updatePendingFriendRequests();
conversation.updateTextInputState(); conversation.updateTextInputState();
} }
// Send our own prekeys as a response // Send our own prekeys as a response
if (message.direction === 'incoming' && message.friendStatus === 'accepted') { if (message.direction === 'incoming' && message.friendStatus === 'accepted') {
libloki.sendEmptyMessageWithPreKeys(pubKey); libloki.sendEmptyMessageWithPreKeys(pubKey);
@ -990,9 +993,9 @@ MessageReceiver.prototype.extend({
); );
} }
await conversation.onFriendRequestAccepted(); await conversation.onFriendRequestAccepted({ updateUnread: false });
} }
console.log(`Friend request for ${pubKey} was ${message.friendStatus}`, message); window.log.info(`Friend request for ${pubKey} was ${message.friendStatus}`, message);
}, },
async innerHandleContentMessage(envelope, plaintext) { async innerHandleContentMessage(envelope, plaintext) {
const content = textsecure.protobuf.Content.decode(plaintext); const content = textsecure.protobuf.Content.decode(plaintext);
@ -1000,15 +1003,17 @@ MessageReceiver.prototype.extend({
if (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) { if (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) {
let conversation; let conversation;
try { try {
conversation = ConversationController.get(envelope.source); conversation = window.ConversationController.get(envelope.source);
} catch (e) { } } catch (e) {
throw new Error('Error getting conversation for message.')
}
// only prompt friend request if there is no conversation yet // only prompt friend request if there is no conversation yet
if (!conversation) { if (!conversation) {
this.promptUserToAcceptFriendRequest( this.promptUserToAcceptFriendRequest(
envelope, envelope,
content.dataMessage.body, content.dataMessage.body,
content.preKeyBundleMessage, content.preKeyBundleMessage
); );
} else { } else {
const keyExchangeComplete = conversation.isKeyExchangeCompleted(); const keyExchangeComplete = conversation.isKeyExchangeCompleted();
@ -1017,7 +1022,8 @@ MessageReceiver.prototype.extend({
// We are certain that other user accepted the friend request IF: // We are certain that other user accepted the friend request IF:
// - The message has a preKeyBundleMessage // - The message has a preKeyBundleMessage
// - We have an outgoing friend request that is pending // - We have an outgoing friend request that is pending
// The second check is crucial because it makes sure we don't save the preKeys of the incoming friend request (which is saved only when we press accept) // The second check is crucial because it makes sure we don't save the preKeys of
// the incoming friend request (which is saved only when we press accept)
if (!keyExchangeComplete && content.preKeyBundleMessage) { if (!keyExchangeComplete && content.preKeyBundleMessage) {
// Check for any outgoing friend requests // Check for any outgoing friend requests
const pending = await conversation.getPendingFriendRequests('outgoing'); const pending = await conversation.getPendingFriendRequests('outgoing');
@ -1040,7 +1046,7 @@ MessageReceiver.prototype.extend({
} }
// Exit early since the friend request reply will be a regular empty message // Exit early since the friend request reply will be a regular empty message
return; return null;
} }
if (content.syncMessage) { if (content.syncMessage) {
@ -1054,9 +1060,7 @@ MessageReceiver.prototype.extend({
} else if (content.receiptMessage) { } else if (content.receiptMessage) {
return this.handleReceiptMessage(envelope, content.receiptMessage); return this.handleReceiptMessage(envelope, content.receiptMessage);
} }
if (!content.preKeyBundleMessage) { throw new Error('Unsupported content message');
throw new Error('Unsupported content message');
}
}, },
handleCallMessage(envelope) { handleCallMessage(envelope) {
window.log.info('call message from', this.getEnvelopeId(envelope)); window.log.info('call message from', this.getEnvelopeId(envelope));
@ -1266,7 +1270,7 @@ MessageReceiver.prototype.extend({
preKeyBundleMessage.signature, preKeyBundleMessage.signature,
].map(k => dcodeIO.ByteBuffer.wrap(k).toArrayBuffer()); ].map(k => dcodeIO.ByteBuffer.wrap(k).toArrayBuffer());
return { return {
...preKeyBundleMessage, ...preKeyBundleMessage,
identityKey, identityKey,
preKey, preKey,
@ -1284,13 +1288,13 @@ MessageReceiver.prototype.extend({
signature, signature,
} = preKeyBundleMessage; } = preKeyBundleMessage;
if (pubKey != StringView.arrayBufferToHex(identityKey)) { if (pubKey !== StringView.arrayBufferToHex(identityKey)) {
throw new Error( throw new Error(
'Error in handlePreKeyBundleMessage: envelope pubkey does not match pubkey in prekey bundle' 'Error in handlePreKeyBundleMessage: envelope pubkey does not match pubkey in prekey bundle'
); );
} }
return await libloki.savePreKeyBundleForNumber({ return libloki.savePreKeyBundleForNumber({
pubKey, pubKey,
preKeyId, preKeyId,
signedKeyId, signedKeyId,
@ -1306,8 +1310,8 @@ MessageReceiver.prototype.extend({
return textsecure.storage.get('blocked-groups', []).indexOf(groupId) >= 0; return textsecure.storage.get('blocked-groups', []).indexOf(groupId) >= 0;
}, },
handleAttachment(attachment) { handleAttachment(attachment) {
console.log("Not handling attachments."); window.log.info('Not handling attachments.');
return; return Promise.reject();
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
attachment.id = attachment.id.toString(); attachment.id = attachment.id.toString();
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign

@ -7,11 +7,10 @@
StringView, StringView,
dcodeIO, dcodeIO,
log, log,
btoa,
_
*/ */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
/* eslint-disable no-unreachable */
function OutgoingMessage( function OutgoingMessage(
server, server,
@ -249,7 +248,8 @@ OutgoingMessage.prototype = {
if (accessKey && !senderCertificate) { if (accessKey && !senderCertificate) {
return Promise.reject( return Promise.reject(
new Error( new Error(
'OutgoingMessage.doSendMessage: accessKey was provided, but senderCertificate was not' 'OutgoingMessage.doSendMessage: accessKey was provided, ' +
'but senderCertificate was not'
) )
); );
} }

Loading…
Cancel
Save