diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7fc3b92b3..e47117dd9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -878,7 +878,7 @@ "message": "Waiting for device to register..." }, "pairNewDevicePrompt": { - "message": "Scan the QR Code on your secondary device" + "message": "Scan the QR Code on your other device" }, "pairedDevices": { "message": "Linked Devices" @@ -2240,7 +2240,7 @@ "message": "Remove" }, "invalidHexId": { - "message": "Invalid Session ID or LNS Name", + "message": "Invalid Session ID", "description": "Error string shown when user types an invalid pubkey hex string" }, "invalidLnsFormat": { @@ -2360,7 +2360,7 @@ "message": "Say hello to your Session ID" }, "allUsersAreRandomly...": { - "message": "Your Session ID is the unique address people can use to contact you on Session. Your Session ID is totally private, anonymous, and has no connection to your real identity." + "message": "Your Session ID is the unique address people can use to contact you on Session. With no connection to your real identity, your Session ID is totally anonymous and private by design." }, "getStarted": { "message": "Get started" @@ -2409,7 +2409,7 @@ "message": "Enter your Session ID below to link this device to your Session ID." }, "enterSessionIDHere": { - "message": "Enter your Session ID here" + "message": "Enter your Session ID" }, "continueYourSession": { "message": "Continue Your Session" @@ -2460,7 +2460,7 @@ "message": "Enter Session ID" }, "pasteSessionIDRecipient": { - "message": "Enter a Session ID or LNS name" + "message": "Enter a Session ID" }, "usersCanShareTheir...": { "message": "Users can share their Session ID from their account settings, or by sharing their QR code." @@ -2604,10 +2604,10 @@ "message": "Secret words" }, "pairingDevice": { - "message": "Pairing Device" + "message": "Linking Device" }, "gotPairingRequest": { - "message": "Pairing request received" + "message": "Linking request received" }, "devicePairedSuccessfully": { "message": "Device linked successfully" diff --git a/js/background.js b/js/background.js index 36741687e..48d63175f 100644 --- a/js/background.js +++ b/js/background.js @@ -1391,10 +1391,11 @@ pubKey ); await window.lokiFileServerAPI.updateOurDeviceMapping(); - // TODO: we should ensure the message was sent and retry automatically if not const device = new libsession.Types.PubKey(pubKey); const unlinkMessage = new libsession.Messages.Outgoing.DeviceUnlinkMessage( - pubKey + { + timestamp: Date.now(), + } ); await libsession.getMessageQueue().send(device, unlinkMessage); diff --git a/js/models/blockedNumbers.js b/js/models/blockedNumbers.js index b4604c2fc..cb24c6466 100644 --- a/js/models/blockedNumbers.js +++ b/js/models/blockedNumbers.js @@ -47,15 +47,6 @@ return _.include(groupIds, groupId); }; - storage.addBlockedGroup = groupId => { - const groupIds = storage.get(BLOCKED_GROUPS_ID, []); - if (_.include(groupIds, groupId)) { - return; - } - - window.log.info(`adding groupId(${groupId}) to blocked list`); - storage.put(BLOCKED_GROUPS_ID, groupIds.concat(groupId)); - }; storage.removeBlockedGroup = groupId => { const groupIds = storage.get(BLOCKED_GROUPS_ID, []); if (!_.include(groupIds, groupId)) { diff --git a/js/models/conversations.js b/js/models/conversations.js index 6611f9578..fa8219d3b 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -250,9 +250,14 @@ ? BlockedNumberController.block(this.id) : BlockedNumberController.blockGroup(this.id); await promise; - this.trigger('change'); + this.trigger('change', this); this.messageCollection.forEach(m => m.trigger('change')); this.updateTextInputState(); + if (this.isPrivate()) { + await textsecure.messaging.sendContactSyncMessage([this]); + } else { + await textsecure.messaging.sendGroupSyncMessage([this]); + } }, async unblock() { if (!this.id || this.isPublic() || this.isRss()) { @@ -262,9 +267,14 @@ ? BlockedNumberController.unblock(this.id) : BlockedNumberController.unblockGroup(this.id); await promise; - this.trigger('change'); + this.trigger('change', this); this.messageCollection.forEach(m => m.trigger('change')); this.updateTextInputState(); + if (this.isPrivate()) { + await textsecure.messaging.sendContactSyncMessage([this]); + } else { + await textsecure.messaging.sendGroupSyncMessage([this]); + } }, setMessageSelectionBackdrop() { const messageSelected = this.selectedMessages.size > 0; @@ -1382,7 +1392,7 @@ const groupInvitMessage = new libsession.Messages.Outgoing.GroupInvitationMessage( { identifier: id, - + timestamp: Date.now(), serverName: groupInvitation.name, channelId: groupInvitation.channelId, serverAddress: groupInvitation.address, @@ -2720,7 +2730,7 @@ const ourConversation = window.ConversationController.get(ourNumber); let profileKey = null; if (this.get('profileSharing')) { - profileKey = storage.get('profileKey'); + profileKey = new Uint8Array(storage.get('profileKey')); } const avatarPointer = ourConversation.get('avatarPointer'); const { displayName } = ourConversation.getLokiProfile(); diff --git a/js/models/messages.js b/js/models/messages.js index 159ae9479..15abc5ca2 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1087,8 +1087,10 @@ } const { body, attachments, preview, quote } = await this.uploadData(); + const ourNumber = window.storage.get('primaryDevicePubKey'); + const ourConversation = window.ConversationController.get(ourNumber); - const chatMessage = new libsession.Messages.Outgoing.ChatMessage({ + const chatParams = { identifier: this.id, body, timestamp: this.get('sent_at'), @@ -1096,8 +1098,14 @@ attachments, preview, quote, - lokiProfile: this.conversation.getOurProfile(), - }); + }; + if (ourConversation) { + chatParams.lokiProfile = ourConversation.getOurProfile(); + } + + const chatMessage = new libsession.Messages.Outgoing.ChatMessage( + chatParams + ); // Special-case the self-send case - we send only a sync message if (recipients.length === 1) { diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 3c7d9b7f7..6f7a81652 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -96,8 +96,9 @@ class LokiMessageAPI { // eslint-disable-next-line more/no-then snode = await primitives.firstTrue(promises); } catch (e) { + const snodeStr = snode ? `${snode.ip}:${snode.port}` : 'null'; log.warn( - `loki_message:::sendMessage - ${e.code} ${e.message} to ${pubKey} via ${snode.ip}:${snode.port}` + `loki_message:::sendMessage - ${e.code} ${e.message} to ${pubKey} via snode:${snodeStr}` ); if (e instanceof textsecure.WrongDifficultyError) { // Force nonce recalculation diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index d139cbed6..e49d28380 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -289,6 +289,7 @@ window.storage.get('primaryDevicePubKey') ), isKickedFromGroup: this.model.get('isKickedFromGroup'), + isBlocked: this.model.isBlocked(), timerOptions: Whisper.ExpirationTimerOptions.map(item => ({ name: item.getName(), diff --git a/libtextsecure/account_manager.js b/libtextsecure/account_manager.js index e39ca1984..46ec46f7a 100644 --- a/libtextsecure/account_manager.js +++ b/libtextsecure/account_manager.js @@ -553,7 +553,7 @@ timestamp: Date.now(), primaryDevicePubKey, secondaryDevicePubKey: ourPubKey, - requestSignature, + requestSignature: new Uint8Array(requestSignature), } ); await window.libsession @@ -616,14 +616,7 @@ ); // We need to send the our profile to the secondary device - const { displayName } = ourConversation.getLokiProfile(); - const avatarPointer = ourConversation.get('avatarPointer'); - const profileKey = window.storage.get('profileKey'); - const lokiProfile = { - displayName, - profileKey, - avatarPointer, - }; + const lokiProfile = ourConversation.getOurProfile(); // Try to upload to the file server and then send a message try { @@ -631,7 +624,10 @@ const requestPairingMessage = new libsession.Messages.Outgoing.DeviceLinkGrantMessage( { timestamp: Date.now(), - ...authorisation, + primaryDevicePubKey: ourPubKey, + secondaryDevicePubKey: secondaryDeviceStr, + requestSignature: new Uint8Array(requestSignature), + grantSignature: new Uint8Array(grantSignature), lokiProfile, } ); @@ -657,7 +653,7 @@ const conversations = window.getConversations().models; await textsecure.messaging.sendGroupSyncMessage(conversations); await textsecure.messaging.sendOpenGroupsSyncMessage(conversations); - await textsecure.messaging.sendContactSyncMessage(conversations); + await textsecure.messaging.sendContactSyncMessage(); }, 5000); }, validatePubKeyHex(pubKey) { diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js deleted file mode 100644 index 49f47d0fb..000000000 --- a/libtextsecure/outgoing_message.js +++ /dev/null @@ -1,535 +0,0 @@ -/* global - textsecure, - libsignal, - window, - libloki, - StringView, - lokiMessageAPI, -*/ - -/* eslint-disable more/no-then */ -/* eslint-disable no-unreachable */ -const NUM_SEND_CONNECTIONS = 3; - -const getTTLForType = type => { - switch (type) { - case 'device-unpairing': - return 4 * 24 * 60 * 60 * 1000; // 4 days for device unpairing - case 'onlineBroadcast': - return 60 * 1000; // 1 minute for online broadcast message - case 'pairing-request': - return 2 * 60 * 1000; // 2 minutes for pairing requests - default: - return 24 * 60 * 60 * 1000; // 1 day default for any other message - } -}; - -function _getPaddedMessageLength(messageLength) { - const messageLengthWithTerminator = messageLength + 1; - let messagePartCount = Math.floor(messageLengthWithTerminator / 160); - - if (messageLengthWithTerminator % 160 !== 0) { - messagePartCount += 1; - } - - return messagePartCount * 160; -} - -function _convertMessageToText(messageBuffer) { - const plaintext = new Uint8Array( - _getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 - ); - plaintext.set(new Uint8Array(messageBuffer)); - plaintext[messageBuffer.byteLength] = 0x80; - - return plaintext; -} - -function _getPlaintext(messageBuffer) { - return _convertMessageToText(messageBuffer); -} - -function wrapInWebsocketMessage(outgoingObject, timestamp) { - const source = - outgoingObject.type === - textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER - ? null - : outgoingObject.ourKey; - - const messageEnvelope = new textsecure.protobuf.Envelope({ - type: outgoingObject.type, - source, - sourceDevice: outgoingObject.sourceDevice, - timestamp, - content: outgoingObject.content, - }); - const requestMessage = new textsecure.protobuf.WebSocketRequestMessage({ - id: new Uint8Array(libsignal.crypto.getRandomBytes(1))[0], // random ID for now - verb: 'PUT', - path: '/api/v1/message', - body: messageEnvelope.encode().toArrayBuffer(), - }); - const websocketMessage = new textsecure.protobuf.WebSocketMessage({ - type: textsecure.protobuf.WebSocketMessage.Type.REQUEST, - request: requestMessage, - }); - const bytes = new Uint8Array(websocketMessage.encode().toArrayBuffer()); - return bytes; -} - -function getStaleDeviceIdsForNumber(number) { - return textsecure.storage.protocol.getDeviceIds(number).then(deviceIds => { - if (deviceIds.length === 0) { - return [1]; - } - const updateDevices = []; - return Promise.all( - deviceIds.map(deviceId => { - const address = new libsignal.SignalProtocolAddress(number, deviceId); - const sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address - ); - return sessionCipher.hasOpenSession().then(hasSession => { - if (!hasSession) { - updateDevices.push(deviceId); - } - }); - }) - ).then(() => updateDevices); - }); -} - -function OutgoingMessage( - server, - timestamp, - numbers, - message, - silent, - callback, - options = {} -) { - if (message instanceof textsecure.protobuf.DataMessage) { - const content = new textsecure.protobuf.Content(); - content.dataMessage = message; - // eslint-disable-next-line no-param-reassign - message = content; - } - this.server = server; - this.timestamp = timestamp; - this.numbers = numbers; - this.message = message; // ContentMessage proto - this.callback = callback; - this.silent = silent; - - this.numbersCompleted = 0; - this.errors = []; - this.successfulNumbers = []; - this.fallBackEncryption = false; - this.failoverNumbers = []; - this.unidentifiedDeliveries = []; - - const { - numberInfo, - senderCertificate, - online, - messageType, - isPublic, - isMediumGroup, - publicSendData, - autoSession, - } = options || {}; - this.numberInfo = numberInfo; - this.isPublic = isPublic; - this.isMediumGroup = !!isMediumGroup; - this.isGroup = !!( - this.message && - this.message.dataMessage && - this.message.dataMessage.group - ); - this.publicSendData = publicSendData; - this.senderCertificate = senderCertificate; - this.online = online; - this.messageType = messageType || 'outgoing'; - this.autoSession = autoSession || false; -} - -OutgoingMessage.prototype = { - constructor: OutgoingMessage, - numberCompleted() { - this.numbersCompleted += 1; - if (this.numbersCompleted >= this.numbers.length) { - this.callback({ - successfulNumbers: this.successfulNumbers, - failoverNumbers: this.failoverNumbers, - errors: this.errors, - unidentifiedDeliveries: this.unidentifiedDeliveries, - messageType: this.messageType, - }); - } - }, - registerError(number, reason, error) { - if (!error || (error.name === 'HTTPError' && error.code !== 404)) { - // eslint-disable-next-line no-param-reassign - error = new textsecure.OutgoingMessageError( - number, - this.message.toArrayBuffer(), - this.timestamp, - error - ); - } - - // eslint-disable-next-line no-param-reassign - error.number = number; - // eslint-disable-next-line no-param-reassign - error.reason = reason; - this.errors[this.errors.length] = error; - this.numberCompleted(); - }, - reloadDevicesAndSend(primaryPubKey, multiDevice = true) { - const ourNumber = textsecure.storage.user.getNumber(); - - if (!multiDevice) { - if (primaryPubKey === ourNumber) { - return Promise.resolve(); - } - - return this.doSendMessage(primaryPubKey, [primaryPubKey]); - } - - return ( - window.libsession.Protocols.MultiDeviceProtocol.getAllDevices( - primaryPubKey - ) - // Don't send to ourselves - .then(devicesPubKeys => - devicesPubKeys.filter(pubKey => pubKey.key !== ourNumber) - ) - .then(devicesPubKeys => { - if (devicesPubKeys.length === 0) { - // No need to start the sending of message without a recipient - return Promise.resolve(); - } - return this.doSendMessage(primaryPubKey, devicesPubKeys); - }) - ); - }, - - getKeysForNumber(number, updateDevices) { - const handleResult = response => - Promise.all( - response.devices.map(device => { - // eslint-disable-next-line no-param-reassign - device.identityKey = response.identityKey; - if ( - updateDevices === undefined || - updateDevices.indexOf(device.deviceId) > -1 - ) { - const address = new libsignal.SignalProtocolAddress( - number, - device.deviceId - ); - const builder = new libsignal.SessionBuilder( - textsecure.storage.protocol, - address - ); - if (device.registrationId === 0) { - window.log.info('device registrationId 0!'); - } - return builder - .processPreKey(device) - .then(async () => { - // TODO: only remove the keys that were used above! - await libloki.storage.removeContactPreKeyBundle(number); - return true; - }) - .catch(error => { - if (error.message === 'Identity key changed') { - // eslint-disable-next-line no-param-reassign - error.timestamp = this.timestamp; - // eslint-disable-next-line no-param-reassign - error.originalMessage = this.message.toArrayBuffer(); - // eslint-disable-next-line no-param-reassign - error.identityKey = device.identityKey; - } - throw error; - }); - } - - return false; - }) - ); - let promise = Promise.resolve(true); - updateDevices.forEach(device => { - promise = promise.then(() => - Promise.all([ - textsecure.storage.protocol.loadContactPreKey(number), - textsecure.storage.protocol.loadContactSignedPreKey(number), - ]) - .then(keys => { - const [preKey, signedPreKey] = keys; - if (preKey === undefined || signedPreKey === undefined) { - return false; - } - const identityKey = StringView.hexToArrayBuffer(number); - return handleResult({ - identityKey, - devices: [ - { deviceId: device, preKey, signedPreKey, registrationId: 0 }, - ], - }).then(results => results.every(value => value === true)); - }) - .catch(e => { - throw e; - }) - ); - }); - - return promise; - }, - - // Default ttl to 24 hours if no value provided - async transmitMessage(number, data, timestamp, ttl = 24 * 60 * 60 * 1000) { - const pubKey = number; - - try { - // TODO: Make NUM_CONCURRENT_CONNECTIONS a global constant - const options = { - numConnections: NUM_SEND_CONNECTIONS, - }; - options.isPublic = this.isPublic; - if (this.isPublic) { - options.publicSendData = this.publicSendData; - } - await lokiMessageAPI.sendMessage(pubKey, data, timestamp, ttl, options); - } catch (e) { - if (e.name === 'HTTPError' && e.code !== 409 && e.code !== 410) { - throw new textsecure.SendMessageNetworkError(number, '', e, timestamp); - } else if (e.name === 'TimedOutError') { - throw new textsecure.PoWError(number, e); - } - throw e; - } - }, - - async buildMessage(devicePubKey) { - const updatedDevices = await getStaleDeviceIdsForNumber(devicePubKey); - const keysFound = await this.getKeysForNumber(devicePubKey, updatedDevices); - - // Check if we need to attach the preKeys - const enableFallBackEncryption = !keysFound; - const flags = this.message.dataMessage - ? this.message.dataMessage.get_flags() - : null; - // END_SESSION means Session reset message - const isEndSession = - flags === textsecure.protobuf.DataMessage.Flags.END_SESSION; - const isSessionRequest = false; - - if (enableFallBackEncryption || isEndSession) { - // Encrypt them with the fallback - const pkb = await libloki.storage.getPreKeyBundleForContact(devicePubKey); - this.message.preKeyBundleMessage = new textsecure.protobuf.PreKeyBundleMessage( - pkb - ); - window.log.info('attaching prekeys to outgoing message'); - } - - const messageBuffer = this.message.toArrayBuffer(); - const logDetails = { - message: this.message, - }; - - const ourPubKey = textsecure.storage.user.getNumber(); - const ourPrimaryPubkey = window.storage.get('primaryDevicePubKey'); - const secondaryPubKeys = - (await window.libsession.Protocols.MultiDeviceProtocol.getSecondaryDevices( - ourPubKey - )) || []; - let aliasedPubkey = devicePubKey; - if (devicePubKey === ourPubKey) { - aliasedPubkey = 'OUR_PUBKEY'; // should not happen - } else if (devicePubKey === ourPrimaryPubkey) { - aliasedPubkey = 'OUR_PRIMARY_PUBKEY'; - } else if (secondaryPubKeys.some(device => device.key === devicePubKey)) { - aliasedPubkey = 'OUR SECONDARY PUBKEY'; - } - libloki.api.debug.logSessionMessageSending( - `Sending :${this.messageType} message to ${aliasedPubkey} details:`, - logDetails - ); - - const plaintext = _getPlaintext(messageBuffer); - - // No limit on message keys if we're communicating with our other devices - // FIXME options not used at all; if (ourPubkey === number) { - // options.messageKeysLimit = false; - // } - const ttl = getTTLForType(this.messageType); - const ourKey = textsecure.storage.user.getNumber(); - - return { - ttl, - ourKey, - sourceDevice: 1, - plaintext, - pubKey: devicePubKey, - isSessionRequest, - enableFallBackEncryption, - }; - }, - - async encryptMessage(clearMessage) { - if (clearMessage === null) { - window.log.warn( - 'clearMessage is null on encryptMessage... Returning null' - ); - return null; - } - const { - ttl, - ourKey, - sourceDevice, - plaintext, - pubKey, - isSessionRequest, - enableFallBackEncryption, - } = clearMessage; - // Session doesn't use the deviceId scheme, it's always 1. - // Instead, there are multiple device public keys. - const deviceId = 1; - - const address = new libsignal.SignalProtocolAddress(pubKey, deviceId); - - let sessionCipher; - - if (enableFallBackEncryption) { - sessionCipher = new libloki.crypto.FallBackSessionCipher(address); - } else { - sessionCipher = new libsignal.SessionCipher( - textsecure.storage.protocol, - address - ); - } - - const innerCiphertext = await sessionCipher.encrypt(plaintext); - - const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher( - textsecure.storage.protocol - ); - - const senderCert = new textsecure.protobuf.SenderCertificate(); - - senderCert.sender = ourKey; - senderCert.senderDevice = deviceId; - - const ciphertext = await secretSessionCipher.encrypt( - address.getName(), - senderCert, - innerCiphertext - ); - const type = textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER; - const content = window.Signal.Crypto.arrayBufferToBase64(ciphertext); - - return { - type, - ttl, - ourKey, - sourceDevice, - content, - pubKey, - isSessionRequest, - }; - }, - // Send a message to a public group - async sendPublicMessage(number) { - await this.transmitMessage( - number, - this.message.dataMessage, - this.timestamp, - 0 // ttl - ); - - this.successfulNumbers[this.successfulNumbers.length] = number; - this.numberCompleted(); - }, - // Send a message to a private group member or a session chat (one to one) - async sendSessionMessage(outgoingObjects) { - // TODO: handle multiple devices/messages per transmit - const promises = outgoingObjects.map(async outgoingObject => { - if (!outgoingObject) { - return; - } - const { pubKey: destination, ttl } = outgoingObject; - - try { - const socketMessage = wrapInWebsocketMessage( - outgoingObject, - this.timestamp - ); - await this.transmitMessage( - destination, - socketMessage, - this.timestamp, - ttl - ); - this.successfulNumbers.push(destination); - } catch (e) { - e.number = destination; - this.errors.push(e); - } - }); - - await Promise.all(promises); - - this.numbersCompleted += this.successfulNumbers.length; - this.numberCompleted(); - }, - async buildAndEncrypt(devicePubKey) { - const clearMessage = await this.buildMessage(devicePubKey); - return this.encryptMessage(clearMessage); - }, - // eslint-disable-next-line no-unused-vars - async doSendMessage(primaryPubKey, devicesPubKeys) { - if (this.isPublic) { - await this.sendPublicMessage(primaryPubKey); - return; - } - this.numbers = devicesPubKeys; - - if (this.isMediumGroup) { - await this.sendMediumGroupMessage(primaryPubKey); - return; - } - - const outgoingObjects = await Promise.all( - devicesPubKeys.map(pk => this.buildAndEncrypt(pk, primaryPubKey)) - ); - - this.sendSessionMessage(outgoingObjects); - }, - - sendToNumber(number, multiDevice = true) { - return this.reloadDevicesAndSend(number, multiDevice).catch(error => { - if (error.message === 'Identity key changed') { - // eslint-disable-next-line no-param-reassign - error = new textsecure.OutgoingIdentityKeyError( - number, - error.originalMessage, - error.timestamp, - error.identityKey - ); - this.registerError(number, 'Identity key changed', error); - } else { - this.registerError( - number, - `Failed to retrieve new device keys for number ${number}`, - error - ); - } - }); - }, -}; - -window.textsecure = window.textsecure || {}; -window.textsecure.OutgoingMessage = OutgoingMessage; diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 1b45085e2..f31114f99 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -360,8 +360,13 @@ MessageSender.prototype = { }); }, - async sendContactSyncMessage() { - const convosToSync = await libsession.Utils.SyncMessageUtils.getSyncContacts(); + async sendContactSyncMessage(convos) { + let convosToSync; + if (!convos) { + convosToSync = await libsession.Utils.SyncMessageUtils.getSyncContacts(); + } else { + convosToSync = convos; + } if (convosToSync.size === 0) { window.console.info('No contacts to sync.'); @@ -397,11 +402,7 @@ MessageSender.prototype = { } // We only want to sync across closed groups that we haven't left const sessionGroups = conversations.filter( - c => - c.isClosedGroup() && - !c.get('left') && - !c.isBlocked() && - !c.isMediumGroup() + c => c.isClosedGroup() && !c.get('left') && !c.isMediumGroup() ); if (sessionGroups.length === 0) { window.console.info('No closed group to sync.'); @@ -458,6 +459,7 @@ MessageSender.prototype = { if (myDevice !== 1 && myDevice !== '1') { const syncReadMessages = new libsession.Messages.Outgoing.SyncReadMessage( { + timestamp: Date.now(), readMessages: reads, } ); diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 479cdb73c..49a46a8f5 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -9,11 +9,11 @@ message Envelope { UNKNOWN = 0; CIPHERTEXT = 1; KEY_EXCHANGE = 2; - PREKEY_BUNDLE = 3; //Used By Signal. DO NOT TOUCH! we don't use this at all. + PREKEY_BUNDLE = 3; //This field is used by Signal. DO NOT TOUCH!. RECEIPT = 5; UNIDENTIFIED_SENDER = 6; MEDIUM_GROUP_CIPHERTEXT = 7; - FALLBACK_MESSAGE = 101; // contains prekeys and is using simple encryption + FALLBACK_MESSAGE = 101; // Custom Encryption for when we don't have a session or we need to establish a session } optional Type type = 1; @@ -35,7 +35,7 @@ message Content { optional NullMessage nullMessage = 4; optional ReceiptMessage receiptMessage = 5; optional TypingMessage typingMessage = 6; - optional PreKeyBundleMessage preKeyBundleMessage = 101; + optional PreKeyBundleMessage preKeyBundleMessage = 101; // The presence of this indicated that we want to establish a new session (Session Request) optional LokiAddressMessage lokiAddressMessage = 102; optional PairingAuthorisationMessage pairingAuthorisation = 103; } diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 6b0e38c39..1aaf0643f 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -5,7 +5,6 @@ .panel, .panel-wrapper { - height: calc(100% - #{$header-height}); overflow-y: scroll; } @@ -22,9 +21,16 @@ .panel-wrapper { display: flex; flex-direction: column; + flex-grow: 1; overflow: initial; } + .conversation-content-left { + display: flex; + flex-direction: column; + flex-grow: 1; + } + .main.panel { .discussion-container { flex-grow: 1; diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 79fcac46e..e92145d17 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -1350,6 +1350,16 @@ label { overflow: hidden; user-select: all; overflow-y: auto; + padding: 0px 5px 20px 5px; + + &.session-id-editable-textarea:placeholder-shown { + padding: 20px 5px 0px 5px; + } + + &.group-id-editable-textarea { + margin-top: 15px; + white-space: nowrap; + } } input { @@ -1594,6 +1604,10 @@ input { } } .create-group-name-input { + display: flex; + justify-content: center; + width: 100%; + .session-id-editable { height: 60px !important; diff --git a/stylesheets/_session_left_pane.scss b/stylesheets/_session_left_pane.scss index b45426787..ab1341ba7 100644 --- a/stylesheets/_session_left_pane.scss +++ b/stylesheets/_session_left_pane.scss @@ -238,9 +238,11 @@ $session-compose-margin: 20px; box-shadow: 0 0 100px 0 rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; + flex-grow: 1; align-items: center; overflow-y: auto; overflow-x: hidden; + .session-icon .exit { padding: 13px; } @@ -307,6 +309,8 @@ $session-compose-margin: 20px; } .session-id-editable { + width: 90%; + textarea::-webkit-inner-spin-button { margin: 0px 20px; width: -webkit-fill-available; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 6abcbe9d0..72254409c 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -11,7 +11,6 @@ import { ContactName } from './conversation/ContactName'; import { TypingAnimation } from './conversation/TypingAnimation'; import { Colors, LocalizerType } from '../types/Util'; -import { SessionButton, SessionButtonColor } from './session/SessionButton'; export type PropsData = { id: string; @@ -181,11 +180,11 @@ export class ConversationListItem extends React.PureComponent { {!isPublic && !isRss && !isMe ? ( {blockTitle} ) : null} - {!isPublic && !isRss && !isMe ? ( + {/* {!isPublic && !isRss && !isMe ? ( {i18n('changeNickname')} - ) : null} + ) : null} */} {!isPublic && !isRss && !isMe && hasNickname ? ( {i18n('clearNickname')} ) : null} diff --git a/ts/components/DevicePairingDialog.tsx b/ts/components/DevicePairingDialog.tsx index e56bba982..c15604d88 100644 --- a/ts/components/DevicePairingDialog.tsx +++ b/ts/components/DevicePairingDialog.tsx @@ -262,14 +262,6 @@ export class DevicePairingDialog extends React.Component { private requestReceived(secondaryDevicePubKey: string | EventHandlerNonNull) { // FIFO: push at the front of the array with unshift() this.state.pubKeyRequests.unshift(secondaryDevicePubKey); - window.pushToast({ - title: window.i18n('gotPairingRequest'), - description: `${window.shortenPubkey( - secondaryDevicePubKey - )} ${window.i18n( - 'showPairingWordsTitle' - )}: ${window.mnemonic.pubkey_to_secret_words(secondaryDevicePubKey)}`, - }); if (!this.state.currentPubKey) { this.nextPubKey(); this.stopReceivingRequests(); diff --git a/ts/components/EditProfileDialog.tsx b/ts/components/EditProfileDialog.tsx index 54abe0337..0c21c7e9c 100644 --- a/ts/components/EditProfileDialog.tsx +++ b/ts/components/EditProfileDialog.tsx @@ -337,7 +337,7 @@ export class EditProfileDialog extends React.Component { setProfileName: this.state.profileName, }, () => { - // Update settinngs in dialog complete; + // Update settings in dialog complete; // now callback to reloadactions panel avatar this.props.callback(this.state.avatar); } diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index caf1a77f5..8b1cb57ed 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -454,7 +454,7 @@ export class ConversationHeader extends React.Component { const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser'); const blockHandler = isBlocked ? onUnblockUser : onBlockUser; - const disappearingMessagesMenuItem = !isKickedFromGroup && ( + const disappearingMessagesMenuItem = !isKickedFromGroup && !isBlocked && ( {(timerOptions || []).map(item => ( { const resetSessionMenuItem = !isGroup && ( {i18n('resetSession')} ); - const blockHandlerMenuItem = !isMe && !isGroup && !isRss && ( + const blockHandlerMenuItem = !isMe && !isRss && ( {blockTitle} ); diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 296cd02b4..2664b6ba0 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -24,6 +24,7 @@ interface Props { } export class ActionsPanel extends React.Component { + private ourConversation: any; constructor(props: Props) { super(props); this.state = { @@ -31,6 +32,7 @@ export class ActionsPanel extends React.Component { }; this.editProfileHandle = this.editProfileHandle.bind(this); + this.refreshAvatarCallback = this.refreshAvatarCallback.bind(this); } public componentDidMount() { @@ -42,10 +44,36 @@ export class ActionsPanel extends React.Component { this.setState({ avatarPath: conversation.getAvatarPath(), }); + // When our primary device updates its avatar, we will need for a message sync to know about that. + // Once we get the avatar update, we need to refresh this react component. + // So we listen to changes on our profile avatar and use the updated avatarPath (done on message received). + this.ourConversation = conversation; + + this.ourConversation.on( + 'change', + () => { + this.refreshAvatarCallback(this.ourConversation); + }, + 'refreshAvatarCallback' + ); } ); } + public refreshAvatarCallback(conversation: any) { + if (conversation.changed?.profileAvatar) { + this.setState({ + avatarPath: conversation.getAvatarPath(), + }); + } + } + + public componentWillUnmount() { + if (this.ourConversation) { + this.ourConversation.off('change', null, 'refreshAvatarCallback'); + } + } + public Section = ({ isSelected, onSelect, diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index a6ca82b3e..749143d7e 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -196,6 +196,7 @@ export class SessionClosableOverlay extends React.Component { editable={!noContactsForClosedGroup} placeholder={placeholder} value={groupName} + isGroup={true} maxLength={window.CONSTANTS.MAX_GROUPNAME_LENGTH} onChange={this.onGroupNameChanged} onPressEnter={() => onButtonClick(groupName, selectedMembers)} diff --git a/ts/components/session/SessionGroupSettings.tsx b/ts/components/session/SessionGroupSettings.tsx index d5b25fb2d..c7e64f8eb 100644 --- a/ts/components/session/SessionGroupSettings.tsx +++ b/ts/components/session/SessionGroupSettings.tsx @@ -22,6 +22,7 @@ interface Props { isAdmin: boolean; amMod: boolean; isKickedFromGroup: boolean; + isBlocked: boolean; onGoBack: () => void; onInviteContacts: () => void; @@ -215,10 +216,12 @@ export class SessionGroupSettings extends React.Component { isAdmin, isKickedFromGroup, amMod, + isBlocked, } = this.props; const { documents, media, onItemClick } = this.state; const showMemberCount = !!(memberCount && memberCount > 0); - const hasDisappearingMessages = !isPublic && !isKickedFromGroup; + const hasDisappearingMessages = + !isPublic && !isKickedFromGroup && !isBlocked; const leaveGroupString = isPublic ? window.i18n('leaveOpenGroup') : isKickedFromGroup @@ -235,9 +238,11 @@ export class SessionGroupSettings extends React.Component { }); const showUpdateGroupNameButton = - isPublic && !isKickedFromGroup ? amMod : isAdmin; + isPublic && !isKickedFromGroup + ? amMod && !isBlocked + : isAdmin && !isBlocked; const showUpdateGroupMembersButton = - !isPublic && !isKickedFromGroup && isAdmin; + !isPublic && !isKickedFromGroup && !isBlocked && isAdmin; return ( @@ -313,16 +318,18 @@ export class SessionGroupSettings extends React.Component { isAdmin, isPublic, isKickedFromGroup, + isBlocked, } = this.props; - const showInviteContacts = (isPublic || isAdmin) && !isKickedFromGroup; + const showInviteContacts = + (isPublic || isAdmin) && !isKickedFromGroup && !isBlocked; return ( { @@ -28,7 +29,14 @@ export class SessionIdEditable extends React.PureComponent { } public render() { - const { placeholder, editable, text, value, maxLength } = this.props; + const { + placeholder, + editable, + text, + value, + maxLength, + isGroup, + } = this.props; return ( { )} > { id: 'message-ttl', title: window.i18n('messageTTL'), description: window.i18n('messageTTLSettingDescription'), - hidden: false, + // TODO: Revert + // TTL set to 2 days for mobile push notification compabability + // temporary fix .t 13/07/2020 + // + // TODO: Hook up this TTL to message sending when re-enabling. + // This setting is not used in any libsession sending code + hidden: true, type: SessionSettingType.Slider, category: SessionSettingCategory.Privacy, setFn: undefined, @@ -452,7 +459,10 @@ export class SettingsView extends React.Component { step: 6, min: 12, max: 96, - defaultValue: 24, + defaultValue: NumberUtils.msAsUnit( + window.CONSTANTS.TTL_DEFAULT_REGULAR_MESSAGE, + 'hour' + ), info: (value: number) => `${value} Hours`, }, confirmationDialogParams: undefined, diff --git a/ts/receiver/groups.ts b/ts/receiver/groups.ts index a2f4254dc..498cc7cd7 100644 --- a/ts/receiver/groups.ts +++ b/ts/receiver/groups.ts @@ -146,13 +146,7 @@ interface GroupInfo { } export async function onGroupReceived(details: GroupInfo) { - const { - ConversationController, - libloki, - storage, - textsecure, - Whisper, - } = window; + const { ConversationController, libloki, textsecure, Whisper } = window; const { id } = details; @@ -189,12 +183,6 @@ export async function onGroupReceived(details: GroupInfo) { updates.left = true; } - if (details.blocked) { - storage.addBlockedGroup(id); - } else { - storage.removeBlockedGroup(id); - } - conversation.set(updates); // Update the conversation avatar only if new avatar exists and hash differs @@ -210,6 +198,13 @@ export async function onGroupReceived(details: GroupInfo) { ); conversation.set(newAttributes); } + const isBlocked = details.blocked || false; + if (conversation.isClosedGroup()) { + await BlockedNumberController.setGroupBlocked(conversation.id, isBlocked); + } + + conversation.trigger('change', conversation); + conversation.updateTextInputState(); await window.Signal.Data.updateConversation(id, conversation.attributes, { Conversation: Whisper.Conversation, diff --git a/ts/receiver/multidevice.ts b/ts/receiver/multidevice.ts index 5cf786f64..9633c3247 100644 --- a/ts/receiver/multidevice.ts +++ b/ts/receiver/multidevice.ts @@ -12,6 +12,7 @@ import { MultiDeviceProtocol, SessionProtocol } from '../session/protocols'; import { PubKey } from '../session/types'; import ByteBuffer from 'bytebuffer'; +import { BlockedNumberController } from '../util'; async function unpairingRequestIsLegit(source: string, ourPubKey: string) { const { textsecure, storage, lokiFileServerAPI } = window; @@ -287,6 +288,7 @@ export async function handleContacts( await removeFromCache(envelope); } +// tslint:disable-next-line: max-func-body-length async function onContactReceived(details: any) { const { ConversationController, @@ -427,7 +429,15 @@ async function onContactReceived(details: any) { verifiedEvent.viaContactSync = true; await onVerified(verifiedEvent); } - await conversation.trigger('change'); + + const isBlocked = details.blocked || false; + + if (conversation.isPrivate()) { + await BlockedNumberController.setBlocked(conversation.id, isBlocked); + } + conversation.updateTextInputState(); + + await conversation.trigger('change', conversation); } catch (error) { window.log.error('onContactReceived error:', Errors.toLogFormat(error)); } diff --git a/ts/session/constants.ts b/ts/session/constants.ts new file mode 100644 index 000000000..eb08e2103 --- /dev/null +++ b/ts/session/constants.ts @@ -0,0 +1,13 @@ +import { NumberUtils } from './utils'; + +// Default TTL +export const TTL_DEFAULT = { + PAIRING_REQUEST: NumberUtils.timeAsMs(2, 'minutes'), + DEVICE_UNPAIRING: NumberUtils.timeAsMs(4, 'days'), + SESSION_REQUEST: NumberUtils.timeAsMs(4, 'days'), + SESSION_ESTABLISHED: NumberUtils.timeAsMs(2, 'days'), + END_SESSION_MESSAGE: NumberUtils.timeAsMs(4, 'days'), + TYPING_MESSAGE: NumberUtils.timeAsMs(1, 'minute'), + ONLINE_BROADCAST: NumberUtils.timeAsMs(1, 'minute'), + REGULAR_MESSAGE: NumberUtils.timeAsMs(2, 'days'), +}; diff --git a/ts/session/index.ts b/ts/session/index.ts index 6d121dfbf..c2a115733 100644 --- a/ts/session/index.ts +++ b/ts/session/index.ts @@ -3,7 +3,8 @@ import * as Protocols from './protocols'; import * as Types from './types'; import * as Utils from './utils'; import * as Sending from './sending'; +import * as Constants from './constants'; export * from './instance'; -export { Messages, Utils, Protocols, Types, Sending }; +export { Messages, Utils, Protocols, Types, Sending, Constants }; diff --git a/ts/session/messages/outgoing/content/ContentMessage.ts b/ts/session/messages/outgoing/content/ContentMessage.ts index 5eab1bc3e..abe7b97a1 100644 --- a/ts/session/messages/outgoing/content/ContentMessage.ts +++ b/ts/session/messages/outgoing/content/ContentMessage.ts @@ -1,5 +1,6 @@ import { Message } from '../Message'; import { SignalService } from '../../../../protobuf'; +import { Constants } from '../../..'; export abstract class ContentMessage extends Message { public plainTextBuffer(): Uint8Array { @@ -14,7 +15,6 @@ export abstract class ContentMessage extends Message { * this value can be used in all child classes */ protected getDefaultTTL(): number { - // 1 day default for any other message - return 24 * 60 * 60 * 1000; + return Constants.TTL_DEFAULT.REGULAR_MESSAGE; } } diff --git a/ts/session/messages/outgoing/content/EndSessionMessage.ts b/ts/session/messages/outgoing/content/EndSessionMessage.ts index 5368dcf85..79461b775 100644 --- a/ts/session/messages/outgoing/content/EndSessionMessage.ts +++ b/ts/session/messages/outgoing/content/EndSessionMessage.ts @@ -1,9 +1,10 @@ import { SessionRequestMessage } from './SessionRequestMessage'; import { SignalService } from '../../../../protobuf'; +import { Constants } from '../../..'; export class EndSessionMessage extends SessionRequestMessage { public ttl(): number { - return 4 * 24 * 60 * 60 * 1000; // 4 days + return Constants.TTL_DEFAULT.END_SESSION_MESSAGE; } protected contentProto(): SignalService.Content { diff --git a/ts/session/messages/outgoing/content/SessionEstablishedMessage.ts b/ts/session/messages/outgoing/content/SessionEstablishedMessage.ts index c5b19b66a..35a4bfacb 100644 --- a/ts/session/messages/outgoing/content/SessionEstablishedMessage.ts +++ b/ts/session/messages/outgoing/content/SessionEstablishedMessage.ts @@ -2,6 +2,7 @@ import { ContentMessage } from './ContentMessage'; import { SignalService } from '../../../../protobuf'; import * as crypto from 'crypto'; import { MessageParams } from '../Message'; +import { Constants } from '../../..'; export class SessionEstablishedMessage extends ContentMessage { public readonly padding: Buffer; @@ -18,7 +19,7 @@ export class SessionEstablishedMessage extends ContentMessage { this.padding = crypto.randomBytes(paddingLength); } public ttl(): number { - return 5 * 60 * 1000; + return Constants.TTL_DEFAULT.SESSION_ESTABLISHED; } protected contentProto(): SignalService.Content { diff --git a/ts/session/messages/outgoing/content/SessionRequestMessage.ts b/ts/session/messages/outgoing/content/SessionRequestMessage.ts index b7c0be756..765be235b 100644 --- a/ts/session/messages/outgoing/content/SessionRequestMessage.ts +++ b/ts/session/messages/outgoing/content/SessionRequestMessage.ts @@ -1,6 +1,7 @@ import { ContentMessage } from './ContentMessage'; import { SignalService } from '../../../../protobuf'; import { MessageParams } from '../Message'; +import { Constants } from '../../..'; export interface PreKeyBundleType { identityKey: Uint8Array; @@ -17,7 +18,6 @@ interface SessionRequestParams extends MessageParams { } export class SessionRequestMessage extends ContentMessage { - public static readonly ttl = 4 * 24 * 60 * 60 * 1000; // 4 days private readonly preKeyBundle: PreKeyBundleType; constructor(params: SessionRequestParams) { @@ -25,8 +25,14 @@ export class SessionRequestMessage extends ContentMessage { this.preKeyBundle = params.preKeyBundle; } + public static defaultTTL(): number { + // Gets the default TTL for Session Request, as + // public static readonly ttl: number <-- cannot be assigned a non-literal + return Constants.TTL_DEFAULT.SESSION_REQUEST; + } + public ttl(): number { - return SessionRequestMessage.ttl; + return SessionRequestMessage.defaultTTL(); } protected getPreKeyBundleMessage(): SignalService.PreKeyBundleMessage { diff --git a/ts/session/messages/outgoing/content/TypingMessage.ts b/ts/session/messages/outgoing/content/TypingMessage.ts index 3c1f247bd..37e91dc77 100644 --- a/ts/session/messages/outgoing/content/TypingMessage.ts +++ b/ts/session/messages/outgoing/content/TypingMessage.ts @@ -1,9 +1,9 @@ import { ContentMessage } from './ContentMessage'; import { SignalService } from '../../../../protobuf'; -import { TextEncoder } from 'util'; import { MessageParams } from '../Message'; import { StringUtils } from '../../../utils'; import { PubKey } from '../../../types'; +import { Constants } from '../../..'; interface TypingMessageParams extends MessageParams { isTyping: boolean; @@ -26,7 +26,7 @@ export class TypingMessage extends ContentMessage { } public ttl(): number { - return 60 * 1000; // 1 minute for typing indicators + return Constants.TTL_DEFAULT.TYPING_MESSAGE; } protected contentProto(): SignalService.Content { diff --git a/ts/session/messages/outgoing/content/data/DeviceUnlinkMessage.ts b/ts/session/messages/outgoing/content/data/DeviceUnlinkMessage.ts index 3b6bd99e2..4cba93a08 100644 --- a/ts/session/messages/outgoing/content/data/DeviceUnlinkMessage.ts +++ b/ts/session/messages/outgoing/content/data/DeviceUnlinkMessage.ts @@ -1,9 +1,10 @@ import { DataMessage } from './DataMessage'; import { SignalService } from '../../../../../protobuf'; +import { Constants } from '../../../..'; export class DeviceUnlinkMessage extends DataMessage { public ttl(): number { - return 4 * 24 * 60 * 60 * 1000; // 4 days for device unlinking + return Constants.TTL_DEFAULT.DEVICE_UNPAIRING; } public dataProto(): SignalService.DataMessage { diff --git a/ts/session/messages/outgoing/content/link/DeviceLinkGrantMessage.ts b/ts/session/messages/outgoing/content/link/DeviceLinkGrantMessage.ts index a07217104..af4750782 100644 --- a/ts/session/messages/outgoing/content/link/DeviceLinkGrantMessage.ts +++ b/ts/session/messages/outgoing/content/link/DeviceLinkGrantMessage.ts @@ -25,6 +25,13 @@ export class DeviceLinkGrantMessage extends DeviceLinkRequestMessage { requestSignature: params.requestSignature, }); + if (!(params.lokiProfile.profileKey instanceof Uint8Array)) { + throw new TypeError('profileKey must be of type Uint8Array'); + } + if (!(params.grantSignature instanceof Uint8Array)) { + throw new TypeError('grantSignature must be of type Uint8Array'); + } + this.displayName = params.lokiProfile.displayName; this.avatarPointer = params.lokiProfile.avatarPointer; this.profileKey = params.lokiProfile.profileKey; diff --git a/ts/session/messages/outgoing/content/link/DeviceLinkRequestMessage.ts b/ts/session/messages/outgoing/content/link/DeviceLinkRequestMessage.ts index 945f9f00e..86454c786 100644 --- a/ts/session/messages/outgoing/content/link/DeviceLinkRequestMessage.ts +++ b/ts/session/messages/outgoing/content/link/DeviceLinkRequestMessage.ts @@ -1,6 +1,7 @@ import { ContentMessage } from '../ContentMessage'; import { SignalService } from '../../../../../protobuf'; import { MessageParams } from '../../Message'; +import { Constants } from '../../../..'; export interface DeviceLinkMessageParams extends MessageParams { primaryDevicePubKey: string; secondaryDevicePubKey: string; @@ -14,13 +15,23 @@ export class DeviceLinkRequestMessage extends ContentMessage { constructor(params: DeviceLinkMessageParams) { super({ timestamp: params.timestamp, identifier: params.identifier }); + + if (!(params.requestSignature instanceof Uint8Array)) { + throw new TypeError('requestSignature must be of type Uint8Array'); + } + if (typeof params.primaryDevicePubKey !== 'string') { + throw new TypeError('primaryDevicePubKey must be of type string'); + } + if (typeof params.secondaryDevicePubKey !== 'string') { + throw new TypeError('secondaryDevicePubKey must be of type string'); + } this.primaryDevicePubKey = params.primaryDevicePubKey; this.secondaryDevicePubKey = params.secondaryDevicePubKey; this.requestSignature = params.requestSignature; } public ttl(): number { - return 2 * 60 * 1000; // 2 minutes for pairing requests + return Constants.TTL_DEFAULT.PAIRING_REQUEST; } protected getDataMessage(): SignalService.DataMessage | undefined { diff --git a/ts/session/protocols/SessionProtocol.ts b/ts/session/protocols/SessionProtocol.ts index 0af4690d8..8e15fa875 100644 --- a/ts/session/protocols/SessionProtocol.ts +++ b/ts/session/protocols/SessionProtocol.ts @@ -81,7 +81,7 @@ export class SessionProtocol { const now = Date.now(); const sentTimestamps = Object.entries(this.sentSessionsTimestamp); const promises = sentTimestamps.map(async ([device, sent]) => { - const expireTime = sent + SessionRequestMessage.ttl; + const expireTime = sent + SessionRequestMessage.defaultTTL(); // Check if we need to send a session request if (now < expireTime) { return; diff --git a/ts/session/snode_api/serviceNodeAPI.ts b/ts/session/snode_api/serviceNodeAPI.ts index 2742fa9a4..3d8aa459f 100644 --- a/ts/session/snode_api/serviceNodeAPI.ts +++ b/ts/session/snode_api/serviceNodeAPI.ts @@ -298,8 +298,6 @@ export async function storeOnNode( return false; } - const res = snodeRes as any; - const json = JSON.parse(snodeRes.body); // Make sure we aren't doing too much PoW const currentDifficulty = window.storage.get('PoWDifficulty', null); diff --git a/ts/session/utils/Number.ts b/ts/session/utils/Number.ts new file mode 100644 index 000000000..bc4f4267f --- /dev/null +++ b/ts/session/utils/Number.ts @@ -0,0 +1,47 @@ +type TimeUnit = + | 'second' + | 'seconds' + | 'minute' + | 'minutes' + | 'hour' + | 'hours' + | 'day' + | 'days'; + +export const timeAsMs = (value: number, unit: TimeUnit) => { + // Converts a time to milliseconds + // Valid units: second, minute, hour, day + const unitAsSingular = unit.replace(new RegExp('s?$'), ''); + + switch (unitAsSingular) { + case 'second': + return value * 1000; + case 'minute': + return value * 60 * 1000; + case 'hour': + return value * 60 * 60 * 1000; + case 'day': + return value * 24 * 60 * 60 * 1000; + default: + return value; + } +}; + +export const msAsUnit = (value: number, unit: TimeUnit) => { + // Converts milliseconds to your preferred unit + // Valid units: second(s), minute(s), hour(s), day(s) + const unitAsSingular = unit.replace(new RegExp('s?$'), ''); + + switch (unitAsSingular) { + case 'second': + return value / 1000; + case 'minute': + return value / 60 / 1000; + case 'hour': + return value / 60 / 60 / 1000; + case 'day': + return value / 24 / 60 / 60 / 1000; + default: + return value; + } +}; diff --git a/ts/session/utils/index.ts b/ts/session/utils/index.ts index 5548d7125..d9cafc0fa 100644 --- a/ts/session/utils/index.ts +++ b/ts/session/utils/index.ts @@ -2,6 +2,7 @@ import * as MessageUtils from './Messages'; import * as GroupUtils from './Groups'; import * as SyncMessageUtils from './SyncMessage'; import * as StringUtils from './String'; +import * as NumberUtils from './Number'; import * as PromiseUtils from './Promise'; import * as ProtobufUtils from './Protobuf'; @@ -14,6 +15,7 @@ export { SyncMessageUtils, GroupUtils, StringUtils, + NumberUtils, PromiseUtils, ProtobufUtils, }; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 80c2ddc47..ed24d82ee 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -54,6 +54,7 @@ export type ConversationType = { isTyping: boolean; isSecondary?: boolean; primaryDevice: string; + isBlocked: boolean; }; export type ConversationLookupType = { [key: string]: ConversationType; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 786340d27..35f19a9f0 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -11,6 +11,7 @@ import { import { getIntl, getRegionCode, getUserNumber } from './user'; import { PropsData as ConversationListItemPropsType } from '../../components/ConversationListItem'; +import { BlockedNumberController } from '../../util'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; @@ -119,6 +120,16 @@ export const _getLeftPaneLists = ( isSelected: true, }; } + const isBlocked = + BlockedNumberController.isBlocked(conversation.primaryDevice) || + BlockedNumberController.isGroupBlocked(conversation.id); + + if (isBlocked) { + conversation = { + ...conversation, + isBlocked: true, + }; + } // Add Open Group to list as soon as the name has been set if ( diff --git a/ts/test/session/messages/ChatMessage_test.ts b/ts/test/session/messages/ChatMessage_test.ts index 9ca1c0ffc..8e807f7b8 100644 --- a/ts/test/session/messages/ChatMessage_test.ts +++ b/ts/test/session/messages/ChatMessage_test.ts @@ -9,6 +9,7 @@ import { import { SignalService } from '../../../protobuf'; import { TextEncoder } from 'util'; import { toNumber } from 'lodash'; +import { Constants } from '../../../session'; describe('ChatMessage', () => { it('can create empty message with just a timestamp', () => { @@ -128,11 +129,11 @@ describe('ChatMessage', () => { expect(firstAttachment?.url).to.be.deep.equal('url'); }); - it('ttl of 1 day', () => { + it('correct ttl', () => { const message = new ChatMessage({ timestamp: Date.now(), }); - expect(message.ttl()).to.equal(24 * 60 * 60 * 1000); + expect(message.ttl()).to.equal(Constants.TTL_DEFAULT.REGULAR_MESSAGE); }); it('has an identifier', () => { diff --git a/ts/test/session/messages/ClosedGroupChatMessage_test.ts b/ts/test/session/messages/ClosedGroupChatMessage_test.ts index b7b93115e..4b6408aae 100644 --- a/ts/test/session/messages/ClosedGroupChatMessage_test.ts +++ b/ts/test/session/messages/ClosedGroupChatMessage_test.ts @@ -9,6 +9,7 @@ import { TextEncoder } from 'util'; import { TestUtils } from '../../test-utils'; import { StringUtils } from '../../../session/utils'; import { PubKey } from '../../../session/types'; +import { Constants } from '../../../session'; describe('ClosedGroupChatMessage', () => { let groupId: PubKey; @@ -44,7 +45,7 @@ describe('ClosedGroupChatMessage', () => { .to.be.equal(chatMessage.timestamp); }); - it('ttl of 1 day', () => { + it('correct ttl', () => { const chatMessage = new ChatMessage({ timestamp: Date.now(), }); @@ -52,7 +53,7 @@ describe('ClosedGroupChatMessage', () => { groupId, chatMessage, }); - expect(message.ttl()).to.equal(24 * 60 * 60 * 1000); + expect(message.ttl()).to.equal(Constants.TTL_DEFAULT.REGULAR_MESSAGE); }); it('has an identifier', () => { diff --git a/ts/test/session/messages/DeviceLinkMessage_test.ts b/ts/test/session/messages/DeviceLinkMessage_test.ts index c7d3c8118..785bfa018 100644 --- a/ts/test/session/messages/DeviceLinkMessage_test.ts +++ b/ts/test/session/messages/DeviceLinkMessage_test.ts @@ -7,6 +7,7 @@ import { } from '../../../session/messages/outgoing'; import { SignalService } from '../../../protobuf'; import { LokiProfile } from '../../../types/Message'; +import { Constants } from '../../../session'; describe('DeviceLinkMessage', () => { let linkRequestMessage: DeviceLinkRequestMessage; @@ -115,9 +116,13 @@ describe('DeviceLinkMessage', () => { }); }); - it('ttl of 2 minutes', () => { - expect(linkRequestMessage.ttl()).to.equal(2 * 60 * 1000); - expect(linkGrantMessage.ttl()).to.equal(2 * 60 * 1000); + it('correct ttl', () => { + expect(linkRequestMessage.ttl()).to.equal( + Constants.TTL_DEFAULT.PAIRING_REQUEST + ); + expect(linkGrantMessage.ttl()).to.equal( + Constants.TTL_DEFAULT.PAIRING_REQUEST + ); }); it('has an identifier', () => { diff --git a/ts/test/session/messages/DeviceUnlinkMessage_test.ts b/ts/test/session/messages/DeviceUnlinkMessage_test.ts index f29bea841..a26e1ba53 100644 --- a/ts/test/session/messages/DeviceUnlinkMessage_test.ts +++ b/ts/test/session/messages/DeviceUnlinkMessage_test.ts @@ -3,6 +3,7 @@ import { beforeEach } from 'mocha'; import { DeviceUnlinkMessage } from '../../../session/messages/outgoing'; import { SignalService } from '../../../protobuf'; +import { Constants } from '../../../session'; describe('DeviceUnlinkMessage', () => { let message: DeviceUnlinkMessage; @@ -21,8 +22,8 @@ describe('DeviceUnlinkMessage', () => { ); }); - it('ttl of 4 days', () => { - expect(message.ttl()).to.equal(4 * 24 * 60 * 60 * 1000); + it('correct ttl', () => { + expect(message.ttl()).to.equal(Constants.TTL_DEFAULT.DEVICE_UNPAIRING); }); it('has an identifier', () => { diff --git a/ts/test/session/messages/EndSessionMessage_test.ts b/ts/test/session/messages/EndSessionMessage_test.ts index 4b6e864de..53e49f8df 100644 --- a/ts/test/session/messages/EndSessionMessage_test.ts +++ b/ts/test/session/messages/EndSessionMessage_test.ts @@ -4,6 +4,7 @@ import { beforeEach } from 'mocha'; import { EndSessionMessage } from '../../../session/messages/outgoing'; import { SignalService } from '../../../protobuf'; import { TextEncoder } from 'util'; +import { Constants } from '../../../session'; describe('EndSessionMessage', () => { let message: EndSessionMessage; @@ -64,8 +65,8 @@ describe('EndSessionMessage', () => { expect(decoded.dataMessage).to.have.deep.property('body', 'TERMINATE'); }); - it('ttl of 4 days', () => { - expect(message.ttl()).to.equal(4 * 24 * 60 * 60 * 1000); + it('correct ttl', () => { + expect(message.ttl()).to.equal(Constants.TTL_DEFAULT.END_SESSION_MESSAGE); }); it('has an identifier', () => { diff --git a/ts/test/session/messages/GroupInvitationMessage_test.ts b/ts/test/session/messages/GroupInvitationMessage_test.ts index 931daa504..5dddd2ecb 100644 --- a/ts/test/session/messages/GroupInvitationMessage_test.ts +++ b/ts/test/session/messages/GroupInvitationMessage_test.ts @@ -3,6 +3,7 @@ import { beforeEach } from 'mocha'; import { GroupInvitationMessage } from '../../../session/messages/outgoing'; import { SignalService } from '../../../protobuf'; +import { Constants } from '../../../session'; describe('GroupInvitationMessage', () => { let message: GroupInvitationMessage; @@ -38,8 +39,8 @@ describe('GroupInvitationMessage', () => { ); }); - it('ttl of 1 day', () => { - expect(message.ttl()).to.equal(24 * 60 * 60 * 1000); + it('correct ttl', () => { + expect(message.ttl()).to.equal(Constants.TTL_DEFAULT.REGULAR_MESSAGE); }); it('has an identifier', () => { diff --git a/ts/test/session/messages/ReceiptMessage_test.ts b/ts/test/session/messages/ReceiptMessage_test.ts index f5e3391a5..9643f92a9 100644 --- a/ts/test/session/messages/ReceiptMessage_test.ts +++ b/ts/test/session/messages/ReceiptMessage_test.ts @@ -7,6 +7,7 @@ import { } from '../../../session/messages/outgoing'; import { SignalService } from '../../../protobuf'; import { toNumber } from 'lodash'; +import { Constants } from '../../../session'; describe('ReceiptMessage', () => { let readMessage: ReadReceiptMessage; @@ -42,9 +43,11 @@ describe('ReceiptMessage', () => { expect(decodedTimestamps).to.deep.equal(timestamps); }); - it('ttl of 1 day', () => { - expect(readMessage.ttl()).to.equal(24 * 60 * 60 * 1000); - expect(deliveryMessage.ttl()).to.equal(24 * 60 * 60 * 1000); + it('correct ttl', () => { + expect(readMessage.ttl()).to.equal(Constants.TTL_DEFAULT.REGULAR_MESSAGE); + expect(deliveryMessage.ttl()).to.equal( + Constants.TTL_DEFAULT.REGULAR_MESSAGE + ); }); it('has an identifier', () => { diff --git a/ts/test/session/messages/SessionEstablishedMessage_test.ts b/ts/test/session/messages/SessionEstablishedMessage_test.ts index 40d672bb8..270c4364d 100644 --- a/ts/test/session/messages/SessionEstablishedMessage_test.ts +++ b/ts/test/session/messages/SessionEstablishedMessage_test.ts @@ -3,6 +3,7 @@ import { beforeEach } from 'mocha'; import { SessionEstablishedMessage } from '../../../session/messages/outgoing'; import { SignalService } from '../../../protobuf'; +import { Constants } from '../../../session'; describe('SessionEstablishedMessage', () => { let message: SessionEstablishedMessage; @@ -21,8 +22,8 @@ describe('SessionEstablishedMessage', () => { ); }); - it('ttl of 5 minutes', () => { - expect(message.ttl()).to.equal(5 * 60 * 1000); + it('correct ttl', () => { + expect(message.ttl()).to.equal(Constants.TTL_DEFAULT.SESSION_ESTABLISHED); }); it('has an identifier', () => { diff --git a/ts/test/session/messages/SessionResetMessage_test.ts b/ts/test/session/messages/SessionResetMessage_test.ts index 6f7bb758d..2013e380b 100644 --- a/ts/test/session/messages/SessionResetMessage_test.ts +++ b/ts/test/session/messages/SessionResetMessage_test.ts @@ -4,6 +4,7 @@ import { beforeEach } from 'mocha'; import { SessionRequestMessage } from '../../../session/messages/outgoing'; import { SignalService } from '../../../protobuf'; import { TextDecoder, TextEncoder } from 'util'; +import { Constants } from '../../../session'; describe('SessionRequestMessage', () => { let message: SessionRequestMessage; @@ -64,8 +65,8 @@ describe('SessionRequestMessage', () => { ); }); - it('ttl of 4 days', () => { - expect(message.ttl()).to.equal(4 * 24 * 60 * 60 * 1000); + it('correct ttl', () => { + expect(message.ttl()).to.equal(Constants.TTL_DEFAULT.SESSION_REQUEST); }); it('has an identifier', () => { diff --git a/ts/test/session/messages/TypingMessage_test.ts b/ts/test/session/messages/TypingMessage_test.ts index ad14f4961..f89a19abc 100644 --- a/ts/test/session/messages/TypingMessage_test.ts +++ b/ts/test/session/messages/TypingMessage_test.ts @@ -7,6 +7,7 @@ import Long from 'long'; import { toNumber } from 'lodash'; import { StringUtils } from '../../../session/utils'; import { TestUtils } from '../../test-utils'; +import { Constants } from '../../../session'; describe('TypingMessage', () => { it('has Action.STARTED if isTyping = true', () => { @@ -79,12 +80,12 @@ describe('TypingMessage', () => { ); }); - it('ttl of 1 minute', () => { + it('correct ttl', () => { const message = new TypingMessage({ timestamp: Date.now(), isTyping: true, }); - expect(message.ttl()).to.equal(60 * 1000); + expect(message.ttl()).to.equal(Constants.TTL_DEFAULT.TYPING_MESSAGE); }); it('has an identifier', () => { diff --git a/ts/test/session/protocols/SessionProtocol_test.ts b/ts/test/session/protocols/SessionProtocol_test.ts index 2ce8ccaf4..e836c4488 100644 --- a/ts/test/session/protocols/SessionProtocol_test.ts +++ b/ts/test/session/protocols/SessionProtocol_test.ts @@ -124,7 +124,7 @@ describe('SessionProtocol', () => { }); // Set the time just before expiry - clock.tick(SessionRequestMessage.ttl - 100); + clock.tick(SessionRequestMessage.defaultTTL() - 100); await SessionProtocol.checkSessionRequestExpiry(); expect(getItemById.calledWith('sentSessionsTimestamp')); @@ -140,7 +140,7 @@ describe('SessionProtocol', () => { }); // Expire the request - clock.tick(SessionRequestMessage.ttl + 100); + clock.tick(SessionRequestMessage.defaultTTL() + 100); await SessionProtocol.checkSessionRequestExpiry(); expect(getItemById.calledWith('sentSessionsTimestamp')); @@ -159,7 +159,7 @@ describe('SessionProtocol', () => { sandbox.stub(SessionProtocol, 'sendSessionRequestIfNeeded').resolves(); // Expire the request - clock.tick(SessionRequestMessage.ttl + 100); + clock.tick(SessionRequestMessage.defaultTTL() + 100); await SessionProtocol.checkSessionRequestExpiry(); expect(getItemById.calledWith('sentSessionsTimestamp')); diff --git a/ts/test/state/selectors/conversations_test.ts b/ts/test/state/selectors/conversations_test.ts index f771aebe4..d29c91ae0 100644 --- a/ts/test/state/selectors/conversations_test.ts +++ b/ts/test/state/selectors/conversations_test.ts @@ -29,6 +29,7 @@ describe('state/selectors/conversations', () => { mentionedUs: false, isSelected: false, isTyping: false, + isBlocked: false, }, id2: { id: 'id2', @@ -47,6 +48,7 @@ describe('state/selectors/conversations', () => { mentionedUs: false, isSelected: false, isTyping: false, + isBlocked: false, }, id3: { id: 'id3', @@ -65,6 +67,7 @@ describe('state/selectors/conversations', () => { mentionedUs: false, isSelected: false, isTyping: false, + isBlocked: false, }, id4: { id: 'id4', @@ -83,6 +86,7 @@ describe('state/selectors/conversations', () => { mentionedUs: false, isSelected: false, isTyping: false, + isBlocked: false, }, id5: { id: 'id5', @@ -101,6 +105,7 @@ describe('state/selectors/conversations', () => { mentionedUs: false, isSelected: false, isTyping: false, + isBlocked: false, }, }; const comparator = _getConversationComparator(i18n, regionCode); diff --git a/ts/util/blockedNumberController.ts b/ts/util/blockedNumberController.ts index baaabe7d4..d4f9538bd 100644 --- a/ts/util/blockedNumberController.ts +++ b/ts/util/blockedNumberController.ts @@ -89,6 +89,26 @@ export class BlockedNumberController { } } + public static async setBlocked( + user: string | PubKey, + blocked: boolean + ): Promise { + if (blocked) { + return BlockedNumberController.block(user); + } + return BlockedNumberController.unblock(user); + } + + public static async setGroupBlocked( + groupId: string | PubKey, + blocked: boolean + ): Promise { + if (blocked) { + return BlockedNumberController.blockGroup(groupId); + } + return BlockedNumberController.unblockGroup(groupId); + } + public static async blockGroup(groupId: string | PubKey): Promise { await this.load(); const id = PubKey.cast(groupId);